ISUCON12予選にチーム Takedashi として参加し、予選通過しました (52957点)
2022-07-23のISUCON12予選にチーム Takedashiとして参加して、52957点で予選通過しました。 予選通過はISUCON8以来だと思います。その間に子供が二人産まれており、時間の流れを感じますね。
リポジトリはこちら github.com
メンバーの @chibieggも記事を書いているのでこちらもどうぞ http://blog.chibiegg.net/2022/07/24_15_864.htm
メンバーはいつもの
アイコン | メンバー | 役割 |
---|---|---|
chibiegg | インフラ/デプロイ/プルリク管理/データ整備/コーディング | |
__math | 司令塔/コードリーディング/コーディング | |
misodengaku | コードリーディング/ミドルウェアチューニング/コーディング |
となっています。今回は絵に描いたように上手に物事を進められたのがよかったです、100点満点!
スコア推移:
司令塔の動き方
他の2人と話し合ったわけではないのですが、今年は勝手に自分の動き方を変えていました。スクラム開発?
リスクマネジメントを最も重要視しました 具体的には以下の3つの項目を頭の中で比較して、改善内容をチームの人にお願いしていました。
- リスク : 実装の難易度、手戻りが発生する確率、実装が失敗した時のrevertは容易か
- 費用 : 実装からベンチマーク実行終了の合計時間。 実装が20分、デプロイ1分、ベンチマークが2分なら23分
- 効果 : 実装後のベンチスコアの上昇割合。 1000 -> 1500 なら 1.5倍
これらをベースに優先度を付けます。 リスク、費用は個人の特性や実装内容で変わるので難しいですね。
我々のチームは何回か本選に出場したことがあるので、スコアの安定感を高めることが出来れば予選突破の安定するだろうと思いリスクを取りすぎないようにしました。 長年固定メンバーで色々な大会に出ていると、各々の得意分野、実装時間の見積もりの精度が上がって良いですね。毎年違うメンバーで出る場合はこのあたりの戦略が変わるだろうなと思います。
改善点の見つけ方
ボトルネック以外は目をつむる、これに尽きます。 優先順位は
- 各種メトリクスを見る
- アプリケーションで遊んで仕様を一通り確認する
- 規約とかを読む
- コードを読む
です。
最序盤はメトリクスが出揃ってないので、その間に規約やコードを読んだりしておくといいかと思います。コードを読むのが一番優先度が低い理由ですが、自分はほっといたらコード読んでるのでわざと優先度を下げてます。
実装方針の伝え方
序盤は改善点が大量にあるので、見つけた改善点、気づきをGoogle Docにまとめてチームの人に見せていました。 中盤後半は手が打てる数点しかないのと、slack + 脳内だけで整理しきれるのでdocには残してません オフラインならホワイトボードでいいんですけどね…
本番時系列
9:00 本番前
vscodeとgolangをubuntuに入れたり、公開鍵がちゃんとgithubに登録されてるか確認したり…
10:00 競技開始
- @chibiegg : CloudFormationでプロビジョニング開始
- @misodengaku, __math: 規約とかを読んでる
10:10 初回ベンチマーク
- インスタンスが立ったのでとりあえずベンチを走らせる
- @chibiegg: コードをprivate repoに上げる
- @chibieggと@misodengakuにpprofとかnginxのログ、mysqlのスローログとかを取れるようにお願いする
10:?? アプリを試す、コードを眺める
- MySQLとSQLiteが併用されていることに気づく。各tenant毎にsqliteのdbファイルが切られているが、各クエリに
tenantId = ?
という文字があるので、マージできそうだなと漠然と考えていた。 とりあえず MySQLをisuportsと別サーバに移動
dispenseID
という、"admin" MySQLに依存したID発行関数があり、uuidに変えようか迷ったものの、まだ読んでない箇所もあるのでコードリーディングを続行。
11:00 nginxのログからボトルネックの確認
^/api/player/competition/[id]/ranking ^/api/player/player/[^/]+$
の2つのエンドポイントが遅いことがわかる。visit_history
以外のMySQLの負荷は低いので、go+SQLiteのisuportsのIO waitかCPUだろうとあたりを付けて、この辺のコードを読み込む。
* sqliteのテーブル構造と、特にplayer_score
tableの使われ方を把握する。
11:00 visit_historyの改善
- @chibiegg にとりあえず visit_history のindexを貼ってもらう。MySQLの負荷が大幅に下がり、go + sqliteの負荷がボトルネックになることを確認。
現在2台構成 (go+sqlite, mysql)で、go+sqliteのCPUが張り付いていたので、
dispenseID
はとりあえず放置決定に。
11:xx player_score
の改善
player_score
のレコード量が減らせることに気づいたものの、ここで間違ってたら手戻りがひどいため他の人と理解が正しいかダブルチェックしてもらう。- 各(
tenantId, competitionId
)毎に、同じプレイヤーのレコードは1つまでしか必要ないことがわかったので、2つの事をしたくなった- SQLiteのデータ量を減らす
- 初期データをいじるので少しリスクが大きめ
competitionScoreHandler
でinsertするデータ量を減らす- 間違っていてもinitializeすれば容易にデータが戻せる
- SQLiteのデータ量を減らす
ということでまず後者を実装してもらう : github.com
ベンチが通ったので、前者の処理をしても大丈夫そうだと確証が取れる。
11:xx SQLite -> MySQL
作業は2段階に分けた。
分けた理由: * "merge"が完了するだけでコードの見通しが良くなり、複数テナントにまたがるSQLの実行回数が1回になる。IOもCPU使用率も減りそう * "merge"をするだけでいちいちconnectionをopen, closeしているコードが全部消える。現状のままでconnectionを貼り続けるコードにするのはバグりやすそう * "migration"に失敗しても手戻りがほとんど発生しないのでリスクが低い
ふたりとも忙しそうだし、__mathが一部担当。 chibieggの手が空いたら交代してもらうことに。
12:10 細々とした改善
competitionScoreHandler
はBulk Insertに出来る。実装は軽いしバグりにくそう、将来ボトルネックになりそう、ということで @chibiegg にやってもらう。 github.com- @misodengakuに引き続きpprof周りをいい感じにやってもらう。
- sqliteのmergeを行っている。dumpは一瞬だったがdump後1つのDBに流しこむのに時間がかかる。
- 1.dbをコピーして、それに2~99.dbの中身を詰め込めばいいのでは?と思ったものの、それよりはデータ量を減らす方がいいだろうなと思い放置。
12:30 SQLiteにindex
12:40 1つのSQLiteファイルにマージ後を見越した改善
- pprofを見ると基本的に
sqlx.GetConnect
が時間を食っている事がわかる。メインの方針は変わらず。-
GetConnect
はscanAny
等の別関数を呼び出していて、そこに時間がかかっている。名前通りconnectionを貼るのに時間がかかっているわけではないっぽい
-
- @__math: tenantDBを1つにmergeした前提のコードを書き始める github.com
13:38 visit_history
の削減
visit_history
の不要なレコードを生成しないように。また、不要なレコードの削除も- 未だにsqliteのmergeは終わらず。
player_score
のデータを大幅に減らせば移行が一瞬で終わりそうという話を伝えて @chibieggに丸投げする。 github.com
13:49 sqlite内のデータの削減
14:03 MySQLへ移行
- レコード数が減ったので、 sqlite -> MySQL の移行も一瞬で終わる。 github.com 現在の構成は
ベンチを回すとそれぞれのCPU使用率が 100, 200, 150% 程度。tenantDBのCPUがボトルネックである。
14:49 N+1の解消
competitionRankingHandler
のN+1を解消する。 github.com
14:58 flockをDBMSのトランザクションに (40000+)
- ファイルを使ったトランザクションを消す。これで4万点。 CPUがボトルネックだったのでは…?と思ったけど、 N+1の解消で事情が変わった可能性が? github.com
既に予選突破が見えてきているので、あまりチャレンジャーな事をしないように意識し始める。
15:30 rankingのクエリ最適化
- @__math : INSERT時にrankingを作れば
limit 100 offset ?
が消せるので、これと適切なindexを合わせて負荷軽減。 github.com - @ misodengaku :
playerHandler
のN+1を消してもらう github.com github.com
16:10 retry-after による負荷コントロール
- たまに500が返ってきて スコア-10%を喰らい始めたので
competitionsAddHandler
で 429 Retry-After を返すようにお願いする。- 負荷を見ながら429を返す実装は難しいので、10%の確率で429を返すようにした。 github.com
- @misodengaku にはmysqlの設定の調整をしてもらう
- 予選通過と予選一位のどっちがいいかという話で、守りに入りましょうという結論になる。
16:58 一部をインメモリに
- @chibiegg,@__mathで
player
やplayer_score
をキャッシュするように改造。終わってから思ったんですけど、これは守りに入ったチームの戦略なのだろうか。 50000くらい出始める。 github.com github.com github.com
17:01 INSERTの完了を待たずにOKを返す (50000+)
- 最後に @__mathが
visit_history
のINSERTを非同期に github.com
17:05 再起動テスト
- 再起動試験を始める
- @misodengakuと@chibieggがログの出力の抑制、systemdの見直し
- その後全員で再起動後のテスト、DBが立ち上がるのが遅い場合にちゃんと動くかの確認などをする。 github.com
再起動テスト中に52957点が出る。あんまり高い点数が出て85%ルールに引っかかるとつらいので、ここでベンチマークを終了する。
再起動テストも終了してお開きに。
おまけ
準備表のバグで度々1,2位を独占して何だか嬉しかった