2014年2月24日月曜日

【GAE】スピンアップ問題2~クラスロード~

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

只今、クラウド基盤「Google App Engine(以下、GAE)」の連載しています。

現在はGAE界最大の敵である「スピンアップ」のシリーズです。

何で重いのか?

過去の記事でもご紹介した通り、スピンアップとはインスタンスの起動のことです。
これが非常に重い為に「インスタンスの度にユーザが待たされる」という状況が発生します。
これがスピンアップ問題です。

しかし、何でスピンアップがこんなに重いのでしょう?

これは、Javaプログラマーなら簡単に察しがつくと思います。

Javaって、起動が遅いですよね。

あれですよ、あれ。
C言語とかで作ったバッチだと一瞬でレスポンスが返って来るのに、Javaだと何秒も待ったりします。
でも、一回起動してしまえば、以後の処理は高速ですよね?

そう、Javaというのは、「初回起動時に重たい処理を全部済ませて、以降の処理は高速に行う」という思想の言語なのです。

つまり、インスタンスの起動処理であるスピンアップが重い原因は、Javaの初回起動の重さと直結したものなのです。

具体的には「クラスロードが重い」、これが原因です。

クラスロードとは


クラスロードとは、Javaのクラスを呼び出すこと。。。

まあ、当たり前ですが、普通の開発ではそんなこと気にしませんよ。
普通の開発では、その処理に必要なクラスを都度、普通に呼び出してコーディングすれば良いですからね。

その為、

「クラスロード? 意味は分かるけど、だから何だよ?」

みたいな感じに、イマイチ臨場感が無い単語だと思います。
しかし、スピンアップ問題を検討する上では、クラスロードの真の意味を理解して頂かなければなりません。

さっそく、クラスロード時間計測用のテストソースを作ってみました。
これで「クラスロード時間」とやらをチェックしてみましょう。

以下、ソース。

public class SpinUpTimeController extends AbstractWebapiController {

    /** logger */
    private Logger logger = Logger.getLogger(SpinUpTimeController.class.getName());

    /* (非 Javadoc)
     * @see jp.co.net.genesis.controller.webapi.AbstractWebapiController#doResponse()
     */
    @Override
    public String doResponse() throws Exception {
        
        logger.fine("1");

        //対象ユーザを取得
        UserService userService = UserServiceFactory.getUserService();
        userService.getCurrentUser();
        
        logger.fine("2");

        //DAOを生成
        UserSettingDao userSettingDao = new UserSettingDao();
        
        logger.fine("3");

        //キーを作成
        Key key = userSettingDao.createKey("endo@genesis-net.co.jp");
        
        logger.fine("4");

        //キーから既存レコードを取得する。
        UserSetting model = userSettingDao.getOrNull(key);
        if(model == null){
            model = new UserSetting();
            model.setKey(key);
        }
        
        logger.fine("5");
        
        //全角文字要素をURIエンコードする。
        model.encode();
        
        logger.fine("6");

        Map map = new HashMap();
        map.put("responseCode", ResponseCode.SUCCESS.responseCode);
        map.put("userSetting", model);
        
        logger.fine("7");

        Gson gson = new Gson();
        String result = gson.toJson(map);
        
        logger.fine("8");

        return result;

    }
    
    /* (非 Javadoc)
     * @see jp.co.net.genesis.controller.webapi.AbstractWebapiController#checkUser()
     */
    protected boolean checkUser() throws Exception{

        logger.fine("a1");
        
        if(!PermitAccount.isAfterAttestation()){
        }
        
        logger.fine("a2");

        if(!PermitAccount.isPermitted()){
        }
        
        logger.fine("a3");

        return true;

    };
}

ソースの意味としては、適当に作った処理の隙間隙間にログを出力して時間を表示するだけです。
「元々は何をする為のソースだったの?」とかは今回は関係お気になさらず。
時間だけがポイントです。

では、コイツに初回リクエストを投げて、スピンアップさせてみたいと思います。
結果はこちら。


合計で7862msも要してしまいました。
たった1リクエストに8秒。
これは遅い……。

でも、2回目のリクエストなら29msですので、約300倍の差が出ています。
如何にスピンアップが重いかがお分かりになるかと思います。

