Hatena::ブログ(Diary)

かみやんの技術者日記 このページをアンテナに追加 RSSフィード

2013-02-17 [Web] Webシステムのフロントエンド高速化で最初にやるべきこと このエントリーを含むブックマーク このエントリーのブックマークコメント

前回のエントリーで、Dartの次は、TypeScriptを検証する。と書いたけど、なぜか自分のPCでは、VisualStudio for WebにTypeScriptプラグインインストールできなかったので、TypeScriptを使うことを諦めました。コマンドラインコンパイルはできたけど、それでは型付け言語のメリットであるIDEによる補完や参照検索やリネームリファクタリングが効かないので。ちなみにプログラマのPCではあっさりインストールできたとのこと。がっくり。

というわけで、Dartを実戦投入することを決定してDartで開発をしています。

フロントエンド高速化のExpiresヘッダ

さて、今日の本題。Webシステムのフロントエンド高速化のお話です。Webシステムの速度の大きなボトルネックとしてDB負荷がありますが、ブラウザ側のレンダリングを高速化する話としてフロントエンド高速化があります。一般には、

などがあります。が、もっとも効果的なのは最後のExpiresヘッダの追加ではないか。と。特にスマホなどのモバイル端末からのアクセスなど回線が遅い場合には、絶大な効果を発揮するはず。と。

とにかく俺は高速化が大好きなのだ!!

Expiresヘッダとは

HTTPサーバがレスポンスを返すときに、

Expires: Sun, 17 Feb 2014 22:28:04 JST

のような日付を返すと、その時刻まではこのコンテントは変更しないよ。とブラウザに伝えるもので、上記だと「2014/2/17」まで変更しないよ。と伝えています。つまり、今から1年後を指定しています。

通常ブラウザは、Expiresの期限が来るまではコンテントをストレージキャッシュし、二度とサーバに同じURLをリクエストしません(キャッシュがいっぱいになったので消すとかがなければ)。

ブラウザHTTPリクエストを送らなくなるので、通信時間は減り、サーバが応対する必要がなく、AWSのようにトラフィック料がかかるサーバ費用も抑えられます。

という訳でExpiresヘッダの威力は絶大だ!

Apacheの場合

mod_expiresを追加して、

ExpiresDefault "access plus 1 years"

を書くだけでOKです。

MIME-Type別に指定する場合は、

ExpiresByType text/html "access plus 1 years"

のようにExpiresByTypeを利用すればOKです。

ただ、MIME-Typeだけでは静的コンテンツと動的コンテンツの区別がつかないので、僕は、JavaServlet側で対応しました(PHPとかRubyとかでも同様でしょう)。

コンテントの更新に対応

Expiresヘッダを追加した場合、二度とブラウザが同じURLをアクセスして来なくなる可能性が高いため、静的コンテントを更新したときに問題になります。その場合は、URLを変えるしかありません。通常はファイルのバージョン番号を入れるか最終更新日を入れるかです。

<img src="hello.png">

と書く代わりに

<img src="hello.png?20130217">

などのように書くということです。上記の20130217は、当然最終更新日が「2013/02/17」という意味です。hello.pngを更新する度にjspの?20130217を修正していくのは大変すぎるので、

<img src=<%=Resource.get("hello.png")%> >

というようにResource.get()を通すようにしました。Resourceクラスは、自作のクラスで静的コンテントのURLを入力しその最終更新日を「{URL}?lm=YYYYMMddHHmmss」形式で返すものです。

Resource#get()メソッドが最終更新日を得る方法ですが、デバッグモードとリリースモードの2つを用意しました。デバッグモードでは、リクエストされたURLが存在するローカルのファイルの最終更新日をみてそれを返します。

リリースモードでは、事前にバッチファイルで作成したlastModified.propertiesファイルを参照し高速にレスポンスするものです。lastModified.propertiesファイルは、キーがURL、値がYYYYMMddHHmmss形式の更新日になっています。本番環境にデプロイするときにバッチを起動してlastModified.propertiesを更新します。lastModified.propertiesファイルは更新されるとTomcatが感知して自動リロードしてくれます。それ以外のときのpropertiesファイルの参照ではJavaライブラリ内でpropertiesファイルをキャッシュしてくれていてメモリアクセスしかないので高速です。

拡張子

ちなみに僕が管理しているプロジェクトでは、静的コンテンツは*.css, *.js, *.pngなどのように通常の拡張子を使い、動的コンテンツは実態がServletでもJSPでもレスポンスがtext/htmlでもimage/pngでもtext/cssでもtext/javascriptでも拡張子を*.jspとしている。

CDN(コンテンツデリバネットワーク)などのキャッシュを効かせる時にも動的か静的かで分けたほうがよいので。

ベンチマーク

高速化を組み込んだ結果、

内容高速化前高速化後
3Gでのページが表示されるまでの時間9秒1秒
LANでのページが表示されるまでの時間700ms200ms
転送量1Mbyte70kbyte

