ISUCON6 で優勝しました
@methane です。タイトルの通り、 ISUCON でとうとう優勝してきました。
チームメンバーは、(予選と同じく) @kizkoh (インフラ担当), @mecha_g3 (アプリ担当) でした。
私は予選のときはガッツリとアプリを書いていたのですが、本戦では netstat -tn
(←老害), top
,
dstat -ai
, sudo perf top
などをみつつ指示をだしたり、方針を決めたり、完全に未経験だった
node.js & react.js 対策をしたりが主な仕事で、あとは序盤のインフラのタスクが大量にあるときに
MySQL を docker から外して基本的なチューニングを入れたり Go を100行程度書いただけです。
結果的には優勝できましたが、メンバーの2人がよく準備し本番でも実力を発揮してくれたのに対して 僕の戦略ミスで中盤から全くスコアを上げられなかったので、最後までヒヤヒヤしていました。
今年の #isucon 予選でisupam触らなくてもスコア伸びたのが、本戦でraactのサーバーサイドレンダリングを真面目に対策しないとスコア伸びない事のフェイントになっていて、見事にハメられた。
— INADA Naoki (@methane) 2016年10月22日
今年の優勝は出題者チームだと思う。
ということで、 nginx や Go でやったことはメンバーの二人が別に記事を書いてくれると思うので、 私の目線で考えたこと、自分でやったことや方針を決めて指示したこと、その考察を書いていきたいと思います。
- インフラ担当の @kizkoh の記事: ISUCON6 にインフラエンジニアとして参加して優勝した!!
- アプリ担当の @mecha_g3 の記事: ISUCON6 優勝しました
お題と初期構成について
お題は、リアルタイムに他人の書いた線が他のユーザーに見えるようなお絵かき掲示板でした。
構成が特徴的で、
- フロントにいるのが通常の Web サーバーではなくて、 react.js を使ったアプリを含む node.js サーバー (以下react)
- そこからリバースプロキシされる形でアプリが存在し、それには各言語の実装がある (以下app)
- app のバックエンドとしては MySQL がいる
- react, app, MySQL がすべて docker になっていて、 docker-compose で動いている
というものでした。
ベンチマーカーは react の Web アプリを想定しているようで、 bundle.js
というファイル(react を
全く知りませんが、たぶんサーバーサイドとクライアントサイドで共通なファイル)が1バイトでも変化したら
FAIL していたので、かなり厳密にチェックしていたのだと思います。
Docker は @kizkoh が判るということでしたが、 node.js & react が全く分からないのがとにかくつらい。
「react が判らないと勝てない問題なら多言語用意する意味ないし、きっと簡単にここはボトルネックじゃなくなって、 1本線を引いたらそれを何百人に転送して何百点もバーンと入るような問題だ。それならGoは得意だし、 ネットワークで協力プレイするサーバーをGoで開発してる僕らも得意だ。実力出せば勝てる」と メンバーと自分に言い聞かせてスタートしました。
序盤戦 (~14:00 くらい)
初回ベンチを終え、 @kizkoh が nginx の準備、 @mecha_g3 がアプリ開発&チューニングの準備を
しているあいだに、フロントの react の server.jsx
というファイルと Go アプリのルーティング部分を見て、
フロントに追加する nginx のリバースプロキシ設定 (/api/
は直接 app にリバースプロキシして、それ以外は
react にリバースプロキスするなど) をまとめたり、 @mecha_g3 に room のオンメモリ化を始めるように指示したりしました。
このときはまだ react がボトルネックでなくなるという楽観的な前提だったので、まずは @mecha_g3 に線を書くところとそれを
SSEで配信するところでオンメモリ化を進めてもらいつつ、 react を docker から引きずり出したり、共有していた開発環境に
MySQL をセットアップしてインフラ再構築中でも app の動作確認ができる状況を用意してから、腹をくくって react の
server.jsx
というファイルを読み始めました。 (server.jsxのソースコード)
そうすると、下の方で renderString()
しているところは、ここからHTMLはブラウザで見たソースがどうやって生成されるのか
全くわからない上に、HTMLの方にはたぶんクライアントとサーバーの整合性を確認するためのものと思われるチェックサムがあって、
何をしたら fail するのか分からないできるだけ触りたくない部分に見えた一方、上の方にはいかにもここをチューニングしてくれ
という雰囲気で /img/:id
というパスの処理をしているコードが目に止まりました。ここもどうやってXMLができるのか全くわからない
ものの、レスポンスヘッダを設定しているところから生の svg を生成していることが一目瞭然でした。
/img/:id:
の部分は renderString()
ではなくて renderToStaticMarkup()
という関数を使っていたので、この関数のリファレンスを
探して見てみます。
https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup
Similar to renderToString, except this doesn't create extra DOM attributes such as data-reactid, that React uses internally. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save lots of bytes.
要するに react の魔術がかかってないプレーンなデータを作るための関数のようです。これは出題者からのヒントだろう。XML生成とか いかにも重そうだし、ここをキャッシュするとか 304 not modified を返せば一気に react は問題にならなくなるんだろう、という希望的予測を立てました。
なお、競技終了後にちょっとディレクトリツリーに目を通してみたら、すぐに components/
というディレクトリ配下にコンポーネント(テンプレート+コード)があって
xml を生成している Canvas というコンポーネントも判りました。このディレクトリを見ていればその後の戦略ももっと変わったかもしれません。
ちょっと検索すれば、同じディレクトリにある Room というコンポーネントで同じ Canvas コンポーネントを利用してるのが判るので、 /img/:id
だけ
高速化しても react 全体が軽くはならないだろう、 /room/:id
も対策が必要だという前提で進められていたはずです。
緊張状態では知らない技術は10倍怖いし、ベンチマーカーが何をチェックしてるかわからないのも10倍怖くて、計100倍の怖さだもんね。仕方ないね。
中盤戦 (~16:30 くらい)
react から ssl を外してフロントを nginx にするとか docker 外しとか一通り終わったので、 @kizkoh には「やりたいことが残ってるならやってていいけど、
無いならムリに何かしようと頑張るより終盤に向けて休憩取っておいて」という指示を出し、 app が返す json とそれを react が変換してブラウザに返す svg を見比べながら、
その svg を Go の内部のデータ構造から直接生成するような fmt.Printf
の塊を作ります。 (ソースコード)
@mecha_g3 のオンメモリ化が終わってからつなぎ込み、svgはあっさりベンチをパス。ただGoに持っていってもsvg生成は結構重い(想定内)ので、 @mecha_g3 にキャッシュを依頼。
その時ちょっと面白がって、 stroke が書かれたときのキャッシュの更新でひと工夫。 svg の更新は stroke の追記オンリーなので、全部再生成するのではなくて、単に追加された
stroke を最後の </svg>
タグの手前に挿入するだけにしてもらいました。スコアにどれくらい影響があったのかはわかりませんが、これが簡単にできたのはGoらしい部分でした。
とはいえ、この工夫が強かったのかと言うと、試してみないとわからないものの、svgを静的ファイルに吐き出してnginxで返すことで参照性能上げたほうが、更新性能を上げるよりスコア上がった可能性が大きいと思います。
さらに、可能性としては、react側の /room/:id
を弄ることができれば、 react から静的ファイルを読み込んで埋め込むのは簡単だったはずです。
静的ファイルを使う場合分散構成がネックになりますが、同じ部屋へのアクセスが同じサーバーに分散されるような nginx の設定は @kizkoh ならすぐにやってくれたはずです。
終盤戦
序盤に考えていたことは実現できたものの、そのときに妄想していたレベルの性能は全然出ていません。
インフラ側でも問題が色々でていましたし、なにより react のCPU使用率が支配的で app の性能を全く活かせていません。
今から react を調べてハマらずにチューニングするのはバクチ過ぎて、暫定ダントツトップの状態から挑戦するのはナシです。 ここまで1台で動かしてきたのを、MySQL を isu2 に (これは僕の指示でしたが、データセットが初期状態にもどって軽くなる以外は無意味でした…)、 react を isu3-5 で動かす分散構成に移行します。
他にも too many open files とか TIME_WAIT とかが問題になっていたので、 nofiles や tcp_tw_reuse などを設定するように指示しつつ、 react が HTTP keep-alive を頑なに拒否していたのを直していきます。
まず、 nginx -> react のリバースプロキシ部分で全く keep-alive されないのは、 express.js 部分でレスポンスヘッダーを追加するAPIを調べて res.append('Connection', 'keep-alive')
を追加したら直りました。
一方 react -> app の方が問題で、ライブラリの依存関係を潜っていった結果、 bitinn/node-fetch
の中の次のようなコードを削除するというちょっと強引な方法で解消しました。
// https://github.com/bitinn/node-fetch/blob/master/index.js#L79-L81 if (!headers.has('connection') && !options.agent) { headers.set('connection', 'close'); }
こういった修正を、再起動試験の合間を縫ってやっていたのですが、終盤はタイムリミットが近づくにつれ何もしなくてもどんどんスコアが下がる状況に陥っていて、 ベストスコアの 85k 点は最後の修正が入ってない状態でした。
考察
終盤にスコアが伸び悩んだ(むしろ落ちた)のが帯域がネックだと予想している点について補足しておきます。 今回使ったインスタンスは2コアの D2v2 で、ベンチマーカーがアクセスするインスタンスは1台のみ、次のグラフは終盤のベンチにおけるその1台の帯域 (bytes/sec) です。
2コアのインスタンスで100MB/secを超えてるのはすごいですね。グラフがガタガタなのは、多分ベンチマーカーが波状攻撃をしかけてきたからだと思います。
次は、各パスへのアクセス数です。集計しているのはベンチマーカーからのアクセスのみで、 react -> app へのアクセスが含まれていません。(これも重要な情報なので、 直接アクセスさせていたのは僕の判断ミスでした)
Request by count 11106 GET /img/* 178 GET / 112 GET /bundle.js 112 GET /css/rc-color-picker.css 112 GET /css/sanitize.css 49 GET /rooms/* 46 POST /api/strokes/rooms/* 41 GET /api/rooms 20 GET /api/stream/rooms/* 3 POST /api/rooms 2 GET //admin/config.php 1 GET /api/rooms/1088
次は、同じくベンチマーカーに返していたレスポンスの、各パスごとの合計/平均バイト数です。
Request by out bytes 2511686449 14110598 GET / 272792736 6653481 GET /api/rooms 266967906 24038 GET /img/* 36283072 323956 GET /bundle.js 9725553 198480 GET /rooms/* 764377 38218 GET /api/stream/rooms/* 340144 3037 GET /css/sanitize.css 303520 2710 GET /css/rc-color-picker.css 184982 4021 POST /api/strokes/rooms/* 75910 75910 GET /api/rooms/1088 1017 339 POST /api/rooms 490 245 GET //admin/config.php
一番帯域を使っている / ですが、これは react から外せていないパスで、この大きいレスポンスが react (別サーバー) -> nginx -> ベンチマーカーに流れているので、 nginx を置いてるマシンの in と out の帯域が同じくらいになっています。
2番めの /api/rooms
ですが、これは直接クライアントから呼ばれるのが41回、(多分)/rooms/*
を処理している react からアクセスされるのが他に 49 回あるので、
実際に使っている帯域はこの倍以上あったはずで、ここが帯域に引きずられてタイムアウトしやすくなったのがスコアが安定しなかった理由だと思います。
まだ見れてないですが、 /
に帯域を減らせるような仕込みがあったのかもしれません。また、それを攻略できなかったとしても、 nginx で全体に ratelimit をかけて
帯域を食うパスを遅くする代わりにそれ以外のパスでタイムアウトになる数を減らすことで、もっとクライアントの並列度を稼いでスコアを取れたかもしれません。
せっかくこれだけの情報を取れていながら、終盤はテンパっていて全然考察できていませんでした。再起動試験中に冷静に最後のもうひと稼ぎを考えないといけませんね。
感想
例年は自分が一番アプリを書ける、かつインフラのチューニングも一番判るという状態で、新卒メンバー2人にタスクを振るという戦い方をしていました。
今年は、 @mecha_g3 を自分と同じくらいアプリが書けると信頼して大まかな方針だけ共有するだけで済んだし、 @kizkoh も僕が全然キャッチアップできていない 近年のミドルウェアを予習してきてくれておまかせできた上に、アプリチューニングが入る前からインフラだけでスコアを伸ばしてくれたおかげで、 手を動かさずに目と頭を使う戦い方ができました。
まだまだ僕の力不足で出題側の想定解法にはたどり着けませんでしたが、3人で強いチームがISUCONで強いチームだということを強く実感でき、 スポコンマンガの登場人物になったかのような快感・達成感を得ることができました。 このチームであと10回くらい勝って賞金で新車のWRXを買いたいです。
終わりになりますが、運営・協賛の各位、特に毎年増え続ける参加者をあたたかみのある運用で運営してくださっている櫛井さんと、 毎年上がり続ける出題レベルのプレッシャーに見事に応える良問と安定ベンチマークシステムを作ってくださった出題チームに感謝いたします。