たごもりすメモ

コードとかその他の話とか。

ISUCON6決勝を戦って敗北した

ぼーっとしてたら1週間が経過してしまった。先日のエントリのとおりISUCON6決勝に通ったので戦ってきた。チームはもちろん引き続き @joker1007 と @tnmt との3人、チームJingisukan。そして負けた。6位。

f:id:tagomoris:20161022085352j:plain

やったことは色々あったし、それ以上やれることも、やっていてうまく結果に出しきれなかったことも色々あった。後悔することもあるけど、出題内容の中で自分(たち)がよく知らず最適化しきれなかったこともあったので、順位はともかく勝てなかったのはしょうがなかったなあ、という感じ。
おおざっぱに経緯を記しておこうと思う。

あれこれ

事前準備は特になし。空のリポジトリを用意したくらい。

当日は余裕をもってチームで集まったので、雑談したりうろうろしながらリラックス。競技開始後、出題内容見てウヘーってなってた。
とりあえずデプロイしてみるも、10コア制限にひっかかってうまくいかず。参加者チャットに同じ目にあってるチームがいっぱいあってちょっと安心する。
しかし Microsoft Azure のコンソール、かっこいいのはいいけどサイズ変更不可な部分で画面を大きく占有されて、MacBookAir 11inch の縦に狭い画面では本当に必要な部分がほんのわずかにしか表示されない、みたいになってつらい。

コア数変更が発表されて再度デプロイ、今度は成功。5台に鍵を配ってログインできることを確認したりしつつ、初期ベンチを回す。そこからRuby実装に切り替え、nginx reverse proxyを置いてログ取得、など。並行してコードを読んだり、手元のMacで環境を起動するための docker-compose 設定ファイルを準備してもらったり、何も設定されてなかった isu02 から isu05 まででもアプリが起動するようにセットアップしてもらったり。

  • 10:32 4048 (PHP実装初期スコア)
  • 10:41 3012 (Ruby実装初期スコア)

この間、11:30くらいに一度手を止めてその後の戦略について話しあう。とりあえず10コアあるけど react.js のサーバサイドレンダリングをやっているプロセスと本プロセス(Rubyアプリケーションサーバ)の負荷のバランスがよくわからず、とりあえずで以下のような戦略を決める。

  • node + ruby をセットで公平に複数台でバランシング
  • isu01 だけは nginx + MySQL を docker 外で起動して使う、アプリサーバはこいつは除外する?
  • Redisなんかの必要があればそれも isu01 で
  • 10コアあるので、負荷によっては isu01 に6コア集めて isu02 〜 isu05 は1コアだけにする?*1
  • コードに N+1 問題的なものがいろいろあるので、それは潰す

このくらい。
f:id:tagomoris:20161024111546j:plain

この時点で react.js アプリの詳細をあんまり深く読み込んでいなかった + react が処理するパスと /api 以下の違いをきちんと追っていなかった + reverse proxyでのキャッシュ戦略についてもっとちゃんと考えて準備をしておくべきだった …… みたいな後悔は今にして思うとある。

  • 12:53 2454 (nginx reverse proxy追加)
  • 13:26 2608 (isu02, LTSV access log追加)
  • 13:31 2509 (MySQLをDocker外部の常駐プロセス化)
  • 13:51 1341 (strokes -> points の N+1 クエリをJOINで1クエリ化、失敗)((全pointsに変化のないstrokeデータをくっつけちゃって、クエリ結果のデータ総量が増大したためだと思う。revertした)))
  • 14:09 1289 (アプリケーションサーバのpuma化)*2
  • 14:27 767 (nginx - nodeプロセス間の通信を https から http に)

このあたり、明らかに効くはずの変更をしてるのにスコアがどんどん下がってて、おいおいどういうことだという話に。FAILしてなければとりあえずOKという感じで進んでたけどベンチマークが大量にエラーを報告しているのを見てなかった。ロジックがおかしいわけではなく、レスポンスタイムアウトなどが大量に報告されてて、あれーとなる。
とりあえずそのへんの対処をしよう……という感じで調整的なことをあれこれやりはじめる。主にはMySQLのコネクション。

  • 14:51 5712 (MySQLへの接続の connection pool 化)
  • 15:03 6338 (strokes -> points N+1 to 2)
  • 15:27 6115 (react 2プロセス化)
  • 15:33 5974 (JSONリアライザをOjに)

ここまではすべて1ノードでの実行。複数ノード化すれば高速化するはずの構成をずっとひっぱってきていた。ある時点からじょーかーさんに複数ノード化の作業をお願いしていて、ここでマージして初めて試す。これでいっきに上位に行けるかなー、と思っていたら、思ったほどではなかった……。自分はこの間、コンフリクトが怖くて大きめの変更を試せず、うまく行くような行かないような微妙な変更ばっか試してた気がする。

  • 15:49 11667 (5ノード化)
  • 15:54 11760 (roomの人数カウントを COUNT を使うSQLに)
  • 16:00 15743 (nginx worker_connections 4096)
  • 16:07 14605 (nginx worker_process 2 nofile 20480)

複数ノード化してみたらMySQLに負荷がだいぶ来るものの、これ以上クエリの改善はどうにも難しそうだぞ、というのがこのあたりではっきりしだす。時間がなくなってきた割にいまいち上位に行ききれなくて焦ってきたあたり。やっぱりキャッシュをちゃんと考えないとダメかーという感じになるが、reverse proxyでのキャッシュ戦略を最初にちゃんと考えてなかったツケがきて、ここで考えるのに時間を使ってしまった。
このとき考えたアイデアは以下のみっつ(実装の案としては4つ)。

  1. RubyアプリケーションからMySQLへのクエリを減らすべく、とにかくあれこれRedisにキャッシュ
  2. Reactのレンダリング結果キャッシュ(案1): nginx でmruby/luaで頑張る?
  3. Reactのレンダリング結果キャッシュ(案2): stroke登録時にrubyからreactにリクエスト発行 → レスポンスをnginxのキャッシュとして登録して次からはノータイムで nginx から返す
  4. /api 以下のレスポンスはReverse proxyからRubyアプリケーションに直接流す

