ISUCON12予選敗戦記

すでにずいぶんと時間が経ってしまったのですが、ISUCON12の予選に参加していました。結果は思わしくなく、予選敗退に終わってしまいました。
もう記憶がおぼろげになってしまっている部分もあるのですが、覚えている部分だけでも振り返って反省していきたいと思います。

事前準備

今回はメンバーは2人でした。

事前練習用にはさくらインターネットさんのご厚意でさくらインターネットのクーポンが参加者に配布されて練習環境を整備することができたので、ありがたく使わせていただきました。
練習課題としては「達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践」の題材であるprivate-isuを用いました。
ただし、Rust実装は公式ではなかったので他の人が実装したものを使いました。

この練習会で、ansibleで最低限のbashrcや各種ツールをリモート環境にセットアップするものや、slackにalpでのログの結果を集計してアップロードするスクリプトなどを整備しました。

連絡はslackを用いることにしました。

予選本番

時刻は大体の時間です。

10:00 予選開始。ルール確認やコードをgithubに上げる作業などを行う。

10:33 Rustの初期実装でベンチを回す。Score: 2504

11:10 Docker上で動いていたアプリケーションを直接動くように変更。 Score: 2775

昔のDockerだとローカルホスト間のネットワーク通信のパフォーマンスが悪かったはずなのだが、解説でもそこは重要ではないとのことだったので、多分もう関係ない話だと思われる。
ただし、自分たちはRustのコンパイル済みのバイナリを手元からアップロードするデプロイ形式をとっていて、そのままのDockerスクリプトだとまずコンパイルが走ってしまうという仕組みになっていて非常にデプロイが遅くなってしまうため、外すという判断をした。
他のチームだとサーバーでコンパイルしてしまうとメモリ不足で困ったとかいう話も聞いたので、この判断は正解だったと思う。

11:31 DBアクセス時のロック周りが怪しいということで適当にはずしてみる変更をする。

何回か走らせて確かにパフォーマンスは上がるが、failedになるケースがあるのでむやみに外せないということになった。

ここで、ロックをファイルを用いる形式からRedisを使う形式に変更するという判断をして、自分が実装することになった。
しかし、Redisの習熟度が十分でなく、実装にかなり手こずってしまう。

11:42 もう1人のメンバーがMySQLのid_generatorを使っていた箇所をuuidを用いてIDを生成する変更する。 Score: 4639

このへんでロックを適当に外したりとか自明にいらないクエリを削除したりなどを相方が試すがたいしてスコアは改善せず。

14:18 ロックを改善する実装がようやく出来上がる。 Score: 4376でたいして改善せず

このへんから自分はvisit_historymin(created_at)なものしか引っ張ってきていないことを利用した最適化の実装にとりかかる。
相方はsqliteを使っている箇所をMySQLにしようとする。

15:43 MySQLを別サーバーに分離 Score: 4315

スコアこそ改善していないが、CPU使用率などのメトリックは改善していたので、将来的には有利なるはずと判断。

しかし、これ以降、やりたい改善の実装がうまく動かず、小手先の細かい改善をいくつか入れる程度で予選終了。なんの成果も得られませんでした。

延長戦

まず自分が担当していたvisit_historyの最適化ですが、DBのトランザクションを貼った時commitをし忘れるとかいうイージーミスでした。
書いていたときの気分としてはdropハンドラでやってくれるだろうと思っていたのですが、実際に呼ばれるのはrollbackです。冷静に考えればそのとおりですよね。

sqliteの移植作業の方はほぼほぼ完成していたものの、player_scoreテーブルが巨大でインスタンス起動時にすべてインサートできないという問題がありました。
しかし、player_scorerow_numが最大のものしか利用されないという性質があるので、それを利用することでインサートする量を圧倒的に減らせます。
これには本番中には気がついていたのですが、実装が間に合いませんでした。

これらを直してplayer_scoreのCSV入稿による一括insertをbulk insertにしたり、transactionを取ることによって不要な自前ロックを外す、MySQL化できたのでN+1だった箇所を撲滅する、Redisでキャッシュするなどを実装することで最終的には5万点以上の点数をとることができました。

bulk insertをする場合、たまにMySQL側でデッドロックを起こすことに気が付きました。どうやらおなじテーブルへのbulk insertが複数スレッドで実行されると、トランザクションを貼っている状態でもindex更新のためのロックを取り合ってdead lockを起こすようです。
正直なんでMySQLがこれを正しくハンドルできないかはよくわからなかったのですが、Mutexを使いbulk insertできるのを1スレッドに限定することでなんとか回避しました。

反省

まず、素振りが足りていなかったのが大きかったかなと思います。
ライブラリを使い慣れていればtransactionがdropでrollbackされるなどでつまずくことはなかったし、
Redisでファイルロックを置き換える作業ももっとスムーズにできたはずです。

また、チームメイトを悪い意味で信頼しすぎていて、作業を任せっきりにしてしまったのもよくなかったです。
コンテスト中はどうしても焦りが出てしまったり、そもそも知識が足りていない分野だと気づきが共有できていなかったりします。
ロックについて相方とじっくり挙動を調べたりすれば、Redis置き換えなどという無駄なステップを踏まずにすんだかもしれないし、
SQLite移行ももう少しすんなりいったかもしれません。
ペアプロや一緒に考える時間はもう少し積極的にとって良いと思いました。

RustのWebアプリケーションでのプロファイリング手法についてもちゃんと検討したいなと思いました。
MySQLやnginxのログ解析はpt-query-digestmysqldumpslowalpなどで確立されていますが、各関数でどれくらいの処理がかかっているかのデータを知りたい場面はそこそこにあります。
他の予選通過チームは自前のmacroを用意してOpenTelemetryでプロファイリングをとっているようだった。

著名なライブラリであるtracingでもSQLの文字列とかはキャプチャできないが、最低限関数呼び出しのトレースは取れるはずなので次回は活用したい。

他チームで参考になりそうな戦略としてはSQL文を一旦すべてスプレッドシートに洗い出すという戦略はよさそうと思いました。

狙いとしてはアクセスパターンを一覧にすることで、不要なDB操作を洗い出したり、今回の場合はSQLite移行の難易度を見積もったりとslow query分析だけでは見えてこないものを発見しやすくするというもので、これはシンプルながら強力そうな戦略に感じました。
やはり冷静になってアプリを分析する時間は必要。

来年はちゃんと3人メンバーを集めて予選通過を狙いたいです。