ISUCON7予選2日目「Railsへの執着はもはや煩悩」で予選通過した

ISUCON7予選に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、217,457点で予選通過だったようです。 正確な値は覚えてませんが、Best Scoreは25万くらいでした。

f:id:k0kubun:20171022224227p:plain

最終形の構成概要

  • appサーバ1
    • puma 16スレッド: 画像のアップロード/表示、雑多なリクエスト対応
    • puma 2スレッド: GET /fetch だけ返す
  • appサーバ2
    • puma 16スレッド: 雑多なリクエスト対応 (画像はnginxがサーバ1に流す)
    • puma 2スレッド: GET /fetch だけ返す
  • DBサーバ

サーバ1, サーバ2をベンチマーク対象にしていました。この構成なのは GET /fetch がスコアにカウントされないため、それ以外にほとんどの時間を使えるようにするためでした。

やったこ

最終コード・他のメンバーのブログはこちら:

github.com

ISUCON7「Railsへの執着はもはや煩悩(ry」で予選通過した - 明日から本気だす

学生のころから何度も同じメンバーで出ているので、いつも通りチーム内で割と綺麗に並列に仕事ができました。

やったことと点数の推移の記録とかはやってないので、やったことだけを適当に列挙していきます*1。グラフからわかる通り多分序盤で意味のある改善が終了しており、終盤はあまりうまくいっていませんでした。

効果があった気がする奴を太字にしておく。

やった人 やったこ
cnosuke 公開鍵の準備とか
rkmathi リポジトリへの主要なコードの追加とか
k0kubun pythonruby変更、systemdの設定のリポジトリへの追加
k0kubun NewRelicのアカウント管理、インストール。この時点では GET /fetch が支配的だったのを確認
k0kubun sleepを消したり消さなかったりした後消す
k0kubun 適当にSELECTするカラムを消したり、messageテーブルにインデックスを貼ったり
k0kubun rack-lineprofを眺めるがほとんど参考にならなかった
cnosuke DBに入ってる画像をファイルにしてnginxから配信できるように変更 (ここで割と上位に来た)、多分Cache-Controlとかもこの辺でついてる
k0kubun GET /fetch を捌くpumaのプロセス (1スレッド)をまだ何もいない2つ目のサーバーに用意し、それ以外を元々いたpuma (16スレッド) に捌かせるようにした (これも結構上がり、後々もインパクトがあった)
rkmathi fetch用pumaのポートを別のサーバーからアクセスできるようにした
k0kubun (本来はruby実装のclose漏れによる)fdの枯渇をOSのfdを増やしたり nginxのworker_rlimit_nofile をいじったりしてどうにかしようとあがくがうまくいかない
cnosuke 2つ目のサーバーもfetchではないメインのpumaがレスポンスを返せるようにnginxを設定、画像を返すのを1つ目のサーバーに絞ったりとか
k0kubun close漏れに気付いて直す、多分workloadが上がってスコアが伸びる
rkmathi get_channel_list_info のループの余計な処理削り
k0kubun なんかsleepを0.2とかいれてみるがスコアが下がるのでなくす
rkmathi NewRelicで計測
cnosuke MySQLの設定がうまく反映されてない奴とかの対応
k0kubun GET /message のN+1つぶし、N=0で壊れるので直したり
k0kubun 自分のプロフィールの時に不要なクエリを減らす、そこのSELECT *のカラムも絞る。NewRelicのブレークダウンがなんかいつもと違って全く詳細になってなくてあまりインパクトのない変更を繰り返している
cnosuke MySQLがいるサーバーのメモリが使われるように設定を修正したり
rkmathi GET /history/:channel_id のN+1つぶし
rkmathi NewRelic見たり
k0kubun さっき自分がN+1で踏んだrkmathiのコードのバグとり
k0kubun GET /fetchのつぶしやすそうなN+1を1つつぶす
k0kubun fetchするpumaのスレッド数を適当にいじり2つにおちつく
k0kubun NewRelicのthread profilerを使うが、今回はびっくりするほど出力が見辛くはっきりいってほとんど役に立たなかった
rkmathi ランダムにする必要のないsaltを固定化
k0kubun Mysql2::Clientをリクエストごとに作るのをやめ、スレッドローカルに使いまわすようにする
cnosuke この辺でk0kubunがperf topを眺めてて、appサーバーがrubyよりlibzが支配的だったので、nginxのgzip_comp_levelをいじったりしている
k0kubun perfを見る限りではrubyボトルネックではなさそうだったが、ERB回り遅いのではと言われたので僕がERBを速くしたruby 2.5に変更(特にスコアに変化はない)
rkmathi GET /register が静的なのでnginxだけで返すように変更
cnosuke DBサーバーにもnginxをたて、そこからプロキシだけすることでglobalな帯域を有効に使えないか試したが、うまくいかなかった
k0kubun TD社内で書いた秘伝のstackprofミドルウェアを取り出し、何が遅いのか計測。rack-lineprofより今回はこっちの方が猛烈に見易かった。fetchでcounter cacheが効きそうなことに気付くが時間の都合でやらない
cnosuke なんかlibzまわりの関係でgzをいろいろやってる
cnosuke 画像を受けてるサーバーは1つだけだが、2つ目の方も最初からある画像は返せないか試していたが、うまくいかない
rkmathi 再起動テスト
rkmathi ログの出力を消したり
k0kubun rkmathiの変更で/registerがoctet-streamになってて動作確認がしにくかったので text/htmlにしてる
cnosuke bigintをintにしてた(のを↑のどこかでやってた)都合で壊れてるのがあって、initializeでauto incrementをリセットする変更をいれてる
k0kubun 余計なcreated_at, updated_atを作らないようにした

あとどこかでtextになってるカラムをvarcharにしたりとかしてました。

心残り

appサーバ1, 2の帯域両方をどうにかして画像のリクエスト処理に使えるようにしたかった(パスのどこかが奇数か偶数かでどちらかに固定で送るとか)ですが、いろいろ考えたけど僕はあまりいい対応が思いつかなかったです。最後 POST /message がボトルネックだったのも、MyISAMにしたりMEMORYにしたりしましたが効果はなかったですね。Redisにするみたいな大きい工事はできませんでした。

GET /fetchのレスポンスは更新があるまで止めておくのが良い(ベンチマーカーがそういう風にスコアをつけてる)、というのを感想部屋で聞いたけど、これも全く気付きませんでした。

あと、奥の手としてJITを考えてましたが全くRubyボトルネックが移せなかったのも残念ですね。

気持ち

上位のチームには相当離されていたので、どこに自分たちが見過していたブレークスルーがあったのか知るのが楽しみです。 本戦に出られるのは学生枠で出してもらっていた時ぶりですが、いい結果を残せるようがんばります。

*1:僕以外の人のタスクは僕が把握してるレベルしか書けないので多分かなり抜けててます