ISUCON7 予選を通過した

ISUCON7 予選に†空中庭園†《ガーデンプレイス》として @ryot_a_rai@mozamimy と参加して2日目1位で通過することができた http://isucon.net/archives/50956331.html

リポジトリhttps://github.com/ryotarai/isucon7q

当日まで

例年 Ruby で参加していたけど、今年は発表された初期実装に Ruby が含まれていなかったこと (※後から追加された) もあり、@ryot_a_rai から Go で参加したいという話が出て Go を選択した。 僕と @mozamimy は Go はまぁまぁ書いたことはあるくらいの状態だったので、事前に一度 Go で練習したり pprof の使い方を教えてもらったりしていた。

Go は個人的にはあんまり好きになれなかった言語ではあるんだけど、ISUCON で使う上では普通に書けば普通に速くなるし、 制限時間があってプレッシャーが高い状況で typo とか型エラーとかのつまらないミスをコンパイル時に検出できるので、Ruby よりやりやすく感じた。 おかげでアプリケーションの改修中にほとんどバグを出さずに進めることができた。

当日

僕は主にアプリケーション側の改修を担当していて、MySQL の設定とか Redis をインストールして使える状態にするとか、deploy.sh を用意して git pull && make && systemctl restart && nginx のログローテートを自動化したりとか、そのへんは他の二人に任せていた。 以下、各サーバは isu1、isu2、isu3 と呼ぶことにする。初期状態で nginx とアプリが動いていたのが isu1 と isu2 で、MySQL が動いていたのが isu3。

とりあえずベンチマークを実行してアクセスログを見て GET /icons/:file_name が遅いことがわかり、コードを読むと MySQL に画像を入れてることがわかったので、社内 ISUCON でも見たな~と思いながらとりあえず Redis に入れることにした。 この時点では MySQL の負荷が高かったので Redis は isu2 に入れた。 これで速くはなったんだけどそれでもまだ icons が支配的で、 :file_name が画像の SHA1 値になっていてパスをキーにしてキャッシュできるので nginx の proxy_cache を使ってキャッシュを入れてみたもののほとんど改善せず。 CPU もメモリも余ってるのになんでこんなにスコアが出ないんだ? というあたりで public 側の帯域によるものだと気付いた。 isu1 と isu2 の両方をベンチマーク対象にしたらスコアは伸びたが、それでもリソースは余っていた。 これをなんとかするには画像を配信しないようにするしかなくて、いつかの ISUCON で見たように Cache-Control なのか?? となっていた。 レギュレーションに 304 のケースに関して明確に記述されていて、スコアが100分の1になってしまうものの帯域で詰まってる以上この先に進むには 304 を返せるようにするしかないと決めて試すことにした。 最初は Last-Modified と ETag をアプリケーション側で返すようにして tcpdump をしながら様子を見ていたけど If-Modified-Since や If-None-Match が icons に対して送られてこなくてうーん? と思いながら Cache-Control も返すようにしたら挙動が変わって 304 を返せるようになって一気にスコアが伸びた。 この時点で 16:15 くらいで 98840 点だった。

icons の壁を越えると MySQL や Redis がボトルネックになってきて、ここからは N+1 クエリを直すとか、Redis に移せるものは Redis に移すとか、select のカラムを絞るとか、いつもの作業になった。 MySQL のテーブル定義は変えてない気がする? そのへんは任せていたのでよくわかっていない。 微妙に JS や CSS のリクエストでエラーになっていたので、gzip_static on にしたり Cache-Control 系のヘッダを返すようにしたりしていた。 一通りやりきると MySQL や Redis が空いてきてアプリケーションの CPU 負荷がボトルネックになってきたので、Redis を isu2 から isu3 に移したりしていた。 isu1 と isu2 は nginx とアプリが動いていて、isu3 は public 側の帯域確保のために nginx が動いていてベンチマーカからのリクエストを isu1 と isu2 にプロキシしつつ、Redis と MySQL が動いている状態。

最後に Redis の負荷を分散させるために3台に Redis を入れてシャーディングするというのをやっていた。均等にばらけるのか不安だったけど id を3で割ったり icons のハッシュ値を適当に数値化して3で割ったりして接続先の Redis を切り替えるようにした。 MGET とか HMGET を使っている箇所が面倒だったので、そこは3台に同じクエリを投げてからその結果をマージするようなコードを書いた。 いま振り返るとちゃんと必要なキーだけ集めてクエリを投げるように直してもよかったな。 isu3 だけアプリが動いてなくて CPU 負荷が若干低かったので isu3 にウェイトを置くようにして比率を調整したりもしてみたが、あまり効果もなさそうだったので 1:1:2 の比率で決定にした。 このへんでコードフリーズということにして、pprof の削除やロギングの削除をやった。

ラスト1時間で再起動試験と最終スコアの確定をやった。レギュレーションでは最後のスコアが使われることになっており、今回のベンチマークは同じコードで実行しても一割くらいスコアにぶれがあることがわかっていたので、ここまでのベストスコアが59万点だったため58万点以上が出たらそれを最終スコアにしようという風に決めてベンチマークを実行した。 実際には再起動後のベンチマーク一発目で58万点が出たので、もう少し試したい気持ちもありつつも、最終的にはこのときのスコアのまま終えた。

感想

icons の 304 をさっさと試して最初の帯域の問題を早くクリアできたのが大きかったかなと思う。画像をファイルシステムに置いて nginx に任せるとかせずすべてアプリケーション側で扱うようにした結果、Last-Modified や ETag のずれではまるのも意図せず回避できていた。 アプリケーションの改修でバグを出さなかったのも調子がよくて、最後のシャーディングも一発でベンチマークが通って気持ちよかった (スコアはそこまで伸びなかったけど)。 予選から3台のサーバを使う問題で、ベンチマーク対象を複数指定できるという新鮮な設定で楽しめました。ベンチマークもエンキューしたらすぐに実行されるような環境で体験がよかった。 本選でもよいスコアを残したい。