失格でした。そんな
おまけ
アドベントカレンダー 16 日目の記事です!!(遅刻しちゃってごめんなさい)今日は @Takeno_hito がお送りします。
今回は結構真面目に ISUCON に取り組んだので、その参加記を残そうと思います。
メンバー
2年前の ISUCON 12 から traP じゃない知り合いと組んでます!(いつも traP でチーム組んでなくてごめん…)
今回は @su8ru と @kichi2004 とチームを組んで参加しました!
役割分担は、基本僕が改善して、@kichi2004 に改善の一部タスクを投げて、@su8ru にベンチマーク走行とインフラ整備を投げるという形です。だいたい僕です。
事前練習
今回は参加にあたって明確な目標を決めてました。それは「後輩と同期チームには勝つ!」です。代表と班長をしばらくやってたのですが、技術力アピールをサークル内でする機会がどこにもなかったので、ISUCON は「流石に僕もできるぞ」って主張する絶好の場です。
2年前も去年も全く事前練習をせずに挑んでいて痛い目を見てるので、今回はある程度練習をすることに。題材は ISUCON 13 を選びました。
1週間(と1週間分の AWS 代)を犠牲に丁寧に改善を続け、スコアは 138155 まで伸ばしました。本番だと TOP 10 入れるくらいのスコアなのでかなり好成績。やったー!
事前練習でやったことは、
- 計測の仕方をしっかり見る
- デプロイ・計測環境をしっかり整える
- N+1 外す
- キャッシュをする
- サーバー分け
だけ。これだけでも十分な上位スコア狙えるみたい。(DNS に触れないって決めたのもあるけど…!)
キャッシュライブラリは sc を採用。traP / 織時屋 の toki さんが作ってるキャッシュライブラリです。使いやすくて便利!
計測環境の構築は、部内の講習会資料を参考にし、ベーシックに pt-query-digest, alp, pprof だけ突っ込みました。外部の人もちょくちょく参考にしてるみたいで、結構良い資料なのでぜひみてください👀
pprotein を使わなかったのは、真面目に取り組んだの今年が初めてで使いこなせるかわからず心配だったからです。来年は使います…。
当日
ということで当日です。
にゃーんチームのスコアログ
10:00~11:00 / 800 → 2800
@su8ru にインフラのセットアップを全部お任せ
その間に @Takeno_hito @kichi2004 2人でレギュレーションとか色々を読む
初回ベンチマークを回す。→ 800
その後、 インデックスを貼って、2800。slow-query log 見ると chair の総走行距離の計算がめちゃくちゃ重そうなので、 @kichi2004 に chair の distance 距離の前計算をお願いする。
11:00~12:00 / 2800 → 3300
notification の retryAfter を 30ms から 100ms にしてみたんだけど、スコアが下がってしまったので切り戻し。 getChairStats
の N+1 を外して、index を更に追加で貼ったら 3300 まで上がった。
12:00~13:00 / 3300→3300
notification が明らかに重いので、せめて ride の status をキャッシュしたいなと思って色々取り組むけど、ここからかなり泥沼にハマっていきます。未送信の status はキャッシュできたんだけど、ride の latest_status はキャッシュしようとしてもなぜか不整合が発生する……。
最終的には 3 時間くらいをこれに溶かしてるので反省です。
噂によるとマッチングの internal API が transaction 貼ってなくて(初期実装だと transaction がなくても問題ないらしい)ここから壊れてる可能性があるとかないとか……。まじか……。
13:00~14:00 / 3300→3300
更に1時間格闘し、最終的に latest_status のキャッシュは諦めた一方、未送信の status はキャッシュがうまく効いたのでこれだけマージ。スコア変わらず…。
14:00~15:00 / 3300→7000
@kichi2004 の distance 前計算ができたので、ちょっと自分で修正も加えてマージ。 @su8ru がふと retryAfter を 500ms まで伸ばしたらスコアが一気に伸びた。それなら 11 時台に伸ばしたときにも上がってよ〜〜 → 5000
pprof 見てみたら なんか chairAuthMiddleware が重そうだったので、Middleware の chair 取得部分を cache に乗せる。 → 7000
15:00~16:00 7000→15000
サーバーを 2 台に。1台目に Application + nginx, 2台目に DB の構成。2台目の方 slow-query-log ついてなかったからなのもあって一気に 15000 まで。
ここで matching interval の間隔を伸ばしてみたけどスコア全然変わらない。(本当はむしろ間隔を短くしないといけなかった…)
ここで @kichi2004 がマッチングの問題に気づいたので取り掛かり始める。
16:00~17:00 15000→15000
notification のキャッシュに更に時間を溶かす。
ぼく、16:30 にマッチングの問題に気づく。こんなんじゃスコア上がる訳ないじゃん……!!!しかし時すでに遅し……。
17:00~18:00 15000→17000
マッチングを少しでも改良しようとしてたけど時間が足りず、最後の片付けに。
nginx とアプリケーションを一応 2 台に分けたけど、まぁスコアが上がらず。nginx のログを切って 17000 まで。
再起動チェックなどをしてたところ、envcheck で落ちることが判明。(セキュリティルールがちょっとズレていた)すばるに急いで直してもらう。
ここで envcheck が取ったことを確認したはず…?僕は1台は治ってるのを確認したけど、3台全部は見てない……。すばるは全部見ていたらしい
結果
最後は環境チェック NG となってしまった。悲しい。
スコアは fail 前で traP 内 4位。同期の「リアクティブ二子玉川〜♪」に負けたので悔しい!!
↑チームの改善ログを照らし合わせてたけど、差がついたのは DB を無理やり 2分割できたかどうかでした。どうやって分けたの?って聞いたらめっちゃ力技って言われて送られてきたコードがこれ。
func BeginMultiTx(db1, db2 *sqlx.DB) (*MultiDBTx, error) {
tx1, err := db1.Beginx()
if err != nil {
return nil, fmt.Errorf("failed to begin transaction for db1: %w", err)
}
tx2, err := db2.Beginx()
if err != nil {
_ = tx1.Rollback() // tx2のエラー時にtx1をロールバック
return nil, fmt.Errorf("failed to begin transaction for db2: %w", err)
}
return &MultiDBTx{tx1: tx1, tx2: tx2}, nil
}
func (m *MultiDBTx) Commit() error {
// 省略
}
func (m *MultiDBTx) Rollback() error {
// 省略
}
力技すぎるw でもこれでスコア上げてるので、スコア上げてる方が偉いです。負けました。
反省
反省点はたくさん!
- ベンチマーカーのログを見る!(ベンチマーカー担当を完全に分けた結果、僕がベンチマーカーのログを見ることがなかった。マッチングに問題があることに僕が気付けなかった)
- 一回実環境を触る!(アプリケーションの理解度を上げたほうがスコアに何が影響してるか推測しやすい!・デバッグ用)
- DBの分割をできるようにする!
- notification / DNS水攻め みたいな、スコアに関係ない負荷に適切に対処できるようにする
- 再起動試験とか envcheck の後片付けはもっと丁寧にやる!
という感じでした。ISUCON 楽しかった!来年は TOP 30 入りたい!