遅い原因は上のログを見れば分かります。
しかし、このブログの読者の方が上のログに目を凝らすのも面倒で読む気がしないかと思いますので、
私にてポイントをピックアップしてみました。

着目点は、こちら。


2→3の処理で0.2秒を要しています。

その0.2秒の処理で何をやっているかと言うと、こちら。

logger.fine("2");

//DAOを生成
UserSettingDao userSettingDao = new UserSettingDao();
        
logger.fine("3");


ただインスタンスをnewして作っているだけですね。
コンストラクタで何かやっているわけではありません。

ただ、純粋にnewしてインスタンスを作るだけで、0.2秒。

1クラスロードに0.2秒を要するのです!!

よって、どこぞよりダウンロードしてきたライブラリをインポートして、
そのクラスの機能を呼び出して、その中でクラスをロードして、とやっていけば、

0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2……。

スピンアップに7秒も8秒も使ってしまうのは、こういう理屈なのです。

終わりに


というわけで、スピンアップを高速化するには「クラスロードを減らす」、これがキーとなりそうですね。

次回は、具体的に「どうやってクラスロードを減らせば良いのか?」という点について検討してみたいと思います。

2014年2月18日火曜日

【GAE】スピンアップ問題1~始めに~

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

只今、クラウド基盤「Google App Engine(以下、GAE)」の連載しています。

今回はGAE界の最大の敵、スピンアップについてご説明します。

スピンアップとは

まずは過去の記事で掲載したインスタンスの起動についておさらいしましょう。


GAEは、ユーザからのリクエストをトリガーにして「インスタンスの起動」という現象が発生します。

  • ゼロインスタンスの状態から、初めてアクセスが来た時
  • アクセス殺到により追加でインスタンスを立ち上げる時

このような時に発生する「インスタンスの起動処理」、これを『スピンアップ(spin up)』と言います。

激遅

さて、なぜこのスピンアップがGAE界最大の敵かと申しますと、それはもの凄く遅いからです。

開発していて、すぐに気がつきました。


  • 最初だけ妙に遅いな……。2回目からは速いんだけど……。


その時間、十秒近く。画面がホワイトアウトしているんですよ。

元からJavaのWebエンジニアである私には、心当たりがありました。
jspコンパイルです。

  • jspを使ったWebシステムで、デプロイの際にプリコンパイルを行っていないと、初回アクセス時にコンパイルが走って初回だけ重い。

これと酷似している現象であると直感的に察しがつきました。
しかし、調べてみたらスピンアップ問題はこれ以上に厄介な問題です。

jspコンパイル問題は「初回アクセスのみ」発生しますが、スピンアップは「インスタンス起動」の度に発生します。
つまり、何度でも発生するわけですよ。

ある程度のアクセス頻度で安定しているサービスならば、いつもインスタンスが立ち上がっているので問題にはなりませんが、「利用者の少ないサービス」「多過ぎるサービス」だとインスタンスの停止/起動を繰り返しますので、


「このサイト、最初はいつも遅いね」


という印象なサービスになってしまいます。

ここで、Webサイトの表示速度と人間の体感について、データを見てみましょう。

サイトを表示する際、どれくらい待たされると人はイライラし始めるのか、というアンケートがあります。

待ち時間比率
4秒超17%
3秒36%
2秒30%
1秒12%
1秒以下5%

何と、3秒待っただけで過半数の人は「遅い!!」と感じるようです。


  • 表示に3秒以上掛かるサイトは、閲覧される前にブラウザを閉じられる。


技術者にとっては厳しい世界ですね……。

ちなみに、上記3秒ルールは普通のサイトの話です。
アマゾンみたいな大規模通販サイトだと、0.1秒の違いでも如実に売上が変化するそうですよ。

システムの速度はスピードは売上に影響するんです!!

仕事しているとクソ遅いシステムの改修とかやらされてしまうことが多々ありますが、ああいうシステムは最低ですね。

真のWebエンジニアは、刹那の一瞬に命を賭けるオリンピック選手顔負けのスピード狂でなければならないのです。

終わりに

このように「スピンアップにより、最初の表示に10秒」とかでは話にならないわけです。
このスピンアップ時間を如何に短縮するか、これがGAEを活用する上での最初の大問題となるわけです。

