チェアー!
ISUCON14 に @ikura-hamu、 @pikachu、 @H1rono_K の 3 人で「リアクティブ二子玉川~♪」として参加し、最終スコア 23936 点で32位でした。
リポジトリ: https://github.com/reactive-futakotamagawa/isucon14
改善記録
↓の時間の所のリンクから該当コミットを見ることができます。
10:24 pproteinの導入
pprotein を導入しました。サーバーにプログラムにimportしたのは本家のものですが、見る用にデプロイしてたのは フォークしたもの( reactive-futakotamagawa/pprotein )です。
ちなみに、ベンチマーカーのバグが起きていて競技開始直後は点数が出ませんでした。
10:27 (673) ログの設定
MySQL と Nginx のログなどの設定を入れました。
10:45 (893) INDEX追加
chairs
テーブルにaccess_token
でインデックスを貼りました。
11:12 (2919) 全INDEX貼り
スロークエリ上位が全然インデックス貼られてなくて、上位から順にインデックスを貼りました。
ALTER TABLE `ride_statuses` ADD INDEX `idx_ride_statuses_ride_id_created_at` (`ride_id`, `created_at` DESC);
ALTER TABLE `chair_locations` ADD INDEX `idx_chair_locations_chair_id_created_at` (`chair_id`, `created_at`);
ALTER TABLE `chairs` ADD INDEX `idx_chairs_owner_id` (`owner_id`);
ALTER TABLE `rides` ADD INDEX `idx_rides_chair_id_updated_at` (`chair_id`, `updated_at` DESC);
ALTER TABLE `rides` ADD INDEX `idx_rides_user_id_created_at` (`user_id`, `created_at` DESC);
11:44 (3483) getChairStats
のN+1
を解消
アクセスログ上位かつpprof
でも上位だったため改善しました。
13:46 (6870) DBを分離し2台構成
マッチャーが止まってない事に気づかなくて2時間苦戦しました。なぜかベンチマークが落ちる事態が多発したので、いろんなパラメーターを変えたり試行錯誤したりしましたが、dbInitialize
を受け取って5秒後にアプリを終了することで解決しました。(pproteinでログが取れなくなったので、後で環境変数を用いた方式に直しました。)
14:26 (7309) パラメーター調整
環境変数のISUCON_MATCHING_INTERVAL
を1.0
に変更しました。 (ベンチマークが良く落ちていたので、いろいろ試行錯誤して2.5
に上げていましたが、1.0
に下げました。数字が高いほどベンチマーカーが落ちる可能性が低かったです。)
16:14 (7132) 総移動距離取得の長いクエリを複数クエリに分割
椅子の総移動距離を取得する長いクエリを複数のクエリに分割しました。
当初は新しいテーブルに総移動距離を入れる実装で進めましたが、整合性チェックが通らず、テーブルはそのままで実装することになりました。
16:21 (7568) interpolateParams=true
MySQLの接続オプションで interpolateParams=true
にしました。
16:50 (7938) chairs
をaccess_token
でキャッシュ
chairへのリクエストで使われるmiddlewareを軽くするため、chairs
で access_token
に対するキャッシュを入れました。
16:26 (8040) DBをテーブルで分割し2台に、3台構成に
DBのCPU使用率が高かったため、DBをテーブルで分割して2台にしました。
まずは簡単にできそうなusers
とcoupons
で試験的に実装し、成功しました。
- s1 App
- s2 DB (
users
,coupons
) - s3 DB (他のテーブル)
17:03 (8633) 余裕があるDBにテーブルを追加
s3が相変わらず燃えていたので、s2とs3のCPU使用率が均等になるように、スロークエリの上位と実装しやすさを見てchairs
とowners
テーブルをs2に分離しました。
- s1 App
- s2 DB (
users
,coupons
,chairs
,owners
) - s3 DB (他のテーブル)
17:18 (16855) ISUCON_MATCHING_INTERVAL
を0.02
に変更
CPUが余り始めたので何かパラメーター調整することでリクエストを増やせないかと考え、サーバー2台構成にしてfailしまくり試行錯誤の時に1回だけ9000点failが出ていた事を思い出して、ISUCON_MATCHING_INTERVAL
を調整してみると一気にスコアが上がりました。
17:35 (17342) 通知のポーリング間隔を100ms
に伸ばした
RetryAfterMs
を100ms
にして、通知のポーリング間隔を伸ばしました。
17:44 (19840) 余裕があるDBにさらにテーブルを追加
s2にまだCPUの余裕があったので、スロークエリ2位のchair_locations
テーブルをs2に移行した。
- s1 App
- s2 DB (
users
,coupons
,chairs
,owners
,chair_locations
) - s3 DB (他のテーブル)
これで、s1s2s3それぞれCPU90%以上を使う構成になりリソースを最大限活かせていました。
17:48 (23500) ログを切る
MySQL のスローログや Nginx のアクセスログ、アプリのログなどを切り、計測機器を止めました。
もう一回ベンチマークを回してフィニッシュ!
最終盤に H1rono_K が長い間取り組んでいた ride_status
のキャッシュが通り、 main ブランチに入れようとしましたがコンフリクトが多く発生し、最終的に断念しました。悲しい。
最終スコアは 23936 でした。
isu-isu-h について
https://github.com/reactive-futakotamagawa/isu-isu-h-14
(Private リポジトリのコピー)
isu-isu-h(いすいすえいち)は、去年の ISUCON13 に向けて ikura-hamu が開発したツール群です。詳しくはこちらの記事をご覧ください。
今年度はこれを改良して使っています。主な変更点は以下の通りです。
- ansible が読みやすく・書きやすくなった
- 去年までべた書きのplaybookだったものが、roleに分けられ拡張しやすくなっています。
- Go の PGO を使えるようにした。
- pprotein の API から pprotein のプロファイル結果が得られるので、それを用いて PGO (Profile-guided optimization) を使えるようにしました。
- ビルドに時間がかかるのでデフォルトではオフにしていたのですが、当日そのことを完全に忘れていて使えませんでした。悲しい。
- node_exporter から alloy になった
- CPU やメモリの使用量などのメトリクスを取得するために使っていた node_exporter を Grafana の alloy に変えました。
- journal のログも alloy から送信するようにしました。
- 当日デプロイしたところ、元から入っていた何かとポート番号が衝突したらしく、動きませんでした。悲しい。
- observer を ansible でデプロイできるようになった
- Grafana などのデプロイが大変だったので、それらをデプロイするための ansible を用意しました。
- isu-isu-h が無いと ISUCON 練習できない体になっていたのに、そのデプロイが面倒すぎたので、これは大きな進歩だと思います
- Go の 静的解析ツールができた
- 最も大きな変化として、2つの静的解析ツールの開発があります。
intro-h
(いんとろえいち) は、初動で pprotein をソースコードに埋め込むためのツールです。ただ、作りが甘くて echo しか対応しなかったので、webフレームワークが chi だった今回は微妙に不発でした。n1-h
(えぬいちえいち) は、その名の通り N+1 っぽい場所を見つけてくれるツールです。go vet
での実行と golangci-lint の Module plugin としての実行に対応しています。今回の問題にはあまりありませんでしたが、関数の中でのDB呼び出しなどで深くなってるN+1にも対応しているので、素振りでは役立ちました。- golangci-lint の Module plugin の作り方・使い方については別のブログで書こうと思います。
- どちらも名前に h を付けていますが、isu-isu-h の h は ssh である一方で、この2つの h は何となくです。
VSCode で動く n1-h の様子
今後も開発を続けてより便利にしていきたいです。
感想
ikura-hamu
ものすごく悔しいです。去年は「初出場の割には良かったかな」という気分でしたが、今年は自分の実力不足と成長不足を突き付けられました。とにかく手が遅いし正しいコードを書けない。
来年はもっと力を付けて帰ってきたいです。
pikachu
自分ができることはかなりできたかなと思います。スコアにめっちゃ貢献出来て良かった!
「全サーバーのリソースを使い切る」が個人的な目標だったので、最終的に全部CPU90%以上使う良い構成にできて大満足です!
同サークルの他のチームはDBサーバーの分割でかなり悪戦苦闘していたようで、僕も2時間格闘することになりましたが、原因が分かった上でDBのテーブルごとの分割までちゃんと実現できて本当に良かった......!
結構ベンチマークの謎のエラー落ちに悩まされていた時間が長くて、焦りと共にスコアが確実に上がる改善に集中しました。もっと時間的余裕があれば、もっとN+1を直しまくったりキャッシュしまくったりできたかなと思います。
次回は、もっとすべてを解決する圧倒的マッスルパワーをつけて、圧倒的優勝をもぎ取ってやろうと思います!
H1rono_K
paymentGatewayURL
をメモリに載せました。貢献と言っていい貢献はこれぐらいですね。ride_statuses
のキャッシュに取り組んでたんですが、最後まで上手くいかなかった... Go言語には罠が多いですね。Rust感覚でクロージャの置き換えを書いたら無限再帰しました。
callback := func(ctx context.Context) error { ... }
callback2 := func(ctx context.Context) error { ... }
callback = func(ctx context.Context) error {
if err := callback(ctx); err != nil {
// ^^^^^^^^
return err
}
return callback2(ctx)
}
とにかく筋肉が足りなかったなと思います。ガシガシと手続きを書く筋肉、将来のことなど考えずにif文を追加する筋肉、などなど。Rust書かせてくれ!!!!!!