という訳で劇的に高速化されました。ちなみに転送量70kbyteというのは静的コンテンツがすべてキャッシュにヒットした場合(つまり初めてサイトに訪れたときではなく2度目以降)です。転送量70kbyteというのは、すべてjspがレスポンスしたHTMLで、ここをgzip圧縮すると9kbyteに減りました。画像レスポンスはゼロに。

つまりgzip圧縮をして70kbyteが9kbyteになっても1Mbyteの画像が減らなければ効果なし。画像レスポンスを0にした上でgzip圧縮は効果あり。と。

あと転送量の影響はすくないもののcssキャッシュヒットもページレンダリングに大きく影響します。なにせcssファイルが届かなければほぼレンダリングは進まないので。

高速化!高速化!高速化!

Resource.get()のソース

Resource.get()の実体は、下記のような感じ。

public class Resource {
    /**
     * URLを入力してYYYYMMddHHmmss形式の日付を返す<br>
     * 例) 入力 css/base.css 出力 201302101415
     * 
     * @param uri
     * @return みつからないときは、""
     */
    public static String getDate(String uri) {
        ResourceBundle settings = ResourceBundle.getBundle("settings");
        if ("true".equalsIgnoreCase(settings.getString("debug.mode")) == false) {// リリースモードのとき
            ResourceBundle lm = ResourceBundle.getBundle("lastModified");
            if (lm.containsKey(uri)) {
                return lm.getString(uri);
            } else {
                ErrorUtil.sendInfo(uri
                        + " is not found in lastModified.properties.");
                return "";
            }
        } else {// デバッグモードのとき
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("YYYYMMddHHmmss");
                String path = settings.getString("servlet.context") + '/' + uri;
                Path p = FileSystems.getDefault().getPath(path);
                FileTime t = Files.getLastModifiedTime(p);
                Date d = new Date(t.toMillis());
                return sdf.format(d);
            } catch (IOException e) {
                ErrorUtil.sendInfo(uri
                        + " is not found at Resource#getDate() in debug mode.");
                return "";
            }
        }
    }

    /**
     * URLを入力して、 "{url}?YYYYMMddHHmmss" を返す(ダブルクォートあり版)<br>
     * 例) 入力 css/base.css 出力 "css/base.css?lm=201302101415"
     * 
     * @param uri
     * @return
     */
    public static String get(String uri) {
        String date = getDate(uri);
        return "\"" + (date.length() > 0 ? uri + "?lm=" + date : uri) + "\"";
    }
}

StaticResourceServlet.java

〜.cssや〜.pngや〜.jsの静的コンテンツをレスポンスするServletのソースは下記になります。

@WebServlet(urlPatterns = { "*.css", "*.js", "*.png", "*.jpg", "*.jpeg", "*.gif" })
public class StaticResourceServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        try {
            doNext(request, response);
        } catch (Exception e) {
            ErrorUtil.sendError(e);
            throw new ServletException();
        }
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        try {
            doNext(request, response);
        } catch (Exception e) {
            ErrorUtil.sendError(e);
            throw new ServletException();
        }
    }

    /**
     * メイン処理
     * 
     * @param request
     * @param response
     * @throws Exception
     */
    private void doNext(HttpServletRequest req, HttpServletResponse res)
            throws Exception {
        String uri = req.getRequestURI();
        String contextPath = req.getServletContext().getContextPath() + '/';
        String fname = uri.substring(contextPath.length());
        String realPath = req.getServletContext().getRealPath("")
                + File.separator + fname;
        if (uri.indexOf("WEB-INF") != -1) {// WEB-INFが含まれているとき
            throw new Exception("URLにWEB-INFが含まれています");
        }
        if (uri.indexOf("..") != -1) {// ..が含まれているとき
            throw new Exception("URLに..が含まれています");
        }
        if (uri.endsWith(".js")) {// JavaScriptのとき
            res.setCharacterEncoding("UTF-8");
            res.setContentType("text/javascript");
        } else if (uri.endsWith(".css")) {// CSSのとき
            res.setCharacterEncoding("UTF-8");
            res.setContentType("text/css");
        } else if (uri.endsWith(".png")) {// pngのとき
            res.setContentType("image/png");
        } else if (uri.endsWith(".jpg") || uri.endsWith(".jpeg")) {// jpegのとき
            res.setContentType("image/jpeg");
        } else if (uri.endsWith(".gif")) {// gifのとき
            res.setContentType("image/gif");
        } else {
            throw new Exception("サポートしていない拡張子");
        }
        ResponseUtil.setExpiresHeader(res);// Expiresヘッダ
        if (req.getParameter("lm") == null) {// lmパラメータがないとき
            ErrorUtil
                    .sendInfo(uri
                            + "のリクエストでlmパラメータがついていません。Expiresヘッダ高速化に必要です。 at StaticResourceServlet");
        }
        ByteArrayOutputStream baos = null;
        BufferedInputStream bis = null;
        ByteArrayInputStream bais = null;
        try {
            baos = new ByteArrayOutputStream();
            bis = new BufferedInputStream(new FileInputStream(realPath));
            // ファイルからメモリへコピー
            byte[] buf = new byte[4096];
            int size;
            while ((size = bis.read(buf)) >= 0) {
                baos.write(buf, 0, size);
            }
            // メモリからレスポンスへ出力
            res.setContentLength(baos.size());// Content-Lengthヘッダ
            bais = new ByteArrayInputStream(baos.toByteArray());
            ServletOutputStream out = res.getOutputStream();
            while ((size = bais.read(buf)) >= 0) {
                out.write(buf, 0, size);
            }
            out.flush();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (Exception e) {
                }
            }
            if (baos != null) {
                try {
                    baos.close();
                } catch (Exception e) {
                }
            }
            if (bais != null) {
                try {
                    bais.close();
                } catch (Exception e) {
                }
            }
        }
    }
}

