ISUCON7予選に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、217,457点で予選通過だったようです。 正確な値は覚えてませんが、Best Scoreは25万くらいでした。
最終形の構成概要
- appサーバ1
- puma 16スレッド: 画像のアップロード/表示、雑多なリクエスト対応
- puma 2スレッド: GET /fetch だけ返す
- appサーバ2
- puma 16スレッド: 雑多なリクエスト対応 (画像はnginxがサーバ1に長す)
- puma 2スレッド: GET /fetch だけ返す
- DBサーバ
- MySQLがいるだけ
サーバ1, サーバ2をベンチマーク対象にしていた。
やったこと
最終コードはこちら:
学生のころから何度も同じメンバーで出ているので、いつも通りチーム内で割と綺麗に並列に仕事ができました。
やったことと点数の推移の記録とかはやってないので、やったことだけを適当に列挙していきます*1。グラフからわかる通り多分序盤で意味のある改善が終了しており、終盤はあまりうまくいっていませんでした。
効果があった気がする奴を太字にしておく。
やった人 | やったこと |
---|---|
cnosuke | 公開鍵の準備とか |
rkmathi | リポジトリへの主要なコードの追加とか |
k0kubun | python→ruby変更、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を作らないようにした |
心残り
appサーバ1, 2の帯域両方をどうにかして画像のリクエスト処理に使えるようにしたかった(パスのどこかが奇数か偶数かでどちらかに固定で送るとか)ですが、いろいろ考えたけど僕はあまりいい対応が思いつかなかったです。
GET /fetchのレスポンスは更新があるまで止めておくのが良い(ベンチマーカーがそういう風にスコアをつけてる)、というのを感想部屋で聞いたけど、これも全く気付きませんでした。
あと、奥の手としてJITを考えてましたが全くRubyにボトルネックが移せなかったのも残念ですね。
気持ち
上位のチームには相当離されていたので、どこに自分たちが見過していたブレークスルーがあったのか知るのが楽しみです。 本戦に出られるのは学生枠で出してもらっていた時ぶりですが、いい結果を残せるようがんばります。
*1:僕以外の人のタスクは僕が把握してるレベルしか書けないので多分かなり抜けてて、あとで他のメンバーの記事が出たら貼ろうと思います。