Skip to content

Instantly share code, notes, and snippets.

@sonots
Last active August 29, 2015 14:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sonots/d867786811992b72af0a to your computer and use it in GitHub Desktop.
Save sonots/d867786811992b72af0a to your computer and use it in GitHub Desktop.
Isucon4 本戦に参加して17位でした [GoMiami]

ISUCON4 本戦 に参加した記録。 チーム名は GoMiami で、@Spring_MT、@niku4i、自分(@sonots) の三人で参戦してきました。 結果は ISUCON4 本戦結果 の通りで、 8千点台の団子の中に入って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++

最初にやったこと

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

428.5340199999998       358     1.1970224022346363      GET /slots/:slot/ads/:id/asset
61.509066000000004      22      2.7958666363636366      POST /slots/:slot/ads
14.649601999999996      86      0.17034420930232555     GET /me/report
7.291745999999995       358     0.020368005586592165    GET /slots/:slot/ads/:id/redirect
6.530899999999998       358     0.01824273743016759     POST /slots/:slot/ads/:id/count
4.0723450000000003      358     0.01638052941176471     GET /slots/:slot/ad
3.4993119999999998      358     0.009774614525139664    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)

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

5. ミドルウェアの設定変更とか、ボトルネック見つけたり (niku4i)

分散構成自体では得点は伸びなくて(それはわかっていた)、Rack の一時ファイルを tmpfs にしたことで、ローカルで 11000 ぐらい出るようになったけど、remote では 3000 ぐらいしか出なくてうーん、って感じになってた。

次にやったこと

これじゃ全然ダメだから構成変えようって話してて、 動画を 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 点台で止まってるし、benchmarker 側の帯域で詰まっている雰囲気だったから、そこを変えてもブレークスルーにはならないような感じだし、むしろ scp する分遅くなりそう、って話してて結局ちょっとパラメータチューニングして、レギュレーションクリアするための再起動動作確認したりとかしてフィニッシュしてしまった。

懇親会で聞いた話

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

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

おわりに

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

パスワードを「moristaosu」とかにしたら来年はISUCON勝てる #パスワードばれ RT @sonots: 面白い life hack / “「パスワード」が変えてくれた僕の人生 | TABI LABO” http://t.co/8c9nPSbRBn

— そのっつ (SEO Naotoshi) (@sonots) 2014, 11月 9
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment