ISUCON7本戦「Railsへの執着はもはや煩悩」で4位だった

ISUCON7本戦に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、4位でした。

本戦の概要

予選より参加者は少ないと思うので軽く解説しておくと、クッキークリッカーを模したトラフィックのほとんどがWebSocketのアプリで、1万桁とかのスコアを計算する都合ほとんどのチームのボトルネックが最後までBigintの演算になるような問題でした。

僕らは15:00くらいに1位になり、その後はスコアをそれほど改善できず終わってしまいました。

f:id:k0kubun:20171125222955p:plain

方針

最近Ruby向けのJITコンパイラを開発している ので、それを使ってバーンとやろうと思ってましたが、これは開始前にgfxさんにバラされたのでやめました。

というのは嘘ですが *1 、WebSocketと聞いた時点で並列度が必要になるんじゃないかなあと思ってた *2 のと、アプリのCPU演算がボトルネックになってた *3 ので普通に使って速い言語としてGoを使うのを最初から視野にいれていました。

一方で、チーム名からもわかるように我々はRubyに最もなじみがあり計測のノウハウがあるのと、また普通にRubyのコードはGoのコードに比べ文字数が少なく見通しが良かったので、問題を把握するまでの期間Rubyで参戦し、後にGoに切り替えました。

最終形の構成概要

僕は開始2時間くらい寝起きでぽよってたので @cnosuke や @rkmathi が最初に考えた内容ですが、大体以下の感じになりました。 問題の性質上、WebSocket以外を受けるノード以外の構成はどこも同じだったと思います。

  • appサーバ1 (ベンチマーク時この1台のみ指定)
    • WebSocket 以外を返す puma (Sinatra, Ruby)
      • 4つのサーバーに負荷をうまいこと分散していたようですが、僕はこの辺担当してないので他のメンバーの記事にご期待ください
      • WebSocketを担当するウェイトは他の3つが多くなるようにしていたようですが、どのサーバーもCPUは全部使い切れていたようです
    • WebSocket を返すGo
    • nginx (他はGoが直接受ける)
    • puma, Go用の同ホスト向けMySQL
  • appサーバ2
    • WebSocketを返すGo
    • 同ホストのGo向けMySQL
  • appサーバ3
    • WebSocketを返すGo
    • 同ホストのGo向けMySQL
  • appサーバ4
    • WebSocketを返すGo
    • 同ホストのGo向けMySQL

やったこと

github.com

僕は業務で一番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年経って同じメンバーで社会人枠で健闘でき、成長した実感が得られたのがとても良かったです。

時の運次第では優勝に手が届きそうな感覚が得られたので、来年は優勝したいと思います。参加された方と運営の皆さま、お疲れ様でした & ありがとうございました!

*1:単純にISUCONで使うならもうちょっと直したいところがあったが、本戦までに直していなかった

*2:これはそうでもない

*3:実際にはRubyのBignumはGoのbig.Intに比べ別に遅くないそうなのでこれも別に該当しませんでしたが、後述するようにメモリで断念しました