ISUCON9予選に参加しました

ISUCONというウェブアプリケーションを高速化させるというコンテストに参加しました。

去年も参加しましたが、結果は惨敗でした。
今年もチーム名「ガラスボッチ」で参加しましたが、メンバーは僕一人でした。看板に偽りなし。使用言語はNodejs(Typescript)です。
今回は事前に準備を仕込む時間をあまり取れなくまた、メンバー一人ということもあり、結果はGoの初期実装にすら劣るとかいう悲惨な結果でした。

本戦中にやったこと

基本的にはMySQLのN+1クエリ問題の解消とCategoryをメモリにキャッシュすることくらいです。
マルチサーバー化も試みたのですが、アップロードの画像データの扱いと、MySQLの設定で躓いて手を出せませんでした。

本番中はalpでアクセスログを解析してボトルネックを探りつつ改善という方針で行きました。
本番環境へのデプロイはrsyncコマンドを用いてやりました。リモートのgitレポジトリを仲介させるのは面倒くさい。
環境のセットアップとして前回用いたansibleスクリプトに少々手を加えておいて、必要なツールがすぐに入るようにしておきました。

敗因分析

Nodejsの初期実装でのスコアは500程度なのに対して、Go言語での初期実装のスコアは2000ほどありました。
確かにGo言語のほうが一般に実行速度が速いのですが、それにしては遅すぎると思い分析してみました。
alpでの結果を見てみるとNodejsの場合、/items/<id>.jsongetItemメソッド)が時々10倍以上のレベルで遅くなっていることがわかりました。
この関数ではSQLの呼び出しくらいしか動作が重くなりそうなものはなく、初期実装でのSQL文に大きな差はありません。
原因はNodejsはデフォルトではシングルプロセスで動作していることにありました。clusterでマルチプロセス化したところ性能が安定してGo並のパフォーマンスになることが確認できました。
普段、ブラウザで動くフロントエンドのJavascriptしか触っていなかったため、このへんの知識が欠落していたのは痛かったです。

マルチサーバー化に失敗した原因ですが、MySQLのユーザー権限設定はIPアドレスベースで行えることを忘れていて、デフォルトではlocalhostからのアクセスしか容認していなかったからでした

また、画像ファイルのアップロードですが、画像アップロードと提供をするサーバーを1つに絞ってしまうか、nginxのtry_filesをつかうことで回避が可能かと思われます

今回はnetdataでCPU使用率がかなり厳しいことが分かっていたので、マルチサーバー化できなかったのは痛手でした。練習不足ですね。

外部APIを用いて通信している部分もあるのですが、そのうち多くは不要な呼び出しだったらしく、そこにも手を回す必要がありました。
サーバーをhttp2対応させることでもパフォーマンスが向上できたようです。http2対応は考えたのですが、効果がどれほどか自信がなく後回しにしてました。nginxの設定をいじり、アプリケーション側の初期設定を変えて、あと必要に応じてリクエストハンドラの型を変更すればできたので、とりあえずやっておいたほうがよかったかもしれません。

また、bycryptによるパスワード認証が重い、という問題もあります。 実際、ログイン部分の初期実装のレスポンスタイムは他のAPIと比べて高く、特にNodejsの場合、Goに比べてさらに2倍ほど遅い。
問題の制約として「パスワードを平文で保存する」ことが禁止されていました。
この解決策としては、ログイン専用サーバーをつくってしまい、CPU負荷を分散するという方法が想定解の1つだったようですが、運営の意図的にはシーザー暗号レベルの軽量なハッシュ関数での保存も想定解だったようです。
ただし、この「平文で保存する」という文言はかなり曖昧で、何を持って平文保存ではないかが明確でなく、「平文に適当な文字を足して格納する」という方法をとったために失格になりかけたチームがありました。
暗号理論などの世界で「平文ではない」とは「平文とは一致しない」だけで十分らしいですが、問題の文脈、すなわちウェブアプリケーションの開発においてカジュアルに「平文で保存しない」といわれたら、「DBのデータが盗まれても大丈夫なような暗号化を施せ」なのかなと自分は考えて、暗号のアルゴリズムを変えるにしても同程度の攻撃耐性を持つものにしか変えられないだろうと考えていました。
シーザー暗号のようなアルゴリズムの場合、DBから例えば”abcdefghijk…”といったパスワードがどうハッシュ化されているかを確認する、といった方法で簡単に平文が取得できてしまうという点で、「平文で保存する」と同等なのではという判断です。
結局は今回は運営が謝罪して、この制約による失格は全て取り消されましたが、競技者側からするとルールが曖昧な場合、実装の単純さや効果の大きさをとりギリギリを攻めるか、十分に安全をとって最適化を諦めるかは難しい判断です。
ISUCONに限らず、競技のルールにはどうしても曖昧性の残り、最後は運営の主観的な采配になってしまうのは仕方のないことだとは思うのですが、今後はもう少しルールを明確化する・(競技性を失わない範囲で)ルールの一部を事前公開してFAQを募るなど、不公平感のない対策があるといいと思いました。

NodejsでISUCONに勝てるか

今回、予選参加者のうちNodejsを使っているチームはそこそこいたようですが、決勝にはどのチームをいけていません。決勝に進んだチームではGo言語が圧倒的です。
以前からISUCONはGo言語が有利ということは言われていて、前回予選では他の言語を用いたチームもかなり健闘していたのですが、今回はGo言語の強さが色濃く出たのかなあという印象を受けました。
bycryptの実行速度はおそらくGoが有利だっただろうし、Nodejsの場合、ISUCONでよく用いられるメモリ上へのキャッシュ化もマルチプロセスするとプロセス間でキャッシュが共有できなくなるので厳しいのかなと思います。
私の場合は極端な準備不足だったし、メンバーも一人のみという舐めきった構成だったのでまずはそこをなんとかしなければなのですが…
(今年の予選1位と2位は一人チームだったらしいですが、よほど実装力に自信がない限り一人で勝つのは無理でしょう)

来年開催されるかは未定のようですが、開催されるのであればRustかScalaでの初期実装があるとうれしい