その他のパターン

ファイルシステム上にそのままファイルが置かれている静的コンテンツ以外に、DBに静的コンテンツがある場合は、DBのフィールドにLastUpdateなどのTIMESTAMP型を用意して、Resource.get()で取れるようにしています。

あと、JavaScriptから静的コンテンツの最終更新日を取る部分では、JavaScript版のResource.get()相当のメソッド(内容はURLと更新日の連想配列データを埋め込んだもの)をサーバがレスポンスするようにしています。

最初にやりましょう!

この仕組みですが、プロジェクトの開発終了直前でやるはめになりましたが、Resource.get()に書き換え箇所が膨大だったため、死にそうでした。

死にそうでもやる価値はあるので皆さんやった方がよいですが、できればプロジェクトの開始時に仕組みを導入しておいた方が賢明です。

高速化!高速化!高速化!

――
アプリ受託開発、Webシステム受託開発も行っています。以下自社製品。
URL:ibisMail Freeのダウンロード、レビューはこちら
URL:ibisMail for iPhoneダウンロード、レビューはこちら
URL:ibisMail for iPadダウンロード、レビューはこちら
URL:ibisPaintのダウンロード、レビューはこちら サイトは、ibispaint.com
ご意見ご要望はTwitterから: @kamiyan

2013-01-20 [dart] GoogleのDartを使ってみた このエントリーを含むブックマーク このエントリーのブックマークコメント

f:id:kamiyan2:20130121001103p:image

JavaScriptをたくさん使うプロジェクトの現状

JavaScriptをガンガン書かないといけないWebアプリは、ビジネスロジックをJavaServletで書いて(他の言語でもいいけど)、JSPへ転送して(他の言語でもテンプレートとか使うと思うけど)、JavaScriptクライアントに送って、クライアントでは、HTMLCSSでビジュアルがレイアウトされて、JavaScriptがビジュアルを動かしたり、AJAXでまたサーバにリクエストしたり、AJAXのレスポンスでまたビジュアルが変わったり。例えばレイアウトバグがあった場合、JavaServletをみたり、JSPをみたり、HTMLCSSみたり、JavaScriptの実行にエラーがないか確認したり、AJAXで得られたレスポンスの内容をみたりと、かなり開発環境としてカオスであり、不自然な訳です。

あと、JavaScriptのコード量が多くてチーム開発とかだと、コーディングルールをがっちり決めて、ソースコードレビューをしないとめちゃくちゃなコードを書く人が現れたりします。クラス定義の書き方がみんなバラバラだったり、コンストラクタ以外でメンバ変数が勝手に追加されていたり。

//とりあえずここに置いておく

みたいなコメントと共にソースコードの中ほどにメンバ変数が増やされていたりすると、もう死んでくれ!と叫びたくなる訳です。

そもそもJavaScriptのような静的な型がない言語は、IDEの補間が効かなかったり、参照検索がなかったり、実行時にならないと変数名やメソッド名を入力し間違えたことが分からなかったり、名前変更のリファクタリングが効かなかったりと、効率が悪いので好きではありません。

次に作るWebアプリは、JavaScriptに代わる言語で開発したいなと思い、ずっと、代わるものを検討、ウォッチしていました。Dart, Haxe, TypeScript の3つをウォッチしていたのだけど、Haxeは調査済みで一部のプロジェクトで使用し始めています(気が向いたらHaxeのエントリも書きます)。

今回は、GoogleDartを使ってみたのでそのレポート。

Dart言語の特徴

昨年秋に、安定板のDart Editorが出ました。

Dart Editor + Dart SDKは、 http://www.dartlang.org/ ここからダウンロードできます。Dartの特徴は、

という感じです。DartVMネイティブで動かせるのは、当面サーバサイドで使うときでしょう。普通は、dart2jsでJavaScriptに変換してWebブラウザの中で使うのが普通でしょう。

