文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

GAE/go+ginとGAE/java+servletでそれぞれスピンアップの速度差をゆる〜く確認する

GAE/Gに手を出し始めましたよ〜
f:id:treeapps:20160521191008p:plain

最近tree-mapsのフレームワークを、playから素のservletに変えました。では素のservletとgoを比較したらどうだろう?更にgoのginに興味があるので、それ使って比較しみようか、という事で試してみる事にしました。
www.bunkei-programmer.net

最初に断っておきたいのですが、

この記事は正確なベンチマークを取るような精密なものではありません。ゆる〜く適当になんとなく計測するものです。

従って、「こんな比較に意味はない!」「この計測コードはこう書いた方がいい!」のような、精密性を求めるものではありません。もし精密なベンチマークを求めている方は、ご自分で計測環境を構築して計測コードを書いて計測するようお願いします。(そうしないと納得のいく結果にならないかと思います)

GAEとスピンアップ

GAEとは?

GAEはGoogle app engineの略で、googleが提供しているサーバーレスアーキテクチャーのサービスです。制限は多いですが無料で利用できる範囲が広く、個人でサイトを開発するにはうってつけのサービスなのです。
cloud.google.com

私が開発しているサイトにtree-tipsとtree-mapsという2つのサイトがありますが、両方共GAE/J(java)で開発しており、完全に無料枠内で運営しています。

もはや周知の事実ですが、他の言語と比較してjavaは起動(JVMの起動)が凄く遅いです。一旦JVMが起動してしまえば、それ以降の処理は高速です。

GAE/Jのスピンアップ激遅問題

GAEはdockerのようなコンテナ技術が用いられているそうで、アクセスが一定期間無いとコンテナ(インスタンス)が削除されます。それがスピンダウンです。スピンダウンさせる事で、余計なリソース消費を抑えているのですね。

スピンダウンしてインスタンスが無い状態からサイトにアクセスすると、インスタンスが生成(コンテナの起動)され、生成時にjavaも起動します。それがスピンアップです。javaはこのスピンアップ(JVMの起動とフレームワーク等の初期化)が非常に遅いです。

以下のサイトには各言語のスピンアップ速度が掲載されていますが、python・php・goが1秒以下なのに対して、javaだけは3.8秒もかかっています。
www.apps-gcp.com

javaが3.8秒、goが0.3秒だとすると、goはjavaの12.6倍程度高速という事になります。しかし実際の開発では素のservlet(GAE/Jはservlet v2.5)で開発せず、slim3やspringやplay等のフレームワークを使う場合が多いかと思います。すると、JVMの起動に加え、フレームワークの起動時間も追加されます。このフレームワークの初期化処理もばかにならないんですよね。

ローカルでGAE/Jを起動すると解りますが、起動時のコンソールに「情報: jetty-6.1.x」等と表示されるので、スピンアップ時はjettyの起動も含んでいると思われます。(ローカルのjettyは2〜3秒で起動します)

go+gin、java+servletの比較

まず、それぞれどんなコードで試すのかを見てみます。

再度言っておきますが、この記事では完璧なベンチマークを取るような精密なものではありません。ゆる〜く適当になんとなく計測するものです

ソースコード

java+servlet
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.arnx.jsonic.JSON;

public class JsonServlet extends HttpServlet {

    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
        res.setContentType("application/json; charset=utf-8");
        PrintWriter out = res.getWriter();
        out.println(JSON.encode(new Json()));
        out.close();
    }

    public static class Json {
        public String user = "test";
        public String message = "msg";
        public Integer Number = 1;
    }
}

他にもweb.xml等の設定もありますが、servlet部分のみに留めます。単純にjsonを生成してjsonレスポンスを返すコードです。jsonの生成にはJSONICを使っています。

go+ginのコード
package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func init() {
    router := gin.Default()

    router.GET("/", func(c *gin.Context) {
        c.String(200, "hello world")
    })
    router.GET("/json", func(c *gin.Context) {

        type Person struct {
            Name    string `json:"user"`
            Message string `json:"message"`
            Number  int
        }
        var json = Person{
            Name: "test", Message: "msg", Number: 1,
        }

        pretty := c.Query("pretty") == "true"
        if !pretty {
            c.JSON(200, json)
        } else {
            c.IndentedJSON(200, json)
        }
    })

    http.Handle("/", router)
    router.Run(":8888")
}

色々突っ込みどころ満載で余計なコードも多々混じってますが、こんな感じでjsonを返しています。

スピンダウン状態から画面表示が完了するまでの時間

今回の計測では、画面にjsonが返るまでの時間をchromeのdeveloper toolsで計測しています。

計測前は必ず以下のようにインスタンスが0でスピンダウンしている状態になってから計測するようにしています。
f:id:treeapps:20160604215326p:plain

なお、1回の計測だとバラついて結果が偏る可能性が高いので、5回計測しています。(実は中々スピンダウンしなくて、計測するのに3日くらいかかったのは秘密です)

計測環境
ハードウェア iMac Retina
ブラウザ Google chrome v51
ネット回線 ギガプライズの共用回線+無線LAN ac

