ISUCON4 本戦 に参加した記録。 チーム名は GoMiami で、@Spring_MT、@niku4i、自分(@sonots) の三人で参戦してきました。 結果は ISUCON4 本戦結果 の通りで、 8位〜24位が 8000 ± 400 点の団子状態になってて 8017 点の 17 位という結果でフィニッシュでした

事前準備

予選は1台構成だったから、Golang 使えばプロセス間メモリ共有とかいらなかったけど、本戦だと複数台になって結局必要だから、慣れてる Ruby でよさそうって話してて、本戦は Ruby で出る事にした。

ということで、予選の問題をさらにチューニングしたり、Ruby で解き直したりして、cheatsheat のアップデートしたりしてた。

golang で sql, template, http リクエストのパフォーマンスメトリクスとるライブラリ作った #isuconの ruby 版も欲しいなぁと思ったので、sinatra-template_metrics とか、mysql2-metrics とかも作ってた。ruby だとメタプログラミングできるのでサクっと作れて簡単だった。(結局使えなかったけど)

あと、複数台構成になるから、複数サーバをさくっとセットアップできるように、niku4i に chef レシピ用意してもらって、それぞれのマシンで dstat とか top を同時に動かして見るの辛そうだから何かメトリクスツール欲しいなって話してたら、td-agent からデータを Google App Script に毎秒投げて、Google Spread Sheet でグラフ化するというものをさくっと作ってくれてた。これで帯域詰まってるの一目瞭然だったしすごい便利だった。GAS (Google App Script) 職人すごい。niku4i++

最初にやったこと

まず最初は予選の時と同じく、レギュレーションしっかり呼んで、アプリ動かしてメトリクス取ってみて、コード読んだり、アプリ触って動作把握したりという基本の作業をしてた。この最初の段階で取れたメトリクスはこちら

sum(sec)         count   avg(sec)
428.534019 358 1.197022402 GET /slots/:slot/ads/:id/asset 61.50906600 22 2.795866636 POST /slots/:slot/ads 14.64960199 86 0.1703442093 GET /me/report 7.29174599 358 0.02036800558 GET /slots/:slot/ads/:id/redirect 6.53089999 358 0.0182427374 POST /slots/:slot/ads/:id/count 4.072345000 358 0.0163805294 GET /slots/:slot/ad 3.499311999 358 0.00977461452 GET /slots/:slot/ads/:id 0.233793 5 0.0467586 GET /me/final_report 0.059209 1 0.059209 POST /initialize

明確に動画のPOSTとGETで時間かかってるかんじだった。あと、I/O負荷が高かった。

この時点で、1回ミーティング開いて、どういうアプリかシェアしたり、どういう戦略でいくか決めたりしてた。この時に出たアイデアはこんなかんじ

  1. 去年と同じく benchmarker のオプションに複数台指定して、分散アクセスしてくれる仕組みのようだったので、 フロントで帯域制限とかあった場合に、フロントを3台全部に分散させて受け取ったほうが絶対よさそう
  2. 動画は redis に保存されてるけど、それぞれのマシンのメモリ容量が1GBしかないので、スループットあげたら、 1台じゃメモリ足りなくなりそう。redis 3台に分散させたほうがよさそう(これ、benchmarker が投げて来る動画の種類は実際の広告と違って限られてるのでうまく再利用すれば気にする必要なかったかも => 後記: その戦略は利かないとのこと)
  3. 1番のマシンだけ CPU コア数が 2 じゃなくて 1 なので、その1台はあえて別の使い方をしたほうが良いのかも
  4. I/O負荷が高い。動画をPOSTされた時にデータを redis に突っ込前に、RackMultipart が一旦ディスクにファイルを吐き出してしまっているっぽい。tmpfs 使うようにしたほうがよさそう。
  5. 動画をGETする所で、Range ヘッダを見て、ruby で動画ファイルを分割して返しているっぽい?あらかじめ range で切った動画を作って保存しておいたほうがよさそう(これは、Range ヘッダのパターンを洗い出そうと思ってログ取ったら、benchmarker からのリクエストは常に空だったので関係なかった。コメント読むと Chrome 特別対応だったぽい)

とりあえず3台有効に使う分散構成取れるように実装しようって話して、分担して作業することにした。 お弁当並んでたから食べようと思ったら、13:00 までダメです!って言われてたのがこの辺。

実装作業

以下の作業を分担して実施した。

1. redis の書き込み/読み込みをシャーディングする (sonots)

key を適当なハッシュ関数(key.bytes.inject(:+) % 3)に通してどの redis に書き込み/読み込みするか決定することで分散するようにした。けっこうサクっと終わった。

2. final_report 用の log をどこか1つのサーバに保存 (SpringMT)