主な言語仕様

言語仕様の特徴としては、

  • クラスprototype baseは色々問題あるよねというところですね
  • クラスの外のトップレベル関数Javaと違ってクラスの外にも関数書けるよと
  • オプション引数:省略可能引数C++のように引数リストの後ろの方を省略する方法と、名前付き引数にしてどこを省略してもよい2通りがある
  • 文字列インターポレーションPerlのような"Hello, ${name}"みたいな置換ですね
  • マルチライン文字列Perlのヒアドキュメント($s=<<END;)みたいな文字列リテラルですね
  • 動的メソッドハンドリング:実行時にメソッドがあるか確認する(静的にチェックしてほしいのだが)
  • カスケード演算子(..)VBJavaScriptのwithみたいなものですね
  • オプショナルな型:小さなアプリをさっと書くときは型を書かない、ちゃんとしたアプリは、型を書く(私は厳密な型が欲しい)
  • レキシカルスコープ:ちゃんと{}のブロックが識別子のスコープですよ。クロージャも対応しているよ。ローカル変数キャプチャもしているよと。ま、JavaScriptだけが異常だとは思うが。
  • ライブラリライブラリを使うときも作るときも切り替えとかの修正不要。Pubというオープンなリポジトリがかなりお手軽で便利
  • Isolate(アイソレート):マルチスレッドでメモリを共有して管理するのは、開発もデバッグも大変。Isolate同士は、お互いのメモリにアクセスできないので安全

という感じ。ま、モダンな言語仕様ですね。使ってみた感想としては、やはりC++Javaが好きな僕としてはメソッドがあるかどうかを動的に判断するのが辛いか。一応、Dart Editorで黄色い下線がでて警告がでるときもあるけど。

あとは、メソッドオーバーロードができない。JavaScript風にしたのか、オプショナルな型のせいか、オーバーロードがあるとソースが読みにくいという意図なのか、JavaScriptコンパイルするときに美しくないからか。

あとは、intが初期値を代入していないときは、nullになったり。あたりがちょっと気になったところ。

classの例

// 形状インターフェース
abstract class Shape {// abstractが使える
  num perimeter();// num型は、int型とdouble型のsuperクラス
}

// 矩形クラス
class Rectangle implements Shape {//implementsでShapeインターフェースを実装
  final num height, width; //finalは初期値代入のみ可能で以降変更不可
  Rectangle(this.height, this.width);    // コンパクトコンストラクタ表記。引数でメンバー変数を代入するという意味。
  num perimeter() => 2*height + 2*width; // 関数の省略表記。関数本体が単文のときは、このように省略できる。
}

// 正方形クラス
class Square extends Rectangle {// extendsで矩形を継承(継承は1つまで)
  Square(num size) : super(size, size);//コンストラクタで親クラスのコンストラクタを呼ぶときはC++風にコロンを使う、superという予約語を使うところはJava風。
}

ま、これを見るだけでもモダンな感じですね。

Dart Editorの特徴

IDEであるDart Editorの特徴は、

  • コード補完:メンバ変数メソッド名が補完される
  • リファクタリング機能リネームとか
  • アウトラインビュー:クラスのメンバ変数メソッドがツリー表示
  • デバッガ:ブレイクポイント、変数を見る、ステップ実行
  • 静的解析:メンバ変数じゃないとか、代入してないとか静的解析で警告がでる
  • 呼び出し元検索:呼び出し元検索ですぐにそこを表示できる

という感じ。

使ってみた感想としては、コード補完が効かなくなることがあってDart Editorを再起動したら直ったとか、リファクタリングリネームは、ライブラリを超えては無理なのか、効かないときがあった。とか、呼び出し元検索は1度も動かなかった。など普段書いているJavaの安心感に比べると心もとない。

作ってみたアプリ

f:id:kamiyan2:20130120223716p:image

3時間くらいで作ってみたアプリが上記。htmlファイルをほぼ書かず、cssファイルをほぼ書かず、コードでウインドウを作ったり、コントロールを配置したりするWindow Toolkitっぽいもの。クライアント側は、html,cssのコードは書かずにdartだけ書いて、サーバサイドもcsshtmlはほとんど出力せずにjsonとかだけを出力するシステムをイメージしたもの。

なお、Pubというオープンなリポジトリがありpubコマンドというライブラリマネージャがあり、他の人が作ったライブラリインストールしたり、アップデートしたり、自分の作ったライブラリアップロードするのはとても簡単らしいが、pub installコマンドで、自分が書いていたライブラリが削除されるというミスをしてしまいました(そしてオレオレWindow Toolkit作り直し)。pubに関してはまだ調査不足なので、みなさん自分のライブラリを作っている場合は、pubコマンドの使用前にはバックアップしましょう。

#dartをしばらく調査したら、次はTypeScriptを調査予定〜