ここで一番最後の項目、やっちゃえばすぐにできる内容だったんだからやればよかったのに、なんとなく後回しにして結局やらなかった。これはもう、ものすごく後悔している……。
RubyアプリケーションでとにかくRedisにキャッシュするのはじょーかーさんにお願いした。最終的にはこれがこの後2時間で唯一の前進になった。

  • 17:10 18102 (Redis導入 stroke/pointキャッシュ)
  • 17:27 18989 (roomキャッシュ)
  • 17:37 18059 (再起動試験)

Reactのレンダリング結果のキャッシュについては、nginxで頑張る案については正直初手からの準備が足りなさすぎて、この時刻から試すには厳しすぎた。このため案2を採用してRubyのコードからReactにリクエスト投げる → nginxはWebDAVを設定、レンダリング結果をPUTしたらnginxはtry_filesでキャッシュをノータイムで返す、という方針をざっと立てる。nginxでのWebDAV設定をつねさまにお願いして自分はRubyのコードの変更をガッと。
が、このときnginxの設定がうまくいかず時間を浪費 + nginxをdocker-compose化しておかなかったため手元で試せない構成になってしまい、自分のコードもあれこれエンバグしてた。最終的には stroke 登録時にやることが多過ぎたせいで(?) POST リクエストに対するタイムアウトが多くなってしまい、加点要素にしきれなかったと思う。再起動試験後にようやくデプロイ可能状態になって変更を試したんだけど、スコアはほぼ変動なしだったように思う。ベンチマークが報告するエラーは増えていた。

この時点でフィニッシュ。17時前に見えていたスコアから上位は5〜8万点くらいで戦うチームばかりになると思っていたので大幅なスコアアップを目指した手ばかりを打って、あまりうまくいかないものが多かった、という結果になってしまった。

結果

フタを開けてみたら上位があまり伸びておらず、6位。しかも3位までが近くて、えー、もっと堅実な手(/apiの処理とかプロセス/スレッド数調整とか)をちゃんと打っておけば3位にいけたのに! という感じだった……。。。

f:id:tagomoris:20161022192047j:plain

しかし懇親会などであれこれ話したところ、自分たちではきちんと打てていない手がいくつもあった。たぶん影響が大きいものは以下のふたつ。どちらも優勝チームが打っていた手。

  • Reactプロセスのバックエンド化
  • SVGファイル生成とキャッシュ

一度全リクエストをNginxの背後のアプリケーション(優勝チームならGolang、我々ならRuby)で受け、Reactでのレンダリングが必要なものについてだけReactのプロセスにプロキシする。これによりレスポンスを自前アプリで簡単にキャッシュ制御することが可能。
これは正直思い付かなかった、が、思い付いてもよかったなあという内容。/apiのルーティング変更をやらなかったのもそうだけど、Webアプリケーションの構成に対する発想が貧弱になっている気がする。うううう。

SVGファイル生成とキャッシュについては、SVGファイルの中身は実際にはテキストで*3、かつstroke追加時には前に生成されていたものに1行追加するだけで済むので、cache invalidation + 再生成しなくても1回のI/Oで更新後のSVGファイルを生成できる、という話。
正直、まじかー、と思った。SVGファイルというものについての理解が欠けてた。
他のチーム(準優勝チームなど)ではReactのプロセスの外でSVG生成をやってたところもあるらしい、が、この更新時に1行追加というのはファイルフォーマットに対する理解あってこそできることで、なるほどなあ、という感じ。負けた。

そのほか懇親会で出題チームと話したところでは、今回のベンチマーカーは時間が経つごとにどんどん並列度が上がっていくというものだったらしい。なるほど、そりゃリクエスト/レスポンスのタイムアウトが減らないわけだ。たぶんこれにひっかかって最終的なベンチマーカー並列度を充分に上げられていないチームばかりだったのではと思う。*4
あとどこかで思い切って頭おかしいくらいアプリケーションのプロセス数とかを増やしてればガツンとスコアが上がったかもなあ、とか、まあ思うところはいろいろある、が、手を動かしきれなかったなあ。

雑感

なんにしろ面白い問題でした。出題チームのみなさん、お疲れさまでした&ありがとうございました。1日みっちり楽しませてもらいました!

そして、ついにISUCONで敗北してしまったので、ISUCON1からの無敗伝説保持者は誰もいなくなってしまった。これもイベントが成熟してきた証だと思う。ううう……。。。くやしい。
しかし思えば、自分ももう4年くらいマトモに大規模Webアプリケーションをやってないので、負けるのも無理からぬことだよなあ、という気がする。現場にいない人間が勝てるような競技ではない、というのは自然だと思う。去年の出題は頑張った。……という感じでこの1週間は自分を慰めている。

みんなでウィッシュリストをクリックしよう! あと出題側の話をするということで、以下のようなイベントをやるらしいです。自分もパネルでなんか言う。たぶん。

おかれさまでした! また来年! お疲れさま会をジンギスカン屋でやろう! > @joker1007 @tnmt

*1:実現可能性については考えてなかった

*2:並列度の調整をまったくしてない

*3:ここまでは知ってた

*4:このへんのロジックは聞いてみたところちょっとおかしくない? という感じのもので、あれこれ議論してた。講評を待ちたい。