ISUCON4 本戦に参加して8位でした

ISUCON4 にクックパッド選抜の†空中庭園†《ガーデンプレイス》として @ryot_a_rai@__gfx__ と参加し、結果は8位でした http://isucon.net/archives/41187491.html 。 使用言語で大きくスコアが分かれることはないだろうということで、3人が共通して慣れている言語として Ruby を選びました。

最初に試しにベンチマークを一回実行しつつ app.rb を読んで、一回の実行だけでかなりスワップしていることと、入稿された動画が Redis に保存されていることに気付いて、まずそれを何とかしようとした。 LTSV 形式で書かれた nginx / Apacheアクセスログからレスポンスタイムの情報を出す access-log.rb を用意して、その結果から動画の配信が支配的だということがわかった。 Ruby の初期実装では全く使われていない ADS_DIR という意味深な定数があったので、そのディレクトリへ動画をファイルとして保存するように変更し、Ruby ではなく nginx が返せるようにした。 同時に、CPU コア数に対して多すぎる unicorn のワーカ数を減らしたり、unix domain socket を使うようにしたりする変更がされていた。

別々のサーバがそれぞれローカルにファイルを置くとなると、ファイルをどう配信するかが問題になる。 一番最初は、各サーバに ID を割り当て、動画がアップロードされたときに Redis に自分のサーバ ID も含めて保存するようにし、GET /slots/:slot/ads/:id/asset されたときにその動画がどのサーバにあるのか Redis から引いて、そのサーバへプロキシするようなものを golang で用意するような構成を考えていた。 しかしその構成を実装する前に、動画へのパスは GET /slots/:slot/ad が返す JSON に含まれている URL で決まるのでは? という話が出て、試しにその API から返す JSON に適当なクエリストリングをつけてベンチマークを実行したところ、クエリストリングがついた状態でアクセスがきたので、GET /slots/:slot/ad を変えれば動画のエンドポイントは変えられることがわかった。 そこで、JSON に含める動画の URL にサーバ ID を加えることにして、nginx の設定で自分のサーバ ID が渡されたら自分で配信し、そうでなかったらそのサーバ ID のサーバへ proxy_pass するよう設定した。 また、初期実装ではレポートの元となるログがローカルファイルに出力されているせいで複数台の構成でうまくいかないことがわかったので、ログも Redis に保存するようにした。

この状態で再度ベンチマークを実行したところ、CPU がほとんど使われておらず、I/O 負荷も全然高くなくてネットワークがボトルネックとなっていることがわかった。 最初は CPU が弱い1号機を Redis 専用サーバにして、残り2つをフロントかつアプリケーションサーバとして使おうと考えていたけど、3台ともフロントに置く構成でいくことにした。 また、サーバ間の無駄な通信を避けるために、GET /slots/:slot/ad がパスだけではなくホスト名も含んだ状態で動画の URL を返すようにして、nginx 間の proxy_pass が不要な形にした。 この状態で8000点近いスコアを出せて、結局ほぼこのときのスコアのまま終わってしまった。

この後、hiredis-rb を使うようにしてみたり、リダイレクトを一段減らしたり、Linux や nginx の設定でネットワークパフォーマンスを上げようとしたりしたけれども、どれも大して効果は出なかった。 リモートでのベンチマーク実行のスコアが全然安定せず、普通に±1000点くらいのバラつきがあったので、どれが効果があってどれが無いのか判断に困った。 スタンディングを見ても上位チームのスコアの差はだいたい2000点くらいの範囲に収まっていて、リモートのベンチマーク実行の不安定さを考えるとほとんど差が無いと思っていた。

帯域の制限に悩まされているときに、一度30万くらいの異常なスコアを出したチームがあった。 これを実現するには、どうにかして動画を配信せずに済む方法があると思った。 動画を返すときにちゃんと Last-Modified ついてるよなーという確認はしたんだけど、その先の Cache-Control には全然気付いていなかった。

競技後に聞いた話の中では、グローバル IP とプライベート IP の両方を使うという発想は全然なかったなーと反省した。 たしかに NIC 2つあるんだから、帯域の制限で困ってるんだったら両方使う発想は出てもよかった。 とにかく帯域に悩み続けて、メモリや I/O は十分に余裕があって CPU もほぼほぼ idle という状況だったので、app.rb 内の削れる処理に気付いたとしても「でも CPU は超余裕なんだよな……」となって進めなかった。 自分は今回が初めての参加だったけど、悔しいので来年もきっと出ます。