アプリを分散するとなると final_report 用の log をローカルファイルに書いていたのをやめて、 どこか1つに集約して保存する必要がありそうだった。ので、そこを改修。

3. Range のパターン洗い出しと、あらかじめ切ってキャッシュ (niku4i)

ログ取ってみたら、結局 Range 使われてなかったらしい。

4. rack の一時ファイル保存先を tmpfs にする。ファイルが残っても無駄に容量が使われるだけなのですぐ消す (sonots)

一時ファイルの保存ディレクトリを変更するやり方が分かってなかったので調べながら。オプションなどは提供されていないので、Rack のコードを直接いじる必要があった。rack/multipart/parser.rb#L104

5. その他ミドルウェアの設定変更とか (niku4i)

/ を nginx で返してもらおうと思ったけど、そこ benchmarker 使ってなかった。


分散構成自体では得点は伸びなくて(それはわかっていた)、Rack の一時ファイルを tmpfs にしたことで、ローカルで 11000 ぐらい出るようになったけど、remote では 3000 ぐらいしか出なくてうーん、って感じになってた。redis はシングルスレッドだから詰まっちゃってたのかな?要確認。

次にやったこと

これじゃ全然ダメだから構成変えようって話してて、 動画を redis にいちいち送りこむより、nginx で POST された動画をディスクに吐き出して、GET 時にローカルから読み込んだほうがよさそう、という話になったので構成を進化させた。 Range は全然使われてなかったし、気にせず動画を nginx から返すだけでよさそう。

それで、nginx で POST の動画部分だけ取り出して書き込んで、GET 時にそれを返すとかやりたかったんだけど、POST データのうちの一部(動画)だけ取り出すやり方がわからなかったので、 unix domain socket でつながっているローカル app に送り込んで、RackMultipart が吐いているファイルを nginx で指定している public ディレクトリ以下に mv するような形にした。この時点で tmpfs 対応は revert された。

フロント nginx 1台. app 三台. コア数が違うので host1 には weight1, host2 には weight2, host3 には weight2 で振る、という構成になった。

これでローカルでは 22000 ぐらい?うろおぼえ。remote では 8200 ぐらいでてて、remote では帯域で詰まっちゃってたからもう、わからんなーってかんじになった。

フロントを nginx 3台にして、動画ファイルがPOSTされたらアプリで他の2台にも scp で撒く、というアイデアも出たんだけど、みんな 8000 点台で止まってるし、そこを変えてもブレークスルーにはならない感じで、むしろ scp する分遅くなりそう、って話してて結局ちょっとパラメータチューニングして、レギュレーションクリアするための再起動動作確認したりとかしてフィニッシュしてしまった。

スコアが remote とローカルで全然違ってて、remote で試そうとすると queue 待ち10分とかで remote 側で全然検証できなかった。それができてたら scp 実装も試しにやってみてたかもしれない。もっと、remote benchmarker 走らせるマシンいっぱい用意してくれてたらうれしかったな。もしくは、benchmarker の実装に帯域制限いれて、ローカルでも似た挙動するようになってるとか。
 

懇親会で聞いた話

レスポンスの JSON の url を変更すると、どのホストに動画を GET しにいくか制御することができたらしい。なので、フロント nginx が3台でも、全ホストに動画を撒くなんてことをせずに、動画のあるホストを指定できたとのこと。 とはいえ、今回は benchamrker 実行ホスト側の帯域でつまってたから、その構成にしてもスコアはあんまりのびなかったらしい。けど、これがサーバ側の帯域で詰まっていたとしたら、三台構成取れてた他チームから大差でまけてたと思った。

あとは、redirect して返すエンドポイントは 200 で body つけて返せば1リクエスト減らせたし、 動画の配信順序も1度スロット全部返せばあとは1個固定でいいからサイズが小さいやつ(といっても、5.4M か 5.3M かの 5.3M のほうだけど)だけ返すようにしたり、動画自体は得点にはならないからエラーで返してしまえば減点にはなるけどひょっとしたら最終得点は上がるかもしれないと思って試したけど 25% 超えて FAILURE になってしまったからダメだった、とかモ○ス氏は言ってたし、そういうの全然試せてなかったな、と思った。キャッシュコントロールだけじゃない。

おわりに

benchmarker が賢いとか全然思ってなくて、benchmarker の挙動を追うことを全くしてなかったので完敗としか言えない。 リクエストヘッダすらみてなかったので、去年 RubyConf 行ってて本戦欠席してしまった経験値の低さが露呈された。 とりあえずリクエストヘッダをログを出すようにして、benchmarker の気持ちでチューニングできるようになろう。

運営の皆様お疲れさまでした!