ISUCON8予選で1日目に2位で本選出場が決まりました

今回のISUCONは転職してから初めてのISUCONでした。

ISUCON予選には以下のような特徴があります。

  • 会場は各自で用意する必要があるので、普段使い慣れていて作業場として最適なオフィスでやれた方が有利
  • ISUCONは3人のチーム戦なので普段一緒に仕事をしている人と出た方が息を合わせやすい

これらの特徴があるので、同じ会社の人と組みたいと思っていました。ということで今回は会社の同僚である @cubicdaiya さんと @syu_cream さんの3人で出場しました。

一昨年は出題側だったのに、去年は惨敗してしまったので、今年こそは本選に進みたいと思っていました。結果土曜日の2位で本選出場が決まりました。無事に進めてよかったです。

やっていったことを軽く振り返っていきます。リポジトリは以下のものです。

このエントリーはあくまでも私の視点になります。ISUCONでは過度に情報共有をしすぎても時間をとられてしまうので、他のメンバーの作業内容を完全に把握できていないためです。

事前準備

事前に打ち合わせをして最初の30分間に何を行うのかをあらかじめ決めておきました。

それと自作のcatatsuy/notify_slackのsnippet対応をしました。

notify_slackはISUCONではめっちゃ便利なのでみんな使ってください。感想・意見などもお待ちしております。

当日の流れ

時間はざっくりです。

10:00 開始

と行きたかったのですが、チームメンバーの体調がよくなく(私も気圧が低めでそんなによくなかった)、少し開始が遅れました。

私の主な仕事内容はソースコードのリポジトリへのpushと開発環境構築とデプロイスクリプト作成なのでそれを黙々とやりました。

Goのソースコードでgbが使われていて、「今更gb!?嫌がらせか!?」って気持ちになりながら、速攻でGoのvendoringが使える構成に変更しました。
それと手元のMySQLで動かそうとしたらエラーが出てしまいました。sql_modeが厳格化されたことによるものだったみたいなのでmy.cnfsql_mode = ""を足してMySQLを起動し直したら動いたので、割とすんなり作れた印象です。

デプロイスクリプトはインフラ担当の @cubicdaiya さんの作業とバッティングしかねないので、バッティングしないように検証して作成しました。

また普段使っているパフォーマンス計測用のスクリプトがnginxのことしか考慮してなかったので、初期実装のh2oを辞めてOpenRestyに変更し始めました。

11:00 開発開始

このあたりで開発を開始しました。開発環境で流しているクエリを見れるようにしたところ、ものすごい量のN+1クエリが流されていて、まずここを潰さないことにはどうしようもないことがわかりました。
そこでまずはgetEventgetEventsをどうにかしないと先に進めないので、私がそこの解消に着手しました。

私が実装している間、予約など他の箇所にボトルネックがないのか @syu_cream さんが調査していきました。ただ最初のボトルネックとしては getEvent が圧倒的なようで、そこをなんとかしないと先に進めない状況は変わりませんでした。

まず簡単に直せるところだったgetEventsでeventsテーブルの情報をとってきているのに捨てて、その後呼び出しているgetEventでさらに取得しているという問題をコードのコピペで直しました。

12:00 getEventをなんとかしたい

遅いクエリのGROUP BY ~ HAVINGはアプリケーションのロジック的には必要なのですが、ベンチマーカーはチェックしていないのでは?ということで試しに外してみたりしていました。

結論から言うと、初期ベンチは大量にリクエストが遅れないためにチェックできていませんが、ある程度リクエストが来るようになるとチェックするようになるみたいです(この辺は割と今でも謎)。

一旦はこのロジックを削除した状態で実装して、後で問題がありそうだったのでロジックを追加したのですが、いったんシンプルな状態にしてから実装できたので結果的にはよかった気がします。

同時並行でgetEventの中でsheetsに何回もリクエストを送っている部分を外に出して回数を減らすのもやっていました。

13:00 ベンチマークのランダムfail地獄突入

準備していた変更をデプロイするとどうしてもfailすることがあり、その原因がわからず数時間苦しみ続けていました。

この状況でいろんな変更を入れたり、revertしたりを繰り返していきます。

14:00 ランダムfail地獄を脱する

色々試していき、よくfailする原因になっていた /admin/api/reports/events/:id/sales を見ていきました。データがないとポータルには書かれていますが、その後見るとちゃんとデータはあります。
そこで変更が反映されてないのにリクエストが来ているのでは?という仮説を元にこのURLアクセス時にSleepを入れるという荒技を行いました。結果的にこの変更でベンチマークが安定し始めて、開発に戻れる見込みが出てきました。

正直ここまでチーム内はお通夜ムードでしたが、なんとか持ち直していきます。

15:00 スロークエリを眺めて次の作戦を考える

なんとか持ち直し、スロークエリから次の作戦を考え始めます。試しにORDER BY RAND()を外したり(これは結局ダメでしたが)、まだgetEventsのN+1クエリが撲滅しきってなかったのでなくしたり、sheetsに投げているクエリをfor文の外に出したりしていました。

16:00 次の作戦を考え始める

@cubicdaiyaさんは複数台構成について考え始めていきました。開発担当の2人はスロークエリから他に対策できないのかを考えていきました。

私はeventsテーブルのオンメモリキャッシュを実装していましたが、結局ベンチマークを通せなかったので、最終的にこの時間帯から始めた実装はデプロイできませんでした。

複数台構成は3台中1台がMariaDB、残り2台がアプリケーションサーバーという構成にしました。シェルスクリプトを実行しているので/initializeだけMariaDBが動いているサーバーで実行するなどの工夫はありましたが、複数台構成はできていました。

17:00 再起動試験・決め手になったインデックス追加

再起動試験をやり、特に問題ないことを確認した後に、まだMariaDBサーバーの負荷が高いのでそこを何とかしようと @cubicdaiya さんがスロークエリを確認していきました。

そこで reservations テーブルの event_idcanceled_at IS NULL で絞り込んでいるので、そこに複合インデックスを貼りました。ちなみにMySQL/MariaDBではNULLが含まれるカラムに対してもインデックスは有効です。

これが決め手になり4万点超えになりました。ただベンチマークが通ったりfailしたりしていたので、いいスコアが出るまで何回も実行し続けて、いいスコアが出たところで触るのを止めて今回のISUCONは終了しました。

感想

問題はISUCON予選として非常に練られた問題だと思います。ベンチマーカーの実装は非常にいろんなことをチェックしていて、実装は死ぬほど大変だったろうなという気持ちです。ベンチマーカーのチェックが厳しすぎるのか、それに翻弄される時間が圧倒的に長かったですが、それもISUCONっぽさという感じがします。

今から考えるともっと実装をいじってスコアを上げたかった気持ちがありますが、ベンチマーカーに翻弄されたり、わからない部分が多くて判断に迷う部分などが多いので、ISUCONではいつもやりきれずに終わってしまいます。
しかしそれは全チーム同じ条件なので、その条件の中で一番やりきれたチームが勝利する大会です。今回の予選の反省をチーム内でしつつ、本選に備えたいと思います。