ISUCON 14 にてんぽらりーねーむで参加しました
まとめ
- 新卒の同期と2人で参加して、Go言語で19996点で46位でした
- 技術的な学びが多いし、ISUCONの後の酒は美味い。ISUCONは良いぞ🍻
事前準備
以前から準備しているツールと流れ通りに臨みました。
昔はNewRelicなどのAPMを入れて計測をするなど工夫を凝らしてみましたが、ISUCONでは素朴なツールの方が個人的にはやりやすいな、と思っています。
開発&デプロイ
-
/home/isucon/webapp/
以下をGitで管理する - サーバー内で直接編集して反映させる
計測
- MySQLのスロークエリの設定をする
- Nginxの設定をする
- alpとpt-query-digestをインストールする
- ベンチを流した後に、notify_slackで雑に集計する
sudo pt-query-digest /var/log/mysql/slow.log > ~/query.log && /home/isucon/notify_slack ~/query.log && sudo cp /dev/null /var/log/mysql/slow.log && sudo alp --sum -r -f /var/log/nginx/access.log > ~/alp.log && /home/isucon/notify_slack ~/alp.log && sudo cp /dev/null /var/log/nginx/access.log
戦略
- はじめにアプリケーションの仕様の理解の時間を取る
- pt-query-digestで出てきたスロークエリの上位をとりあえず潰す
- その後は、スコア計算から逆算して修正箇所を決めていく
- サーバーの冗長化は残り時間2~3hぐらいから取り組む
当日の流れ
10:00-11:00 準備
以下のようなテンプレ通りに進めました
-
バージョン確認
webサーバー: nginx/h2o DB: mysql/posgresql/mariodb dockerの有無: あり/なし
- セットアップ
-
1台目
- Gitの設定
- Nginxの設定
- MySQLのスロークエリの設定
- alpのインストール
- pt-query-digestのインストール
- 2台目...
- 3台目...
-
1台目
- ベンチを流して、初期スコアを確認する
- マニュアルを2人で確認する
- ファーストアクション決定
初期スコアは979点。
pt-query-digestの結果を確認して、以下のように取り組むことを決めました
- 片方はRANK2~6あたりの単純そうなクエリに対してIndexを貼ってみる
- もう一方はRANK1の複雑そうなクエリに対して、データ構造の変更を含めて改善してみる
# 1 0x0432925663366D80 245.1444 28.9% 76 3.2256 0.06 SELECT chairs chair_locations
# 2 0x9EB401CBD984EACF 203.1134 24.0% 6995 0.0290 0.00 SELECT ride_statuses
# 3 0xB57B4BBCC1A9703E 111.5183 13.2% 2922 0.0382 0.00 SELECT ride_statuses
# 4 0xE74FB978834E34B2 68.2517 8.1% 1858 0.0367 0.00 SELECT ride_statuses
# 5 0x7BE67E7F25C2D64A 65.3168 7.7% 381 0.1714 0.01 SELECT chair_locations
# 6 0x208963461F89E6D8 57.5538 6.8% 1560 0.0369 0.00 SELECT ride_statuses
# 7 0xF17AC39B0009258F 26.8552 3.2% 10320 0.0026 0.00 SELECT rides
# 8 0x813031B8BBC3B329 24.9049 2.9% 7058 0.0035 0.01 COMMIT
# 9 0xB13961B8248A27CF 14.4308 1.7% 10372 0.0014 0.00 SELECT chairs
# MISC 0xMISC 30.6053 3.6% 144168 0.0002 0.0 <114 ITEMS>
11:00−13:30 DBの改善 979 -> 3822
- index関連の修正
- 現在のchairの位置を管理するchair_distancesの追加
- fix: chair-distanceの追加 3512 -> 3822
-
/api/chair/coordinate
で毎回chair_distances
を更新するのは、最終的にボトルネックになりそうな気もしましたが、ゴリ押しました- chair_idでUPDATEをする限りは複数のリクエスト間でロック待ちすることはなさそうかなと思っていました
上記の修正を入れた時点で、pt-query-digestの以下の結果からボトルネックが大分解消されたと判断して、次の修正を考えることにしました。
# ==== ================== ============= ====== ====== ===== ==============
# 1 0x813031B8BBC3B329 84.8057 46.9% 34395 0.0025 0.01 COMMIT
# 2 0x99AA0165670CE848 24.5477 13.6% 245988 0.0001 0.00 ADMIN PREPARE
# 3 0x9EB401CBD984EACF 8.2569 4.6% 35679 0.0002 0.00 SELECT ride_statuses
# 4 0xB13961B8248A27CF 6.9149 3.8% 26311 0.0003 0.00 SELECT chairs
# 5 0xF17AC39B0009258F 6.1605 3.4% 26180 0.0002 0.00 SELECT rides
# 6 0x208963461F89E6D8 4.1634 2.3% 13722 0.0003 0.00 SELECT ride_statuses
# 7 0xB57B4BBCC1A9703E 3.9608 2.2% 13180 0.0003 0.00 SELECT ride_statuses
# 8 0xE74FB978834E34B2 3.6541 2.0% 13230 0.0003 0.00 SELECT ride_statuses
# 9 0x0A4C67FEB1D9D015 3.0581 1.7% 13304 0.0002 0.00 SELECT rides
# 10 0x1FCBCC661B489AD2 2.9153 1.6% 7518 0.0004 0.00 UPDATE chair_distances
改めてマニュアルとベンチの結果を確認し、マッチングの処理を改善してアプリの評判を上げるのが良いのではないか、という判断になりました
この評価はアプリの評判に影響を与えるため、高い評価はユーザー数の増加に繋がります。
評価には以下のような項目が関係していることが知られています。
- 配車を依頼してから実際に椅子が割り当てられるまでの時間。
- 割り当てられた椅子が乗車位置に早く到着しそうかどうか(乗車位置に近い椅子が割り当てられていたり、遠くても速い椅子が割り当てられている場合が好ましい)
- 椅子が割り当てられてから、ユーザーが乗車できるまでの時間。
- ユーザーが乗車してから目的地に到着するまでの時間。
ベンチの結果
time=12:28:03.575 level=INFO msg=90.0%のライドは椅子がマッチされるまでの時間、96.7%のライドはマッチされた椅子が乗車地点までに掛かる時間、98.3%のライドは椅子の実移動時間に不満がありました
以下の順番で対応することにしました。 マッチングはgo/internal_handlers.go
に処理が固まり気味でコンフリクトが発生しそうな場合は、SSE
などの改善に取り掛かる、ということにしました。
- ライドがマッチされる時間を改善
- 乗車位置に近い椅子を割り当てる
- 速い椅子を割り当てる
13:30-15:30 ライドがマッチされる時間を改善 3822 -> 3778
あまりスコアが上がりませんでしたww (むしろ下がった)
- 雑に全員マッチングして、rideが滞留しないように修正
- 椅子がマッチされるまでの時間を雑に改善 (29.4%のライドは椅子がマッチされるまでの時間、97.1%のライドはマッチされた椅子が乗車地点までに掛かる時間、98.5%のライドは椅子の実移動時間に不満がありました)
- fix: 雑に全員マッチング (time=15:30:42.220 level=INFO msg=10.0%のライドは椅子がマッチされるまでの時間、90.0%のライドはマッチされた椅子が乗車地点までに掛かる時間、100.0%のライドは椅子の実移動時間に不満がありました time=15:30:42.220 level=INFO msg=結果 pass=true スコア=3778 種別エラー数=map[])
90.0%のライドは椅子がマッチされるまでの時間
から以下に改善はされたので、スコアは伸びませんでしたがそのままにしました
10.0%のライドは椅子がマッチされるまでの時間
15:30-16:30 乗車位置に近い椅子を割り当てる 3778 -> 9477
- 乗車位置に近い椅子を割り当てる
- GetNearbyChairsInRide
- Revert "fix: 雑に全員マッチング (time=15:30:42.220 level=INFO msg=10.0%のライドは椅子がマッチされるまでの時間、90.0%のライドはマッチされた椅子が乗車地点までに掛かる時間、100.0%のライドは椅子の実移動時間に不満がありました time=15:30:42.220 level=INFO msg=結果 pass=true スコア=3778 種別エラー数=map[])"
- fix: 一度に複数のrideを処理するようにした (msg=53.1%のライドは椅子がマッチされるまでの時間、52.3%のライドはマッチされた椅子が乗車地点までに掛かる時間、97.7%のライドは椅子の実移動時間に不満がありました)
- fix: N+1の解消 (40.3%のライドは椅子がマッチされるまでの時間、49.0%のライドはマッチされた椅子が乗車地点までに掛かる時間、99.0%のライドは椅子の実移動時間に不満がありました)
40.3%のライドは椅子がマッチされるまでの時間、49.0%のライドはマッチされた椅子が乗車地点までに掛かる時間、99.0%のライドは椅子の実移動時間に不満がありました
アプリの評判も全体的に上がってきました 💪
この時点で残り1時間半程度だったので、以下を分担することにしました
- 片方は速い椅子を割り当てる
- もう一方はサーバーの冗長化に取り組む
16:30-17:55 ギリギリでサーバーの冗長化を間に合わせる 9477 -> 19996
サーバー冗長化、嵌りどころが多く苦戦しました
- Deadlockが発生
- DBサーバー側の
isuride-matcher.service
を止めないといけないことに中々気づけず
- DBサーバー側の
- ベンチにて
クリティカルエラーが発生しました error="椅子がライドの完了通知を受け取る前に、別の新しいライドの通知を受け取りました
が発生するものの原因が分からない- 何かしらの排他制御ができていないと思ったのですが、雑に間隔をあけることで事象の解消を狙いました
- 通知のAPIの間隔30ms->500msに伸ばす(多分これはパフォーマンス的にも効果があった)
-
/api/internal/matching
は3秒以上経過したrideを割り当て対象にする -
isuride-matcher.service
の間隔を1秒に伸ばす
- 何かしらの排他制御ができていないと思ったのですが、雑に間隔をあけることで事象の解消を狙いました
- 修正が思った通りの結果にならない
- 単に
go build
を忘れているだけでした 😇
- 単に
ギリギリで以下の修正を入れて終了となりました。19996点まで伸びました 🎉
サーバー冗長化 + 通知間隔を延ばす + 3秒以上経過したrideを割り当て対象にする
1万点以上増えたのは、おそらく サーバー冗長化
と 通知間隔を延ばす
の両方を実施できたおかげだったのかなと思います。
並行して取り組んでいた 速い椅子を割り当てる
の修正についても
クリティカルエラーが発生しました error="椅子がライドの完了通知を受け取る前に、別の新しいライドの通知を受け取りました
上記のエラーが何故か発生してしまい、修正が間に合わずでした。
最終的なベンチマーク上のアプリの評価は以下となっていました。
39.2%のライドは椅子がマッチされるまでの時間、55.6%のライドはマッチされた椅子が乗車地点までに掛かる時間、81.1%のライドは椅子の実移動時間に不満がありました
感想
アプリケーションとしての機能が多く、非常にアプローチの多い問題でした。
その中でもチーム内で優先順位を決めて、時間内にある程度の修正をやりきれたのは非常に良かったと思います。
以下の技術的に面白い項目があったものの、時間内に取り組めなかったのが心残りなので、あとで復習しようと思います。
- SSE(Server-Sent Events)
- Idempotency-Keyヘッダを利用したリクエストの重複防止
- 通知の順序性の確保と、
at least once
の実現
またISUCONの後には、居酒屋で反省会であーだこーだと言い合うのが楽しいですね🍻
一緒に参加したメンバーは新卒の同期であるものの、普段は別の会社で働いているので、年に一度のISUCONの機会はとても貴重だなと感じます。改めてISUCONの運営メンバーに感謝です。
最後に居酒屋で食べたお刺身で締めようと思います。また来年!
Discussion