とりあえずは計測に大きく影響を与えるレベルの貧弱な環境では無いと思います。

java+servletの場合

↓1回目の計測
f:id:treeapps:20160604215840p:plain
↓2回目の計測
f:id:treeapps:20160604215849p:plain
↓3回目の計測
f:id:treeapps:20160604215856p:plain
↓4回目の計測
f:id:treeapps:20160604215909p:plain
↓5回目の計測
f:id:treeapps:20160605112401p:plain

ふむふむ。大体冒頭の記事に書かれている数値通り、3.4〜3.9秒程度かかっています。遅いですが、これでもplay時代より遥かに高速です。

go+ginの場合

↓1回目の計測
f:id:treeapps:20160604220257p:plain
↓2回目の計測
f:id:treeapps:20160604220308p:plain
↓3回目の計測
f:id:treeapps:20160604220316p:plain
↓4回目の計測
f:id:treeapps:20160604220326p:plain
↓5回目の計測
f:id:treeapps:20160604220333p:plain

単位はs(秒)ではなくms(ミリ秒)です。javaの時と桁が変わりましたね。速い。

冒頭の記事は恐らくフレームワークを使わずにjsonを返していると思われ、0.25〜0.37秒かかっているようでした。今回私が試したのは、ginというフレームワークを使ってjsonを返しています。その結果、0.37〜0.6秒かかっています。

表で確認

画像だとちょっと見難いので表にしてみます。

言語 1回目 2回目 3回目 4回目 5回目 平均
java+servlet 3.65秒 3.40秒 3.82秒 3.78秒 3.94秒 3.718秒
go+gin 0.506秒 0.377秒 0.601秒 0.501秒 0.494秒 0.495秒

うーん、go速いですね。というより、javaが遅すぎです。

この計測ではjava側は素のservletで計測しており、フレームワークを使用すると更に遅くなります。まだplayを使っていた頃のtree-mapsは、大体7〜12秒程かかる程遅かったです。tree-tipsはslim3ですが、そちらもほぼ同じです。10秒程度かかってしまうのです。

スピンアップが遅いと・・・

スピンアップというか、画面が表示されるまでにこんなに時間がかかってしまうと、ユーザがストレスを感じでそっ閉じしてしまうし、「ん?このサイト止まってるのか?」と勘違いしてもう二度と訪問してくれない事もあるでしょう。GAE/Jでフレームワークを使う場合はスピンアップから画面表示まで10秒程度かかるので、非常に厄介な問題です。

そもそもスピンダウンが発生するサイトは、非常にアクセス数が少ないかアクセスが無い期間があるサイトです。つまり、tree-mapsやtree-tipsのようなPV数の少ない弱小サイトはスピンダウンが発生しやすいのです。アクセスが継続してあるサイトはそもそもスピンダウンしにくいので、javaであってもスピンアップの遅さの心配は不要です。javaはJVMの起動が遅いだけでそれ以降の処理は高速ですからね。

参考程度ですが、tree-mapsは月間PV数は約1万、tree-tipsの月間PV数は約1.5万です。これくらいのサイトだとアクセスが無い時間が続く事もありますし、サイトの内容的に休日はアクセスが激減するので、結構スピンダウンの機会があるのが現状です。

すると、cron等で定期的に対象サイトにpingしてスピンダウンさせない等の手もありますが、それは最悪ですね。cronを実行する場所(サーバ等)も必要になりますし、pingできているかのチェックも必要になるでしょうし、サービスの本質とは言えない場所に労力はかけたくありませんね。

ちなみにspring frameworkもGAE/Jで動くようですが、slim3やplayよりもっと初期化に時間かかるでしょうから、スピンダウンするようなサイトでspringは厳しいでしょう。ちなみにtree-tipsを作る以前にGAE/Pでpython+djangoをちょっと試してみた事がありますが、スピンアップから画面表示までがくっそ遅かった記憶があります。やはりフレームワークの初期化は遅いです。その点GAE/G+ginは非常に高速に思えます。ginとplayやspringでは機能的な量や違いもありますが、やはり画面表示するまでが速いと色々嬉しいですね。

雑感

golangさんは速かった

いやー、golangさん速いです。GAE/Gならスピンダウンを恐れる必要もないですし、いい感じです。まあGAE/Jもスピンダウンしないアクセス数のあるサイトであれば、全然問題無いです。

最近のWEB開発のように、GAEでもサーバサイド側はAPIのみを提供、フロント側はフレームワークで頑張る類の開発をする事を考えると、言語としての機能が少ないGAEであってもそれ程問題にならないと思っています。どうやらgolangでもreactのサーバサイドレンダリング(SSR)の実行が可能らしく、サーバサイドの重たい処理はgoで他の画面表示部分はreactのSSR、クライアントはreactで、というのをGAE/Gでもできそうの(できない可能性もアリ)で、非常に興味深いです。
qiita.com
こちらの記事を見ると、これらをシングルバイナリに収めてしまう事もできるようなので、中々面白そうです。

GAEにnode.jsランタイム追加されたよね?使えば?