今回の「スピンアップ問題シリーズ」では、このスピンアップについて調査し、掘り下げていこうと思います。
次回は、まずは現状調査。


  • なぜ、スピンアップはこんなに時間が掛かるのか。
  • 具体的に何秒くらい要しているのか。


この辺りから調査を始めていきます。

2014年2月4日火曜日

【GAE】インスタンス時間3

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

只今、クラウド基盤「Google App Engine(以下、GAE)」の連載しています。

さて、前回の記事で、GAEの料金の中核は「フロントエンド・インスタンス時間」であることをご紹介しました。
Google本家の情報によると、一日200万プレビューくらいまでは無料で行けるそうです。

はてさて、本当にそんなレスポンスが出るのか、今回はちょっとDOSアタックを仕掛けて検証してみましょう。

負荷要件

さて、Googleの言う200万プレビューというのは、「効率が良いロジックである場合」の話です。
一回のリクエストが重ければ200万プレビュー無料は達成出来るわけ無いです。

この為、今回の負荷テストでは「標準的な効率的ロジック」に対し、多数のリクエストを投げる要件で検証してみようと思います。

具体的には、「テーブルの主キー検索1件」です。
一回のリクエストでテーブルの主キーを検索し、1レコードを取ってくるという処理は、
DBを使ったものの中でも最も標準的なものですからね。

この要件に対し、大量アクセスをかけて検証してみましょう。

1リクエスト速度

この負荷要件の場合、「1回のリクエストは何秒なのか?」が重用になります。

GAEだと、1リクエストの時間は標準でログに出力されておりまして、簡単に正確な時間が分かります。
それがこれ。



  • ms=26

何と、1リクエストで26msです!!

これは速いですね。
ちなみに、GAEの場合、どれだけデータ量が増えても主キー検索の速度は変わりませんので、安定してこの速度が出せます。

では、この要件で負荷テストを実施します。

アタックバッチ

DOSアタックには、私が自前で作ったマルチスレッドのリクエスト送信バッチを使います。
指定本数だけリクエスト送信スレッドを立てて、無限ループでリクエストを送りまくるという単純なバッチです。

1スレッドの時は、同時に1ユーザがアクセス中。
10スレッドの時は、同時に10ユーザがアクセス中。

と見なすわけです。

お見せする程の価値のあるソースではありませんが、一応以下に張っておきます。

public class DosAtackBatch extends AbstractBatch {

   /** logger */
   private Logger logger = Logger.getLogger(DosAtackBatch.class);

   /** 送信先URL */
   private String URL = "http://genesis-gae-service.appspot.com/webapi/develop/SpinUpTime";

   private int threadCount = 1;

   private int attackTime = 30000;

   private static AtomicInteger count = new AtomicInteger();

   /**
    * @param args
    * @throws Exception
    */
   public static void main(String[] args) throws Exception {

      DosAtackBatch batch = new DosAtackBatch();
      batch.excute(args);

   }

   /*
    * (非 Javadoc)
    *
    * @see jp.co.tacy.batch.AbstractBatch#doExcute(java.lang.String[])
    */
   @Override
   public void doExcute(String[] args) throws Exception {

      final Date dateBefore = new Date();

      for(int i=0;i<threadCount;i++){

      Thread thread = new Thread() {
            /*
             * (非 Javadoc)
             *
             * @see java.lang.Thread#run()
             */
            @Override
            public void run() {

               while (true) {
                  RequestSender sender = new RequestSender();
                  try {
                     sender.sendPost(URL, null);
                     Date dateAfter = new Date();

                     long responseTime = dateAfter.getTime() - dateBefore.getTime();

                     logger.info("アタック回数:" + DosAtackBatch.count.incrementAndGet());
                     logger.info("レスポンス時間:" + responseTime);

                     if(responseTime > attackTime){
                        break;
                     }

                  } catch (IOException e) {
                     logger.fatal("エラーが発生しました。", e);
                  }
               }
            }
         };

         thread.start();
      }

   }

}

計測:1スレッド

では、このバッチを使ってアタックをかけてみます。

  • スレッド数:1
  • 実行時間:30秒
  • リクエスト総数:108回