――
アプリ受託開発、Webシステム受託開発も行っています。以下自社製品。
URL:ibisMail Freeのダウンロード、レビューはこちら
URL:ibisMail for iPhoneダウンロード、レビューはこちら
URL:ibisMail for iPadダウンロード、レビューはこちら
URL:ibisPaintのダウンロード、レビューはこちら サイトは、ibispaint.com
ご意見ご要望はTwitterから: @kamiyan

2013-01-06 [AWS] AWSのEC2の自動リブート このエントリーを含むブックマーク このエントリーのブックマークコメント

AWSに引っ越したら、物理サーバでは発生しなかったフリーズ(CPU100%で張り付く)が、発生しました。

そこで、監視プログラムを緊急作成しました。

上記プログラムダウンロードは、 http://bit.ly/104LVnC こちらから。

AWS引っ越し、1か月目の課金が$5000オーバー。ここから削減して1/3ぐらいに減らしたい。ここからが腕の見せ所(^_^)/

――
アプリ受託開発、Webシステム受託開発も行っています。以下自社製品。
URL:ibisMail Freeのダウンロード、レビューはこちら
URL:ibisMail for iPhoneダウンロード、レビューはこちら
URL:ibisMail for iPadダウンロード、レビューはこちら
URL:ibisPaintのダウンロード、レビューはこちら サイトは、ibispaint.com
ご意見ご要望はTwitterから: @kamiyan

2012-12-17 [Server] ZABBIXのススメ このエントリーを含むブックマーク このエントリーのブックマークコメント

先週は、AWSへ引っ越しについてエントリを書いた。今週は、サービス監視について。

引っ越し前は、MRTGを使っていた。サービス監視というよりは負荷の可視化ですね。SNMP(Simple Network Management Protocol)を使って得られるメトリクスをグラフ化していました。CPU負荷、メモリ使用量、ディスク使用量、ネットワークトラフィック(通信量)、プロセス数などをdaily, weekly, monthlyでグラフ化します。グラフ化されていると、問題が起こったときに何が原因か、または、問題が起こる前に予測ができます。引っ越し前は、それとは別にサービス監視(死活監視)としてPerlスクリプトを使ってプロセスが生きているかを監視して死んでいたら再起動させるなどをしていました。

MRTG導入時は、7年ぐらい前だったので、そろそろ古めかしいシステムでなく近代的なものにしたいなと思って探したところZABBIXがよさげだったので導入しました。

f:id:kamiyan2:20121217001959p:image

上図が、ダッシュボードのトップ画面です。ホストをグループ分けして、どのグループに警告がでているか、エラー(エラーのときは赤くなる)が出ているかわかります。グループをクリックすればさらにホスト一覧へとドリルダウンできます。警告とするトリガーやエラーとするトリガーも設定できます。グループ管理できることで、数百台の場合でも管理できるようになっているのでしょう。また、大量のメトリクスやトリガーやイベントがテンプレートとしてデフォルトインストールされるので、ホストを追加したらテンプレートを追加するだけで、大量のメトリクスなどが一気に追加されて便利です。

f:id:kamiyan2:20121217002000p:image

上図が、CPU使用率です。Load Averageと違って、User Time、System Time、I/O Wait、Nice、Interrupt、Soft IRQ、Steal Timeなども色分けして表示してくれます。また、負荷が高そうな部分をズームしたい場合でもその辺りを横にドラッグして選択すると選択範囲が拡大されます。

MRTGよりよくなったこと

  1. MRTGではサーバ上の設定ファイルを作成、設定値の追加、編集が必要であり、ホストの追加・削除の度にサーバでの作業が必要であったが、ZABBIXではWebUIでホストの追加、メトリクスの追加・削除・編集が可能になった
  2. MRTGではメトリクスの可視化(グラフ化)のみであったが、ZABBIXはトリガとかイベントで通知を送る機能などがある
  3. MRTGではグラフのグループ化は、HTMLを書くなどの必要があったが、ZABBIXではWebUIでグループ化ができたり、グラフ一覧表ページ(Screenと呼ぶ)を追加・編集・削除ができる
  4. MRTGではdaily, weekly, monthlyと固定的であったが、ZABBIXではデータはRDBに保存されており(アイビスではMySQLを使用)、グラフのビューも表示スパンがWebUIで自由に変更できる

ZABBIXの使いにくいところ

いいことづくめにみえますが、使いにくいところもあります。

  1. Screenの編集が使いづらい。全体的にプルダウンを選択したら全画面リロードになって使いにくい。特にScreenの編集がつらい。色々なところをAJAXにして部分書き換えになってくれるとよい。

というところです。WebUIが重いと結局テキストファイルで編集した方が早いのか?とも思えてきます(が、テキストファイルはテキストファイルでミスしやすいのが問題なのだが)。一応、XMLでExportして、テキストエディタで編集してImportもできるのですが、XMLの編集自体が面倒。強力なXMLエディタを探した方がよいのかもしれません。