だったらGAE/Gを通してreactを呼ばないで、最初からGAE+node.jsでやれって話しですが、どうも現在のGAE/node.jsはManaged VM上でしか動かないようです。Managed VMというのは、GCE上でGAEを動かす技術の事で,、スピンダウンしなかったり、今までのGAEとは毛色が違うものです。Managed VMの課金体系はGAEではなくGCEベースなので、お金が必ずかかります。当然ですが、それに対して不満が挙がっていたりするようです。

私が求めているのは、Managed VMではなく、真のGAEです。簡単なGAEバージョンをデプロイして動かし、スケールアップするほどのトラフィックがなければ、そのままにしておいてコストがかからないのを望んでいます。GoogleがNodeをプッシュしていることは嬉しいですし、私はGoogle Cloudの大ファンです。ただ私はGAEネイティブなNodeを望んでいるのです。

https://www.infoq.com/jp/news/2016/03/nodejs-google-app-engine

サイトが弱小な間はできれば無料枠で済ませたいので、現在の課金必須のManaged VMでnode.jsを動かすのはちょっと嫌ですね。個人サイトなら尚更居力課金しない方向でいきたいものです。

Managed VMのメリットは何だ

私はあまりManaged VMの事を解っていませんが、dockerベースで構築するのですよね。そうなってしまうともはやGoogle Container Engineと何が違うの?と思ってしまうのですが、dockerで生成したコンテナ群を自動でオートスケールしてくれるのがManaged VMなのですかね???それだとGoogle Container Engineでオートスケーリンググループでスケールするのと大差無いから、ちょっとManaged VMのGAEのメリットがいまいち解りません。単に私の理解不足なだけだとは思いますが。

みんなもGAE使ってね!

GAEは結構いいサービスだと思うのですが、皆さんあまり積極的に活用していないように見えますね。みんな「AWS!」「GCP!」ばかりです。Managed VMではない通常のGAEは、サーバーレスアーキテクチャーで、オートスケールも自動だし、OpenSSLの脆弱性等のセキュリティに悩まされる心配も無いし、何より無料枠でできる事が非常に多いです。場合によっては月間500万PVのサイトでも無料枠で賄えたりするようです。セキュリティスキャンなる機能も追加されており、定期的にgoogleがセキュリティスキャンしてくれたりします。詳細なメリット等は以下の公式サイトを見ると大体解ります!
cloud.google.com

GAEにはバージョンというものがあって、バージョンに対してデプロイをする事ができます。バージョンという言葉は正直解りにくいですが、ec2のインスタンスみたいなものです。インスタンスにはGAEで提供されているランタイムを自由にのせる事ができます。そしてそのバージョンに何パーセントリクエストを振り分けるかもでき、A/Bテストもできちゃうんですよね。しかもそのバージョンは異なるランタイム(java・php・python・go)自由に選択する事ができるので、フロントはgoで、バッチはjavaで、遊び用はphpで、のような事もできます。

今回の計測テストでは、tree-mapsというプロジェクトの中に、デフォルトバージョンにGAE/Jのフロント、GAE/Jの計測用のバージョンtest、GAE/Gの計測用のバージョンgo-webapp、という3つのバージョンを起動しています。計測用のバージョンもグローバルからアクセスできてしまうので、そこは振り分け機能でフロントに100%バランシングするようにして、検証用のバージョンがユーザの目に触れないようにしています。1つのプロジェクトに複数のバージョンを持てて、そのバージョンのランタイムは自由に選択できます。これが無料枠でできるのって、結構凄いことだなあと思います。

ローカルのファイルに触れなかったり特定の操作ができなかったり、リクエストは◯秒以上かかると自動的に切断、のような厳し目の制限があったりしますが、無料枠でこれだけの事はできます。個人開発といえば月額費用が安価なVPSを選択しがちですが、サーバ自体の面倒を見なくていいというのはやはり魅力的なので、個人サイトを開発されている方はGAEも検討してみてはいかがでしょうか。

ちなみに今GAE/Gを勉強中で、今後tree-mapsをgo+gin+react(SSR使用)で作り直そうと考えています。途中で飽きて放り投げてしまう可能性が高いですが、上手くいったら公開してみます!


凄くどうでもいいお話ですが、ランタイムが増えて略称に困るようになりましたね。昔はjavaとpythonしかなったかので、GAE/JとGAE/Pで通じたのに、今はphpもできてたせいでGAE/Pでは通じなくなりました


世界的に今どう略されているのか知らないですが、やはりGAE/ランタイム名、ですかね。GAE/java、GAE/python、GAE/php、GAE/ruby、GAE/go、GAE/node.js、と。


この記事のタイトルはGAE/GとGAE/JではなくGAE/goとGAE/javaになっているな(こいつGとJだと検索にヒットしにくいだろうから略称にしなかったな絶対)


こういうのは公式が略称を示してくれると助かるんだけどなー

 

そもそもGAEの読み方はジーエーイーであってるのかも解らんな。ガエかもしれないし、略称なんて無いぞ、という可能性もあり得る


何れにせよ我々はグーグル先生に従うまでだ


お前AWS一色でGCP使った事すらないだろ・・・