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秒も使ってしまうのは、こういう理屈なのです。

終わりに


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

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

0 件のコメント:

コメントを投稿