ISUCON11予選参加記(予選敗退)

ISUCON11にチーム「ビックバン・オーガニゼーション」で@cympfhさんと@dekokunさんと参加して、予選敗退しました。お母さんには内緒だぞ!
昨年は別のメンバーとですがRustを使い予選通過できましたが、今年は結果が出せませんでした。

事前準備

この御時世ということもあり、全員Discordでオンラインで連絡を取り合いながら去年の問題を解いていました。
ログ解析にはnginxのログを解析するalpを、MySQLはスロークエリをpt-query-digestで解析することにしました。
ウェブアプリのデプロイは手元でコンパイルしたものをrsyncでデプロイする形式をとりました。
サーバーの基本的な環境設定用のansibleスクリプトも用意しました。

また、デプロイやログ解析用のシェルスクリプトも用意しておきました。

予選本番

9:40からのyoutube liveには全員オンラインになってライブを視聴してました。

まずは、ansibleを走らせつつ、当日マニュアルを読み競技内容の確認を行いました。

Rustの初期実装でベンチを走らせた結果を元に、まずは適当にindexを貼りました。
また、nginxの設定をいじり、静的ファイルはアプリを介さずnginxで直接返すようにしました。

11:40頃、CPU使用率が張り付いていたのを見てDBは別サーバーで動かすことにしました。スコア7610

13:10頃、他のメンバーがiconのmax-ageを設定することでスコア18380

13:55頃、GET /api/isuの最適化のため最新のconditionをウェブアプリ内でキャッシュするようにする。スコア17992であまり変わらず

15:00頃、icon画像をウェブアプリ内でキャッシュするようにする。スコア18196で大きくは変わらず

15:50頃、POST /api/condition/{id}でリクエスト中の最新のコンディションのみをINSERTする変更を加える。スコア35050で大幅に伸びる。
これはGET /api/isu/{id}を叩いたときに新しいコンディションを確認したときに加点される仕組みを利用したハックでした。
本当は全件を一括INSERTしたかったのですが、Rust実装で使われていたsqlxはそのようなSQL文を出力するAPIを持っていなかったのでやや手間がかかるのでこういう実装にしました。
グラフ内のコンディション数は下がってしまうので、そこでの加点はおさえられてしまいますが、それ以上にPOSTの負荷を下げることのほうが効いたようです。

ここでウェブアプリのCPU負荷が高く張り付いていることに気がつき、ウェブアプリを2台立てようと試行錯誤したのですが、なぜかスコアが下がってしまう現象に見舞われ苦戦します。
他にもアプリケーショ内キャッシュを導入したり、DROP_PROBABILITYを変更したり、スコア計算の仕組みを利用したハックの検討などをしたのですが、全て裏目に出てしまいます。
そうこうしている内に競技時間が過ぎ、再起動試験をした後、ログ出力を切り最終的にはスコア39107ということで予選敗退しました。

敗因分析

まず、ウェブアプリの分散に失敗したことですが、ウェブアプリ内でキャッシュを持っていてそれを共有していなかったのが原因のようです。
アクセス数のみをみてPOST /api/condition/{id}だけを別サーバーで捌く、などをしていたのですが、その場合、最新のconditionのキャッシュがPOST時に更新されないことでスコアが下がっていたようです。
今回、POSTの内容を全て無視しても整合性チェックで怒られることがなく、競技中は何が原因かの特定に失敗してしまいました。
ただし、nginxのみを分離する、ということは簡単にできたはずで、これはベンチマーク公開後に試してみたのですが、それだけでスコアが伸びそうな気配がありました(実際のスコアはベンチマーカーサーバーが非力だったためわからず)。

また、nginxからエラーメッセージが出ていて、リクエストがでかすぎるせいで一時ファイルが大量に生成されていることに競技中に気がつきませんでした。
確かにToo many open fileのエラーはアプリ側のログで確認したのですが、ulimitの変更で誤魔化していました。
nginxからのエラーメッセージ見逃しは去年もあったのに、同じミスを繰り返してしまいました。

あとは、isuテーブルからオブジェクトを引っ張るときに全てのカラムを引っ張っていたのですが、これをやめるのもかなり効果的でした。
これは僕は競技中に提案したのですが、そこまで優先度高くないよね、ということで先送りされていました。
が、実際にはicon画像が載っているカラムを引っ張ってきたりするので、かなりの負荷があったようです。
また、無駄なtransactionを生成している部分を消すのもかなり効いたようでした。
このへんはDBでの負荷が比較的少ないように見えたので、きちんとログを確認しておらず気がつきませんでした。
sqlxにはバグがあり、リクエストが中断したときなどにトランザクションを正常に終了できずエラーを吐くというのがあったので、
トランザクションを減らすことはその手のエラー処理の際のCPU負荷を軽減することにもつながっているので、Rustの場合は特に重要だったようです。
ただし、これらのテストはvagrant環境で同一インスタンスにベンチを含めた全てのアプリケーションを入れた場合のテスト結果なので、もしかしたら効果の大小は見誤っているかもしれません。

GET /api/trendはVarnishでキャッシュする戦略はかなりありだったと思います。
実は、事前練習でVarnishの導入は練習していたのですが、使うという発想が出てきませんでした。

総じて、本質的な負荷軽減があまり出来ていないにもかかわらず、チーム全体の思考がスコア計算をハックする方向に傾いてしまっていたのは痛かったと思います。
また、コードの変更内容をチーム全体で共有できていなくて、間違った修正が入ることも多かったので、ある程度チーム内で変更をレビューするステップを加えてもよかったと思います。

感想

結果こそ出せませんでしたが、今年も十分に楽しめました。運営の方々には改めて感謝です。
今年のルールは去年に比べると変更すべき点が多く、スコア計算の仕組みが結構複雑で戦略を問われるものになっていたと感じました。
個人的には、スコア計算の仕組みはシンプルに整合性を保ってリクエストを捌けた数を競うようなルールが好みなのですが、現実のアプリケーションでも厳密にすべてのリクエストを捌いて常に整合性を保つということは要求されていないことも多いと思うので、これはこれでありなのかなあとも思います。俺はどっちでもいいけど。