モヒカンメモ

髪色が定期的に変わることに定評のある(比較的)若者Webエンジニアの備忘録

ISUCON 14に参戦して3位入賞しましたヤッター!!

もはやライフワークとなっている、LINEヤフー社 主催のパフォーマンスチューニングコンテスト「ISUCON」に今年も参戦しました。

isucon.net

ISUCONとはLINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです

リザルト

52,150点 (834チーム中3位)

isucon.net

ISUCON 14: 3位

ISUCON 14: 特別賞

ISUCON 14: Go言語賞 (さくらインターネット様ご提供)

総合 3位 & 特別賞 & Go言語賞を受賞しました!!!!!!!!!!

いや〜〜〜〜〜〜、嬉しい! ISUCONへ本格的に参戦し始めて7回目?の今回、やっと入賞できました👏👏👏👏👏👏👏

去年のブログ

blog.pinkumohikan.com

チーム

のべ3回におよぶチーム名検討会議を経ても決定に至らないほど難産で、最終的に // TODO: あとでちゃんとしたチーム名になおす というチーム名で出場しました。

厳正なる素振り個人バトルの結果、今回はインフラ担当をnekodaisuki氏に奪われてしまったため自分はアプリをいじる担当として参戦しました。

やったことなど

GitHub Repository github.com

初動

初動でindexチューニングするのをルーティーンとしています。ある程度スループットが出ないと真のボトルネックに気付けないことがあるので、最初はパッと確実に倒せる敵を倒すことに注力するようにしています。

mysql> explain SELECT status FROM ride_statuses WHERE ride_id = '01JEHXD1A9DX67EHT04T3E76JN' ORDER BY created_at DESC LIMIT 1;
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
| id | select_type | table         | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                       |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
|  1 | SIMPLE      | ride_statuses | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 4513 |    10.00 | Using where; Using filesort |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
1 row in set, 1 warning (0.01 sec)

mysql> ALTER TABLE ride_statuses ADD INDEX (ride_id, created_at DESC);
Query OK, 0 rows affected (0.20 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain SELECT status FROM ride_statuses WHERE ride_id = '01JEHXD1A9DX67EHT04T3E76JN' ORDER BY created_at DESC LIMIT 1;
+----+-------------+---------------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table         | partitions | type | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+---------------+------------+------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | ride_statuses | NULL       | ref  | ride_id       | ride_id | 106     | const |    2 |   100.00 | NULL  |
+----+-------------+---------------+------------+------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

その間におかしょいが定番施策 (DBコネクションのプーリングや動的プレースホルダ)、nekodaisukiがデータベースサーバの分離を進め、まずまずの出だしでした。

出だしはまずまず

作戦会議

初動のアレコレを一通り終えたあと当日マニュアル・アプリケーション仕様書からの気になりポイントやリソース使用状況などを全員で確認しました。

  • データベースの負荷が現時点のボトルネックであること
  • エンドポイント単位では通知取得 GET /api/app/notification が最も重いこと
  • マッチング間隔、通知のポーリング間隔などの可変要素があること
  • マッチングがランダムとなっているので、改善の余地があること (そこが重要そうと当日マニュアルで匂わされていること)
  • ベンチマーカーのログに ◯%のライドは椅子がマッチされるまでの時間、◯%のライドはマッチされた椅子が乗車地点までに掛かる時間、◯%のライドは椅子の実移動時間に不満がありました のようなヒントが出ていること

上記を踏まえておかしょいはポーリング間隔の調整、nekodaisukiはGo用プロファイラの導入、自分はマッチング間隔をアプリ単体でコントロールできるように実装変更へ着手。

マッチング間隔の調整

スコア 8775 時点のベンチマーカーログに 93.8%のライドは椅子がマッチされるまでの時間、95.9%のライドはマッチされた椅子が乗車地点までに掛かる時間、20.6%のライドは椅子の実移動時間に不満がありました というヒントが出ており、マッチングまでの時間が長いため満足度が下がって離脱が増えていると推測しました。

ベンチマーカーのログ

マッチングの仕様変更をしやすいようにsystemdからキックされていたものをアプリ側でworkerプロセスを用意する形に設計変更するついでに、 93.8%のライドは椅子がマッチされるまでの時間 とあることからマッチング間隔が長すぎるんだろうなあって思って試しに0.5s → 0.1sに短くしてみたらこれがヒットしてスコア 25758に!

スコア 25758

ベンチマーカーのログ

github.com

特別賞狙い

続けて マッチされた椅子が乗車地点までに掛かる時間 を短縮する施策にチャレンジしようとしていたところ、nekodaisuki氏の「いま特別賞に手がかかってるのでは?」という一声で特別賞を狙いに行くことに。

システム全体の負荷を俯瞰したところ依然としてデータベースの負荷が高い状態が確認できた。

負荷状況

データベースのCPU負荷を少しでも下げるためにスロークエリログをとめたが31362点でわずかに届かず。コネクションプーリングを行うようにアプリ側を調整したり、マッチング間隔の微調整をしたところ特別賞スコアに到達!

特別賞スコアに到達

やった〜〜〜〜。

伸び悩み

良い時間だったので入れ替わり立ち替わり休憩を取りながら、自分はマッチングアルゴリズムの改善に着手。 マッチされた椅子が乗車地点までに掛かる時間 を短縮するには、ピックアップ位置に最も近い椅子をアサインすれば良さそうと思って下記のようなPRを準備。

github.com

細かい不具合修正やらパフォーマンス調整やらをして最終的にベンチマークは通る状態になったがスコアは伸びず、「あれれ~??」となって一旦寝かせることに。 マッチされた椅子が乗車地点までに掛かる時間 に対する不満は 86.7% から 43.6% に下がっていたので方向性は間違ってないはずなんだけど。。。

乗車までの時間は短縮できたはずなのに

派生として遠くまで乗ってくれるライドを優先的にアサインするような実装も準備してみたが、こちらもパットせず。

github.com

合計4時間?くらい費やしたが残り1.5hほどとなったので渋々諦め、その後は細かい調整をしたり、indexチューニングでちょっと点を稼いだり、最後に決済のリトライの改善を試みるも無念の時間切れ。意気消沈していましたが、でもいいんだ。なぜなら俺らには特別賞があるから。

これ、今改めて良く見ると改善は上手く行っていてマッチング間隔にネックが戻っていましたね。。。これとマッチング間隔の短縮を組み合わせれば早期にスコア上がっていただろうに・・・。

最高スコアアタック

計測ツールやログ出力をとめ、最後にマッチング間隔やらポーリングやらの調整を行って最高スコアアタックして競技終了。

シャオラ (再掲)

感想

今回もめちゃくちゃ唸らされる良問でした。

色々なところにボトルネックが仕込まれているので「何もできないまま終わる」ことが少なそうなので万人におすすめできて、かつSSEなどのちょっと違う頭を使うようなポイントもあって「どのタイミングで何を課題として捉えて、どういう取捨選択をするか」のシナリオが色々ありそうに思いました。

今回ついに念願の入賞を果たせましたが、終盤は伸び悩んで不完全燃焼感がある状態でのフィニッシュだったのでまた1年間練習して、次は完全燃焼のうえで2位以上を目指したいところです。

LINEヤフー社をはじめ問題作成して下さった皆さま、スポンサーの皆さま、今年も超楽しかったです。来年もまた宜しくお願いします!!!!!!!