今のところプラグインを作成して独自のメトリクスを追加というのはやっていません。この辺は研究してコツコツ追加してゆきたいと思います。

#今日は、選挙でしたね!早く帰って選挙結果のTVをみたい。

――
アプリ受託開発、Webシステム受託開発も行っています。以下自社製品。
URL:ibisMail Freeのダウンロード、レビューはこちら
URL:ibisMail for iPhoneダウンロード、レビューはこちら
URL:ibisMail for iPadダウンロード、レビューはこちら
URL:ibisPaintのダウンロード、レビューはこちら サイトは、ibispaint.com
ご意見ご要望はTwitterから: @kamiyan

2012-12-09 [AWS] AWSでの3種類のネットワーク構成 このエントリーを含むブックマーク このエントリーのブックマークコメント

先日、アイビスサーバの引っ越しがありました。引っ越しは毎度毎度大変です。データセンターに置いていたサーバ群ですが、大変なのでやりたくないですが、引っ越しをせざる負えない事情がありまして、データセンターからAWSサーバ40台の引っ越しをしました。

AWSは、受託開発では経験があるものの自社サービスでは初めて。僕も大急ぎで勉強して、晴れてAWSデビューです。

システム開発は、企画、要件定義、設計、実装、試験、運用とありますが、どれも楽しいですね。ものづくり万歳!。今回は、ネットワークアーキテクチャのお話です。ネットワークアーキテクチャを考えるとき、拡張性はあるか?可用性はあるか?負荷に耐えられるか?運用しやすいか?コストを下げるにはどうするか?と色々考える必要があって楽しいですね。

AWSの話の前に、まずは一般的な三層モデルの解説から。

f:id:kamiyan2:20121209181431p:image

クライアント(Webブラウザまたはアプリ)からApacheへ接続して、その後ろにAP(アプリケーションサーバ)があり、APからDBに接続という形。うちの自社製品は基本JavaなのでWebアプリケーションTomcat上で動いています。

Apache-Tomcat連携しているのは、本来は静的コンテンツはApacheが返し、動的コンテンツはAPが返すためであり、一般に静的コンテンツはTomcatが返すよりApacheが返す方が速いためです。

また、Apacheバーチャルホスト機能を使うためにも先頭にApacheがいます。バーチャルホスト機能は、複数のWebサービスを1台のホストで処理するために必要で負荷が高くないサービスが複数ある場合は、Apache1台に複数サービスを集約してコストを削減します。例えばDNSサーバの設定で、www.example1.jpとwww.example2.jpの2つのドメインを同じIPに設定し、そのホストにApacheを立てて、ブラウザからのリクエストのドメインを見て、振り分け先のAPを切り替えるという形で使います。

3つ目の理由として、mod_rewriteがあります。mod_rewriteは機能が強力過ぎて悪魔のようなモジュールと一部で言われていますが、URL正規表現でマッチさせて、マッチしたらAPフォワードするときにURLを書き換えて転送させることができます。例えば、

http://d.hatena.ne.jp/kamiyan2/20121006

というリクエストをフロントのApacheが受け付けたときに、正規表現で「kamiyan2」と「20121006」をマッチさせて、

http://d.hatena.ne.jp/entry.jsp?uid=kamiyan2&eid=20121006

のようにURLを書き換えて、APに転送させることができます。

RESTfulが流行っていますが、それ以前に検索に引っかかりやすくするために同じリソースを指すならPATHに含めましょうというところですね。

4つ目の理由として、図に「LB」と書いてあるようにロードバランサとしてのApacheですね。mod_proxy_ajpとかmod_proxy_http、mod_jkなどを使ってAPを増やして分散処理をします。

その1:最小サービス(AP + DB)

f:id:kamiyan2:20121209181433p:image

AWSへの引っ越しで最も負荷の小さいサービスは、EC2を1台、RDSを2台(他のサービスと共有)としました。しかもEC2は、マイクロインスタンス(月額約$19)。DBは、RDSのミディアムインスタンス(月額約$172)。RDSミディアムインスタンスは負荷の割にオーバースペックになっていますが、これは他のサービスとの共有で使うために大き目になっています。もう1台RDSは、リードレプリカです。このリードレプリカには、APからのアクセスはしておらず、単なるバックアップ目的です。ファイルにバックアップを取って運用だと障害時に復旧に時間がかかるのでレプリケーションしています。年々サーバ側のデータは増えていく時代なので、バックアップからの復旧前提のシステムは、やるべきではないでしょう。

ちなみに上図のアイコンAmazonが配布しているアイコンで、ネットワーク図を書くには欠かせないアイコンです。こういう標準化をしているところは、Amazonのよいところですね。

なおEC2インスタンスは、Terminateするとインスタンスストレージに保存した情報は消えます。ストレージの情報が消えないようにするにはインスタンスストレージでなくEBS(有料)を使うとお金は余分にかかるけど、ちょっと安心です。

