2016年10月2日日曜日

QiNeelのバックエンドをJava Servletにしようとした

突然ですが転職しました。8月に

新しい職場では主にJava Servletでウェブサービスを開発しているのですが、「Javaは速い」という都市伝説をどこからともなく聞きつけたのでQiNeelも現在のアーキテクチャをなるべく保ったままPython WSGIからJava Servletに移植できないか数週間かけて検討したところ、「できる」との結論に達しました。

Java速いし静的型付けだしいいとこだらけ!と(・∀・)ニヤニヤして「よし、じゃあ移植しよう!」と思ってさらに数週間、実際の業務でServletを使っていると、必ずしもベストな選択肢ではないかなぁ、という考えに移り始めています。

開発速度が遅くなる

これが一番大きな理由です。
確かにJavaは実行速度は速いのかもしれませんが、 プロジェクトが大きくなるほどコンパイル→デプロイ→コンテナのリロードという時間がバカになりません。

継続的に開発していくサービスの場合…というかほとんどのサービスが当てはまると思いますが、ソースコードの修正が頻繁に入ります。そのたびにコンパイルから入るより、ソース修正後に即デプロイできるほうが開発サイクルが速くなります。

型についても、IDEがコメントのドキュメントを認識して引数の型エラーを指摘してくれることが多いので、きちんとドキュメントを書く習慣がつきました。

便利なデータ構造をネイティブにサポートしていない

便利なデータ構造とは、連想配列とか集合とかそういうやつ。Javaでもライブラリでサポートしていますが、ネイティブにサポートしていないと初期化時や使用時に余計な手間がかかります。

Pythonだと
mapvalues = {"a": 1, "b": 2}
setvalues = set(["a", "b", "c"])
と極めて簡潔・直感的に記述できるところを、Javaだと
Map<String, Integer> mapvalues = new HashMap<>();
mapvalues.put("a", 1);
mapvalues.put("b", 2);

Set<String> setvalues = new HashSet<>();
setvalues.add("a");
setvalues.add("b");
setvalues.add("c");
と、オブジェクトの作成と値の設定を別々にやらなきゃいけません。 一生懸命トリッキーにやってもせいぜい
Map<String, Integer> mapvalues = new HashMap<String, Integer>() {{
    put("a", 1);
    put("b", 2);
}};
Set<String> setvalues = new HashSet<String>() {{
    add("a"); add("b"); add("c");
}};
Set<String> setvalues = new HashSet<>(Arrays.asList("a", "b", "c"));
Set<String> setvalues = Stream.of("a", "b", "c").collect(Collectors.toSet());
これくらい。必然的にコードは本質以外の記述が多くなり、冗長になります。

静的ファイルの処理やスケールアウトはWSGIのほうが適している

今時Pythonでウェブサービスを開発する場合はWSGIを使う人が多いと思います。

WSGIサーバが吐き出すデータはHTTPではないのでブラウザと直接通信できず、間にnginxなどをかましてやる必要があります。PHPのように簡単に作れないのでとっつきにくいのですが、大規模になるとこの構成がとても都合がいいのです。たとえば…
  • 静的ファイルへのリクエストは高速・軽量なnginxが直接処理する
  • スケールアウト時はnginxがリバースプロキシの役割を果たす。nginxはリバースプロキシとしても優秀で広く使われている
  • nginxがリバースプロキシの役割を果たすことで、リバースプロキシの利点を享受できる
    • コンテンツの圧縮やSSL暗号化など、コンテンツ生成と関係ない処理を肩代わりすることでWSGIサーバはコンテンツ生成に専念できる
    • WSGIサーバ側は生成したコンテンツをnginxに渡した時点で終了できるので、回線速度の遅いクライアントからのリクエストでもクライアントへの転送完了を待たずに次の準備ができる
一方、Servletでは全てのリクエストをWebコンテナが処理するため、静的ファイルもSSLもWebコンテナが処理し、転送が終わるまで接続を切断できないのでそのぶんリソースを消費します。一昔前の全部入りApache+mod_php+mod_deflate+mod_sslも同じような問題がありました。

もちろんServletでも前にリバースプロキシを置けますし、TomcatなんかはHTTPサーバとサーブレットコンテナに分かれていますが、WSGIは強制的にリバースプロキシ構成にさせられる…つまり、技術者のスキルレベルによらずこの構成になるのが大きいと思います。それに、Webサーバとして機能するものの前にリバースプロキシを置くのはなんだかもったいない気がします。気がするだけなんでしょうけど。

言語自体の速度が占める割合は小さい

決定打はこれ。

ウェブサービスの速度は言語の速度だけで決まるわけではありません。

クライアントからのリクエスト→サーバ内部の転送→レスポンス生成処理→クライアントへレスポンスという流れの中で往復で2回通信が発生し、さらにレスポンス生成処理内でもDBとの通信があれば言語自体が占める時間の割合はかなり小さくなります。
特にDB関連は、適切に設計しないと言語関係なしに死ぬほど時間がかかります。

こんな状況で、「Pythonで書いて0.01秒かかった処理がJavaで書いたら0.005秒になった!」と喜んだところで全く意味がありません。リクエストからレスポンスまで0.1秒から0.995秒になる程度です。

あとは、ウェブサービス固有の話としてはテンプレートエンジンの速度も無視できないでしょう。コンテンツ生成の準備が整った後で最終的に大量の文字列を吐き出すので、もしかしたら本体のビジネスロジックより時間がかかるかもしれません。

コンテンツを生成して終わり!ではない

クライアントがレスポンスを受け取った後でも最終的にブラウザに表示されるまでにはHTML/CSS/JSのパース処理が入るので、単純にコンテンツ生成が速ければいいというものでもありません。

例えば<script>タグがあるとそこでレンダリングがストップするからHTMLの最後に置きましょうとか、CSS/JSはなるべく1つのファイルにまとめましょうとか、画像はCSSスプライトを使いましょうとか、クライアントサイドでできる工夫もたくさんあります。

逆にこのあたりの工夫をしていないと、いつまで経ってもブラウザの読み込み中マークが消えないとかブラウザのレンダリングが途中でブロックされて全体が表示されないといった問題が出て、いくらコンテンツ生成が速くても「このサービスは重い」という悪評につながります。

遅いなら設計を見なおせ

元々ウェブサービスは、リクエストに対してコンマ数秒のレベルで応答しなければいけません。
数十秒、場合によっては数分〜数十分の処理待ちが許される業務用ソフトならともかく、ウェブサービスはかなりシビアです。

そもそもウェブサービスで言語を変えたら体感できるほどに速度が変わるということは、元のプログラムが相当時間がかかっているということで、プログラムの書き方自体に問題がある可能性が大です。

最終的に

というわけで、 結局今までどおりPythonで開発を続けよう!と思いました。

0 件のコメント:

コメントを投稿