結果はこの通り。



1インスタンスしか上がっていません。
まあ、1スレッド=1ユーザという想定ですから、1ユーザしかアクセスしていないのであれば、1インスタンスで足りるのは当然ですね。

計測:10スレッド

次は、一気に10ユーザ行ってみましょう。

  • スレッド数:10
  • 実行時間:30秒
  • リクエスト総数:1024回

どうやら私の作ったバッチは10スレッドを立ち上げても、1スレッド辺りのパワーは落ちないようです。
それなりに良いPCを使わせて頂いております。(^_^)

さて、肝心の結果ですが、何とこれでもインスタンス数は1コでした。
  • 10ユーザがF5アタックしているような状態でもビクともしない!!

計測:20~25スレッド


「性能が凄いのは分かったから、いつになったら変化するんだよ?」

という感じでしょうから結論を述べますと、25スレッドで変化が出ました。

  • スレッド数:25
  • 実行時間:30秒
  • リクエスト総数:2104回

25スレッドで実行すると、インスタンスが2つ立ち上がります。



流石に25スレッドともなるとPCの性能が追いつかなくなってきて、1スレッド辺りのパワーが下がってきているようです。

なので、多少数値が曖昧ですが、概ねの性能が出て来ました。

  • GAEは、20~25人のユーザがF5アタックしているような状態まで、1インスタンスで対処することが出来る。

キリの良い20スレッドで再計測しましたら、以下の実績で1インスタンスでした。

  • スレッド数:20
  • 実行時間:30秒
  • リクエスト総数:1564回

というわけで、この20スレッドの数字が1インスタンスの上限値と位置づけて、算出してみようと思います。


  • 30秒で1564リクエスト
  • 1分なら1564×2=3128リクエスト
  • 1時間なら3128×60=187680リクエスト
  • 1日なら187680×24=4504320リクエスト
  • 1ヶ月なら4504320×30=135129600リクエスト

1日で450万、1ヶ月で1億3000万リクエストまで無料!!

という概算見積もりとなりました。


まとめ

さて、私の計測結果だと、Googleの主張する1日200万プレビューの2倍以上の実績を叩き出してしまいました。

まあ、私がテスト用に作った機能は超軽量ですから、現実的にはもうちょっと重くなるのが普通でしょう。
となると、1日200万プレビュー無料という謳い文句は、妥当な数字であると言うことが出来るかと思います。

結論としましては、

  • GAEは1日200万プレビューまで無料まで実現可能

をキャッチフレーズにしても問題は無いのではないかと。

ただし、この数字はザクッとした見積もりですので、現実にプロジェクトとして導入するなら、もうちょっと細かく計算したいです。

私の計測の場合、1リクエストの処理時間が26msで1日450万という結果でした。ここから比例計算で、

  • 1リクエストの平均が26msの場合:450万
  • 1リクエストの平均が58msの場合:200万
  • 1リクエストの平均が100msの場合:116万
  • 1リクエストの平均が1000msの場合:12万

みたいな感じに算出することが出来ます。
各自、自分の作ったアプリの1リクエストの時間を見て、そこから上の比例計算で割り出してみると良いかと思います。

終わりに

今回の解析で、GAEのコスト効率が概ね見えてきましたね。

ちなみに現在、私はこのブログと平行して社内用を想定したアプリを作っているのですが、一番重い処理でも1リクエストの時間は200msです。

ここから考えて、

  • GAEに最適化した要件であれば、1日200万アクセスまで無料
  • 多少のカスタマイズを入れた業務用アプリなら、平均して1日50万アクセスくらいまで無料

これくらいが現実的な見積もりなんじゃないかと思っています。

それ以上になると有料になりますが、その場合は1時間当たり0.08ドルですから、
業務用としてWebサイトを運用する場合、

  • 1日50万アクセスくらいまで無料。
  • 1日100万アクセスなら毎月5000~6000円くらい。
  • 1万円出してくれれば1日120~130万アクセスくらい行けそう。

という予算感覚です。

こりゃ安いですね。
ビジネスモデルとしての展望が見えてきそうな感じです。

この先もまだまだ気合い入れて勉強していきたいと思います。