うちのシステム群では、LBやAPで使っているのは基本はキャッシュで、重要な情報は保存していないのでEBSは使いませんでした。

また、別途AMI(イメージファイル)から起動するとプライベートIPグローバルIPも動的に決められてしまうので、注意が必要です。なおTerminateではなく、stop/startやrestartではIPは変わらないので大丈夫です。Terminateは、停止というよりは、破棄ですね。OSが起動しなくなった、reboot依頼が効かない、SSHで入れなくなったなどのときに使うようですが、今のところそのような事態になったことはないです。

IPに関しては、EIPでグローバルIP固定IP化しました(ほぼ無料)。ちなみにEIPは初期設定では5件までしか作成できません。これ以上欲しい場合は、Amazonへ連絡する必要があります。グローバルIPが貴重だからでしょうかね(うちは今のところ5件以内で済んでいます)。

その2:スタンダードサービス(CDN+LB+AP+DB)

f:id:kamiyan2:20121209181434p:image

標準的なネットワーク構成となったのは、 http://ibispaint.com 。ibisPaintは、静止画を大量にレスポンスするため事前にCloud Flare(無料CDNサービス)を追加していました。Cloud Flareは、fladdictさんのエントリーで紹介されていたので早速入れました。

AWSでは、WAN接続のデータ通信でも課金されるため、データ量を抑える必要がありますが、静的コンテンツをCDNサーバがレスポンスしてくれるため、AWS側のサーバ群までリクエストが来る量が減りコスト削減になります。ibispaintの実績ではトラフィックの60%を削減できました。通信量のみならずDB負荷も下がりました。また、CDNサービスは世界中にコンテンツキャッシュサーバがあるので、クライアントの近くのサーバが応答するようになっていて、応答が早くなる効果もあります。ibisPaintは、USやオーストラリアアラブ、タイ、韓国台湾などからもアクセスがあるので少しはレスポンスタイム削減にも役立っているかもしれません。なおCloud Flareを使うと、クライアントから最寄りのコンテンツキャッシュサーバにアクセスを振るために、DNSサーバはCloud Flareのものになります。また、動的コンテンツも含めいったんCloud Flareサーバを経由してリクエストが自分のサーバに届きます。CDNサービスはAWSでもあるのですが、有料であるためCloud Flareを利用しました。無料でこれだけのサービスを受けられるとは驚愕です。

CDNの次に、ELB(ロードバランサ、月額約$18)を置きました。データセンターのころは、Apache mod_proxy_ajpによるバランシングでしたが、せっかくAWSに引っ越したということでELBを使いました。ELBにはSSHログイン等はできずAWSのWebUIなどから設定します。ELBの管理はほとんどお任せ状態で、ELBに障害が発生した場合も、勝手に他のホストで継続するようで、固定IPではありません。そのため、CDNからELBのホストへの参照はドメイン名での参照(DNSの設定でCNAME)になります。今のところELBでの障害は経験してませんが。

ELBの後ろにAPサーバを設置。このEC2は、Apache-Tomcatの両方を同じホストにいれました。データセンターのころは、Apacheは主にロードバランサとして使っていたので、ApacheTomcatを同居させることはなかったのですが、AWSでは同居させることにしました。ApacheからTomcatへの接続では、Apache側の設定でTomcatを指定する必要がありますが、別のEC2インスタンスの場合、EC2インスタンスを新規に立てる度に、プライベートIPが決まり、その度にApache側の設定ファイルを編集しなければならないのは、手間が大きいと判断し、Apache-Tomcatを同居させて、Apache側設定ファイルでは、Tomcatのホストをlocalhostとすることで、APの追加作業が楽になるようにしました。本当は、Apache自体をなしにして、ELBからTomcatへ直でつなぎたかったのですが、mod_rewriteを使っており、ELBにはmod_rewrite相当の機能がなかったので、Apache-Tomcatとなりました。この辺り、将来新しいサービスを設計するときは、mod_rewrite不要なフレームワークを使っていきたいなと思います。上図では、4層モデルにみえますが、Apache-Tomcatと2プロセスがあるので、5層モデルになってしまっています。

EC2インスタンスは、スモールインスタンス(月額約$66)を2台としました。ibisPaintはDB負荷やトラフィックは大きいもののAPの仕事はほとんどないので、APCPU負荷は低いですが、可用性(サービス停止回避)のために2台にしています。

RDSの負荷は高いですが、今のところAPからDBへの接続はマスターのみで、レプリカは相変わらずバックアップ用です。間もなく参照系クエリをレプリカへも振ることになるでしょう。RDSは、ラージインスタンス(1台あたり月額約$345)を使っています。

ちなみにibisPaintの投稿された作品(静止画など)は、すべてDBに保存しているため、バックアップに数時間、リストアに数日かかるため、もはやバックアップリストアという選択肢はほぼないに等しいです。

