ISUCON6に参加してきた
チーム「坂寝」の「寝」担当としてISUCON6に参加しました。
結果、予選が2日目の3位、本戦が8位と、そこそこ健闘できたのではないかと思います。
8位!!!!!!!! #isucon pic.twitter.com/scmyLtG0mw
— neguse (@neguse) 2016年10月22日
予選
予選では自分はアプリコードのチューニングを担当しました。
まず最初にそのままGo実装に切り替えてベンチを回してみて、ほぼ0点からのスタートということがわかりました。つらい…
とりあえずプロファイルをとろうとして、GoのNew Relicを試したりしたのですがうまくいかなくて、あきらめてpprofを使ってプロファイリングしました。
プロファイリングの結果、htmlifyの正規表現が圧倒的に重いということがわかりました。 あと、isutarがマイクロサービス的な感じになってて、たぶん無駄なんだろうなあという気がしてました。
まずは正規表現オブジェクトをコンパイルした結果をキャッシュして使いまわすような実装にしてみて、8000点ぐらいとたしかに速くはなったのですがまだまだという感じでした。Goの正規表現は遅いという話もあって、このアプローチだと足りなそうでした。pcre版の正規表現ライブラリを入れてみるという手もあったのですが、ちょっと簡単には入れられなさそうでした。
次に、どうせ正規表現でやっていることは単なる置き換えなのでstrings.Replaceにできないかなと思って置き換えてみました。すると一気に60000点ほどになってめっちゃ速くなったものの、だいぶエラーがでるようになってしまいました。 これはおそらく今回の問題だと単語の最長マッチをする必要があるものの、単語ごとにReplaceをしていく方式だと最長マッチにはならないのでエラーとなる、というのが原因だったと思います。あるいは何かしらバグ(更新処理でキャッシュ無効化してないとか)が入ってしまった可能性もあります。
そこで、もうちょっといいReplace関数はないかとおもってGoのドキュメントを眺めていたところstrings.Replacerを見つけました。これだと単語列をいっきに置換できるので、入力文字列さえ長さ順にソートしておけば最長マッチに使えそうです。
strings.Replacerを使うようにしたり、isutar機能をisudaに統合+キャッシュしたり、あと単語リストを作るクエリがSELET *
になってたのをキーワードだけにしたり等して、最終的に148431点となりました。ただベンチマーカーを走らせるたびに(スコアに影響しない)エラーがたくさん出ていたので、何かしらバグが残っていた可能性は大いにあります。
本戦
本戦では自分はサーバ構成の検討とかDockerまわりのサポートを担当しました。というよりは後述のようにインフラで手一杯になってしまってアプリチューニングまでたどり着けませんでした。
まず最初にAzureのコア数制限にひっかかってホストの起動に失敗する状態でしたので、運営側の対応中に先に何かできないか調べました。するとDeploy to Azure
のリンクからアクセスできるJSONに、デプロイに必要なAnsibleのファイルへのリンク、またAnsibleのファイルからアプリのソースコードがアクセスできるようになっているのに気づきました。このおかげでちょっとだけ早くソースが読めました。
運営側の対応が終わって、デプロイが完了して、topの結果を見たところnodeのプロセスがネックになってそうな感じでした。 そこで以下のように各ホストにプロセスを移動しました。
- isu01
- node -> (一部goにプロキシ)
- isu02
- go
- isu03
- mysql
ここの変更で問題が出てちょっと手間取りました。1つめはAzureのホスト名での名前解決がコンテナ外からは使えるもののコンテナ内からは使えないというものです。しかたなくdocker-compose.ymlのextra_hostsにホストのIPアドレスを書いて無理やり解決しました。 2つ目の問題はgoからのMySQLへのコネクション数が足りなくなるというものです。これはMySQLの設定を変えればよいのですが、Dockerコンテナだとコンフィグファイルを書いてvolumesに指定する等設定方法に違いがあってちょっと手間取りました。
このようにしたところ、isu01のnodeがCPU使用率100%に張り付く感じで、2コアだと200%まで行くはずで、おそらくシングルスレッドしか使えてないのだろうなあと思いました。
nodeの処理はReactのサーバサイドレンダリングや静的ファイルの配信、goのapiサーバのプロキシという感じで、静的ファイルの配信やapiサーバプロキシはnginxとかに任せた方がよさそうな気がしました。 そこで以下のようにサーバ構成を変更しました。
- isu01
- nginx -> (node, goにプロキシ)
- node
- isu02
- go
- isu03
- mysql
このように変えてもまだnodeのCPU使用率が高かったので、最終的に以下のような構成になりました。
- isu01
- nginx -> (node, goにプロキシ)
- node -> (goにプロキシ)
- isu02
- go
- isu03
- mysql
- isu04
- node -> (goにプロキシ)
- isu05
- node -> (goにプロキシ)
このあたりで16時ぐらいになってました。そろそろアプリのチューニングをしようと思ったのですが、プロファイリングに失敗してうまくいきませんでした。
まずpprofやgo-torchを使おうとしたのですが、使ってたフレームワークのgojiだとimport _ "net/http/pprof"
するだけだと/debug
のパスでのハンドラが動かずプロファイリングが行なえませんでした。これはnet/http/pprof
パッケージが公開しているハンドラ関数を直接mux.HandleFunc
に指定することでなんとか動いたようです。
次に、コンテナの外からgo-torchを実行したところ、エラーの内容は覚えていないのですがうまくいかずでした。おそらくpprofを使う上ではプロファイリング対象の実行ファイルのデバッグ情報が必要なものの、コンテナの外からでは実行ファイルにアクセスできないのでだめなんじゃないかという気がします。
プロファイリングが手詰まりになってしまったのでなんとなく重そうかつ簡単にキャッシュ化できそうなところをやろうとしたのですが、逆にスコア下がってしまったりしてうまくいかずでした。 結局アプリ側のチューニングはほとんど手付かずで、スコアに影響したかはわからないです。
そんなこんなで、最終的なスコアは13979点となりました。
感想
予選については、割と満足に取り組めたかと思います。これはプロファイリングでボトルネック探し→チューニング・実装→再度ベンチマークをとって確認というループを効率よく回すことができたためと思います。 またGoのアプリはチューニングしやすいという点がよかったです。これはもう少し詳しく言うと、アプリのコードに手を入れたときに何か間違いがあればコンパイルエラーという形でわかるので間違いが起きにくいという点、プロセスのメモリにキャッシュを持つということがやりやすい(他の言語処理系・サーバだとプロセスが複数立ってしまってプロセス間ではキャッシュを共有しにくいが、Goだと1プロセス内でマルチコアスケールする方式なのでメモリを共有することができてパフォーマンス上有利)という点があるのではないかと思っています。今回の予選問題のように正規表現ライブラリの性能が低いというデメリットもありますが…
本戦については、Dockerをやめるという選択肢を早いうちにとれてればまた違った結果が出てたかもと思っています。Docker由来の問題がいくつか出ていましたし、パフォーマンス的にもホストに直接デプロイするのと比べてオーバーヘッドがあったのではないかと思います。 また問題を見ながら「これ、もうちょっと時間あったらアプリ側のチューニングいろいろしたいなあ」と思っていました。そのうち問題が公開されたらやってみたいです。
全体を通して、今回そこそこ健闘できたのはチーム内コミュニケーションがスムーズにいった点があると思っています。前回ISUCON5にも出ていたのですが、各自それぞれの家からSlackを通じてコミュニケーションしながらオンラインで参加するという形式でやっていて、状況の把握がしづらかったというのがあります。今回は予選では家に集まって、本戦では会場に集まってやれたので、状況の把握がしやすかったです。 チームメイトの方には感謝しかないです。
サーバのパフォーマンスチューニングは仕事でも2度ほどやったことがあるのですが、自分がやったことに対してスコアやレスポンスタイムという形で改善の結果が目で見てわかるという点が楽しいと思っていますし、プロファイリングの仕方や改善の仕方など、たくさんの知識や実装スキルが有効に機能する貴重な場だと思います。ISUCONは楽しいですし、ぜひ来年もあれば参加したいです。