ISUCON7本戦に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、4位でした。
本戦の概要
予選より参加者は少ないと思うので軽く解説しておくと、クッキークリッカーを模したトラフィックのほとんどがWebSocketのアプリで、1万桁とかのスコアを計算する都合ほとんどのチームのボトルネックが最後までBigintの演算になるような問題でした。
僕らは15:00くらいに1位になり、その後はスコアをそれほど改善できず終わってしまいました。
方針
最近Ruby向けのJITコンパイラを開発している ので、それを使ってバーンとやろうと思ってましたが、これは開始前にgfxさんにバラされたのでやめました。
というのは嘘ですが *1 、WebSocketと聞いた時点で並列度が必要になるんじゃないかなあと思ってた *2 のと、アプリのCPU演算がボトルネックになってた *3 ので普通に使って速い言語としてGoを使うのを最初から視野にいれていました。
一方で、チーム名からもわかるように我々はRubyに最もなじみがあり計測のノウハウがあるのと、また普通にRubyのコードはGoのコードに比べ文字数が少なく見通しが良かったので、問題を把握するまでの期間Rubyで参戦し、後にGoに切り替えました。
最終形の構成概要
僕は開始2時間くらい寝起きでぽよってたので @cnosuke や @rkmathi が最初に考えた内容ですが、大体以下の感じになりました。 問題の性質上、WebSocket以外を受けるノード以外の構成はどこも同じだったと思います。
- appサーバ1 (ベンチマーク時この1台のみ指定)
- appサーバ2
- WebSocketを返すGo
- 同ホストのGo向けMySQL
- appサーバ3
- WebSocketを返すGo
- 同ホストのGo向けMySQL
- appサーバ4
- WebSocketを返すGo
- 同ホストのGo向けMySQL
やったこと
他のメンバーのブログは公開されたらここに貼ります。 点数は残してある部分だけ。
僕は業務で一番Rubyを触っている時間が長そうで、またGoもちょっと前はよく書いてたので、主にアプリのコードをいじる担当でした。逆にインフラが絡む部分は他の人に任せています。
点 | やった人 | やったこと |
---|---|---|
7112 | rkmathi | Python初期スコア計測 |
5980 | rkmathi | Rubyに変更、初期スコア |
4621 | rkmathi | Goも一応確認、初期スコア (その後Rubyに戻す) |
- | cnosuke | サーバーやリポジトリのセットアップ |
- | k0kubun | NewRelic をいれたが、99% WebSocketであるということだけがわかり、相性が微妙だったので今回は使用をやめた |
- | k0kubun | 予選でも使用したStackProfミドルウェアを入れた。 calcStatus がボトルネックであることがわかる。 今回もRubyを使っている間結構役に立ったが、StackProf周りは別のところで解説しようと思っているので今回は説明を省略 |
- | k0kubun | m_item テーブルを定数としてメモリに持つようにし、m_item のクエリを全てなくす。あまりスコアは変わらなかった |
- | k0kubun | get_power, get_price のBigintの計算がボトルネックになっていたので、これをcountが0〜50の場合の結果をアプリ起動時に作るようにした。 そこそこスコアが上がった気がする |
8968 | cnosuke | 採番して 2,3,4 にWebSocketのリクエストを分散 |
- | rkmathi | 2,3,4に個別のMySQL用意 |
- | rkmathi | 再起動耐性のための設定 |
- | k0kubun | 50個のキャッシュを500個にしてみたら全然起動しなくなった。この時点でも150までのカウントを使っていたが、150とかにしてもメモリの使用量がモリモリ増えて速攻で2GB使い切ってしまったので、Pumaのプロセスを減らすかとか考えていた(preload_app も試すべきだった)が、GILあるしなあとかまあそういうことを考えるのが面倒なので僕はここでRubyをやめることにした。 |
- | rkmathi | innodb_buffer_pool_sizeなど、MySQLの設定とか |
- | cnosuke | WebSocket以外を返しているノードへのWebSocketへの負荷減らし |
- | rkmathi | サーバーで動いているのをRubyからGoに切り替え |
- | k0kubun | Rubyにやった変更(m_item, power/priceのキャッシュ)を全てGoに移植 |
- | k0kubun | power/price のキャッシュを 50 → 80に調整。 このパラメータをいじるだけでスコアが上がることを発見するが、起動時間が指数関数的に述びていくのでこのへんで断念したが、このあたりからcnosukeが起動時間を縮めるためこれのキャッシュをシリアライズするのを着手していた |
- | rkmathi | 僕らが誰もGoのプロファイリング方法を知らないのでググり始める。rkmathiが速攻でpprofの使い方を理解し結果を共有してくれたのでとても助かった。始めて使ったけどtopとlistだけでかなり多くのことがわかる。あとなんかrkmathiがコールグラフをsvgにしていたが、これも便利だった |
- | cnosuke | WebSocketだけをさばくノードでnginxをなくしてGoで直接受けるように。 今回はあまり効果はなさそうだった |
- | k0kubun | やたら 値が1000の big.Int インスタンスを生成してるっぽかったので毎回同じものを使うように (効果なし) |
24227 | k0kubun | calcStatus内で 1000 * 13 のオーダーでやっているかけ算 + 比較を、13のループの外側で割り算 + ループ内比較 にすることでそこのかけ算のコストを 1/13 に。割とスコアが上がっていた |
- | k0kubun | calcStatus内で 13回やっているかけ算を1つの割り算にできるものをもう1つ発見し、適用。 ここは1000回ループの外なのでインパクトは小さそう |
- | k0kubun | 「1000 * 13回 のかけ算」 → 「1000回 の割り算」 に減らしたオーダーを、 「13回のかけ算」に減らした。これも効果があった記憶 |
- | k0kubun | この時点でpprofを見る限りでは big.Int の String()が90% くらい使っていたし、GoがCPUをほぼ使い切っていたので、僕は後半ずっとこれをどうにかできないかもがいていた。なんかfloat64に変換してLog10で桁数計算したり(これは変換した時点で精度が破滅)、なんかpprofのsvgの読み方をちょっと間違えてString()の後のItoaが重いと勘違いして、Exponentialインスタンスを作る時に整数への変換をスキップしたりしたが、無意味であった。これ以降僕はスコアを上げられていない |
- | rkmathi | puma のワーカー数いじり |
- | rkmathi | longtext -> varchar のalter |
- | cnosuke | power/priceのキャッシュをYAMLにシリアライズしてロードする奴を入れるが、バグのためrevert |
27328 | rkmathi | 再起動テストとかプロファイリングとか。我々はプロファイラを入れるとスコアが上がるジンクスを持っており、pprofを有効にした時最高スコアを更新した |
- | cnosuke | YAMLの奴をデバッグしていて、終了間際に原因がわかるが、ベンチマークを何度も回す余裕がなさそうなので、ここで変更を加えず何度かベンチを回して終了 |
やり残したこと
気付いていたのはaddingとかが過去になった奴をまとめておけそうなくらいですが、僕らの計測の限りではそこはボトルネックではなかったので着手しませんでした。ずっとボトルネックであった big.Int.String()
や、 price/power の計算結果キャッシュ数を増やすのをがんばっていたが両方失敗してこのスコアに留まった形です。
1位のMSAは、僕らはボトルネックだと思っていなかったあたりのオンメモリ化や過去の結果のマージを全て終わらせて高いスコアを出したようなので、計測は難しいですねという感じです。
我々は気付かず懇親会で知ったのは、(具体的な内容はそちらのチームのブログに任せますが) ソン・モテメン・マサヨシ チームのgoroutineを使ったものやGCのチューニングで、同じGoのアプリをいじってるにも関わらず僕らとは全く違う方向性で速くしていたようなので、その辺はGoへの慣れの差が出たような気がします。
感想
最後のISUCON本戦参加は学生枠で惨敗という感じだったのが、3年経って同じメンバーで社会人枠で健闘でき、成長した実感が得られたのがとても良かったです。
時の運次第では優勝に手が届きそうな感覚が得られたので、来年は優勝したいと思います。参加された方と運営の皆さま、お疲れ様でした & ありがとうございました!