また、データセンターからAWSへの引っ越しでは、バックアップしてリストアでの引っ越しでは、サービス停止時間が長すぎるため、必要最低限のテーブルだけバックアップリストアをして(その間だけ短時間サービス停止)、メインの大量のデータは、徐々にデータセンターからAWSへバッチで移動させて、バッチでのデータ移動中は、AWSDBにデータがなければデータセンターにリクエストを振る(フォワーディング)ようにAPプログラムを書き換えて、引っ越しをしました。

大きなデータが数日かけて、ゆっくりと移動してゆく様は、なんともかっこいいですね。

その3:DNSラウンドロビン+自社製LB+AP+DB

f:id:kamiyan2:20121209222802p:image

今回紹介する最後のネットワーク構成です。

こちらのサービスでは、ELBは使わずに自社製のロードバランサを使いました。理由は、ELBではステッキーセッション(同じユーザからのリクエストは同じAPに振る機能)のセッションキーとしてCookieしか使えず、このサービスはガラケーからのアクセスもあるためセッションキーはURLに埋め込んであるためです。i-modeブラウザが長らくCookie非対応だったためやむなくURLに埋め込んであります。なお、Apacheのmod_proxyなどのロードバランサ機能を使わず自社製となっているのは、単にこのシステムを立ち上げるときに、mod_proxyがまだなく、mod_jkかmod_jk2しかなく、また、それらが不安定であったために開発しました。おかげでWebUIでの負荷係数の変更やAP追加や切り離しができたりと使い勝手は良いです。

さらにこのシステムでは、リクエスト頻度が高いためLBを2台をEC2(スモールインスタンス、月額約$66)+EIP(固定IP)で建てて、DNSラウンドロビンで2台に振り分けています。

その後ろのAPは上図では3台ですが実際には16台となっています。APは一部ハイCPUエクストララージ(月額約$547)です。EC2インスタンスは初期設定ではアカウントにつき20台までしか追加できません。それ以上はAmazonへ問い合わせをする必要があります。恐らく後払いシステムなので与信不足とならないようにではないかと思います。

LBからAPの参照は現状hostsファイルにAPを書くことで対応していますが、この辺りはスクリプト等を書いて、自動化したいところです。

RDSの注意事項

RDS(MySQL)は、SSHログインできません。mysqlコマンド等でアクセスすることはできます。また、my.cnfの設定ファイルの項目と同様の設定が、AWSのWebUIから設定できます。しかし、my.cnf設定項目の一部は設定不可になっているので注意です。特にtimezoneの指定ができず、APから接続開始時にセッション変数で変更してアクセスするか、AP側のプログラムを修正する必要があります。

あとSSHログインできないのでスローログとかも出せません。正確には、ログファイルとして出力させることができないので、テーブルに出力させることになります(現状、まだ試していない)。

2013/03 追記 AWSアップデートでログの閲覧のWebUIとAPIが追加されました。

価格はどうなの?

現状、コストの内訳は、

といった感じで圧倒的にEC2でコストがかかっています。割とAPCPU負荷が高いシステムというのもあると思いますが、現状は余分目にAPを立てているので高いというのがあります。これでデータセンターと同額程度のため、ここからチューニングをして台数を減らせば、半分ぐらいになるのではないかなとみています。また、現状の契約はオンデマンドインスタンスですが、リザーブドインスタンスの契約に変えればさらに1/3ですむので、データセンターよりかなり安くなると思います。サーバを増やすのも減らすのも実サーバより手間が少ないのでかなり気楽に調整できるのがいいですね。

さいごに

サーバ40台の引っ越し、サブシステムも含めると20くらいあるシステム。大変な難題プロジェクトなので、緊張の連続でした。とてもハードルが高い作業でしたが、チームメンバー全員猛烈な勢いで勉強し(全員AWS未経験)、上級のネットワークエンジニアと、これがいいんじゃないか、ダメか?とネットワークアーキテクチャについて議論をしつつ、調査、修正、作業手順書作成、実行、試験を短時間でやりとげ、心すり減らしつつも大変有意義でした。

これから自動化スクリプトや、事故が起きたときを想定したマニュアル作りと、復旧訓練、サービス監視のチューニングや、コスト削減のためのチューニングがありますが、またノウハウをコツコツと貯めていきたいと思います。

クラウドに移ってやはりスケールアウトもスケールアップも楽になったのは気が楽ですね。

引っ越しプロジェクト、とても貴重な経験です。

需要があればサービス監視についてのエントリも書こうと思います。

――
アプリ受託開発、Webシステム受託開発も行っています。以下自社製品。
URL:ibisMail Freeのダウンロード、レビューはこちら
URL:ibisMail for iPhoneダウンロード、レビューはこちら
URL:ibisMail for iPadダウンロード、レビューはこちら
URL:ibisPaintのダウンロード、レビューはこちら サイトは、ibispaint.com
ご意見ご要望はTwitterから: @kamiyan