ISUCON初心者の挑戦記 (ISUCON14)

友人が誘ってくれたので初めてisuconに挑戦しました！

競技終了時点のスコアは21858で全体831チーム中55位の結果でした〜。
（ベストスコアが29554だったので、そのスコアで安定的に稼動させられれば企業賞圏内に入れた...悔しい）

友人も初参加であり、初心者2人チームでの参加だったので、真ん中より上になれたらいいな〜ぐらいの気持ちでしたが、結果的には思ったより健闘できて嬉しかったです。

![スクリーンショット 2024-12-09 7.03.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265286/6113e4ef-fd83-cdeb-f243-3251bed6247d.png)

リーダーボードが見えなくなる17時の時点で11位に入れていたときの熱気はそれはもうすごいものでした。
とても楽しかったので、また来年も対戦よろしくお願いいたします。

以下は来年に向けての備忘録です。

# 事前準備

2人とも初参加であり、右も左もわからなかったので[達人が教えるWebパフォーマンスチューニング　〜ISUCONから学ぶ高速化の実践](https://www.amazon.co.jp/dp/4297128462) を各々読み、private-isuを一通りやってみるところから始めました。

その後 11/2 に初めて2人で素振りを行いましたが、あまりの解けなさに「やばすぎ〜〜〜」となり、その後本番まで3回ほど素振りを行い当日を迎えました。
（大体インスタンスを立ち上げて計測の準備をするだけでもう疲れていた）

事前準備はひたすらにMakefileを書いていました。

- `make setup` を叩くと以下セットアップの一連の処理を実行するようにしたり
  - 競技用インスタンスへの使用するツールのインストール 
  - githubへのDeploy keyの登録と競技用インスタンスへの設置
  - 競技用サーバーのホームディレクトリ下をgit管理化
  - etc下に存在するnginxやmysqlの設定ファイルの収集
  - プロファイリング用のツールのインストール
  - プロファイリング用サーバ->競技サーバ アクセス疎通の為のポートフォワード設定
- `make deploy` でcurent branchをデプロイできるようにしたのは体験が良かった

# プロファイリング

pproteinを使用していました。

https://github.com/kaz/pprotein

便利〜〜〜〜〜！

ベンチのinitialize時の処理に対してフックを挟むようにすることで自動で計測を開始し、Web上からpprof,alp,slpの結果をいい感じに見ることのできるツールです。

最初は自前でログローテートの自動化などを自前でやっていたのですが、pproteinではcollect開始時に自動で全てを行なってくれるのでそれが不要になり、かなりハッピ〜でした。

複数台構成になっても、各サーバー内にpprotein agentを起動しておけばログ収集先をWeb上から変更するだけでいい感じになるので神。

またチーム全員が見られるモニターに常時topの結果を表示することで、ベンチ時の状況を観察しやすくしていました。

# 問題説明 ＆ 初動

- ライドチェアというネーミングセンスが神すぎる ＆ 動画もめっちゃ良かった...！
- `make setup` を叩いて、わちゃわちゃしつつも10:30にはプロファイリングの準備も整い、初期ベンチを回しました
- 初期ベンチが 777 でテンションが上がる

# 改善タイム（やったこと）

序盤の大まかな役割分担として友人がDB周りの改善、 自分（@oike17) がアプリケーション周りの改善を行なっていました。
中盤以降はお互いに改善点を見つけたところからどんどん手を動かしていくという感じに。

## 10:30 ~ 12:00 (777 -> 4867)

- プリペアードステートメントの無効化 (友人)
- DBに諸々インデックスを追加 (友人)
- `/api/app/notification` の呼び出し毎に料金の計算を行なっていたので、ライドに対する料金をオンメモリキャッシュ化 (@oike17)
- `/api/app/notification`, `/api/chair/notification` の呼び出し回数がかなり多いので、リトライ間隔を30ms -> 500msに伸ばしてお茶をにごす (@oike17)

## 12:00 ~ 15:00 (4867 -> 19355)

- ライドの最新状態のオンメモリキャッシュ化(@oike17)
  - getLatestRideStatus が様々な箇所で呼び出されており呼び出し回数が多い ＆ ステータスの反映自体は3秒の遅延が許されるとのことだったので、ライドに対する最新のステータスをオンメモリに載せようと奮闘
  - これが魔境だった...この辺りから意図しない状態遷移が起こるようになり、終盤でもこの問題に苦しめられることに
  - この時点ではキャッシュのexpireを短めに設定することで↑の問題自体は解消したので、それでお茶を濁す
- チェアのトータル移動距離と最新の位置情報のオンメモリキャッシュ化（友人）
  - トータル移動距離を計算するために都度移動履歴を全件取得し計算するのが重い...
  - 計算に必要な最新の位置情報をキャッシュしておき、トータル移動距離の計算結果をキャッシュに載せる
- アプリケーションサーバーとDBサーバーを分割(@oike17)
  - 分割前のスコアが8590だったのに対して、ここで一気に点数が伸び2万点が見える

## 15:00 ~ 17:30 (19355 -> 29536)

- Matching Timeを0.5s -> 0.3s にする(@oike17)
- Middlewareの処理中の取得しているチェア情報をオンメモリキャッシュ化 (友人)
- 上位に入るとは全く思っておらず、ここまで全体の順位は見ずに進めていたが、ふとリーダーボードを見ると7位に入っており、？？？？となる
- チェアの位置情報＆ステータスのバルクインサート化 (@oike17)
  - `/api/chair/coordinate` に対するPOSTリクエストがとにかく多い
  - 前述の通りステータスの反映は3秒の遅延を許容するとのことだったので、ハンドラー内では位置情報をbufferに溜め込む処理だけ行いレスポンスを即返す
  - 上記で溜め込んだ位置情報を goroutine で一定時間＆件数おきにバルクインサートする
- NearbyChairで使用しているチェアの最新の位置情報の取得をキャッシュから行う(@oike17)
- Matching Timeを調整 Part. 2（友人）
  - Matching Timeを短くするとスコアが伸びるが、ベンチがかなり不安定になって泣く

## 17:30 ~ 18:00

- サーバの再起動試験
- お掃除: slow-query logやnginxのアクセスログの出力を無効化、pprotein用のコードの削除
- 性能の変数になりうる箇所をいじって、最後にひたすらにベンチを回して点数のブレを楽しむ
  - この時点でなぜかベンチが5回に1回ぐらいfailするようになっており、追試ガチャへの心の準備をする


# 振り返っての悔しいポイント

ざーっとコードを読んでいる際に、マッチングの実装箇所に以下のようなコメントがあり、これは運営からのヒント！！！とチームで話していたが、目に見えるボトルネックの改善を優先してしまってここを後回しにしてしまいました。

> 	// MEMO: 一旦最も待たせているリクエストに適当な空いている椅子マッチさせる実装とする。おそらくもっといい方法があるはず…

アプリケーション自体とベンチスコアの特定計算方法についてもっと理解を深めておけば、ここを改善するという判断ができたなーと反省。

# おわりに

isucon初参加でしたが、思っていた以上に手を動かせて「これがスコアを上げていく快感か！！！」となりました。
実務では普段触っていない箇所を触ることができ、学びも多かったので自分的にはかなり満足しています。

友人とのチームでの参加楽しかったので、来年はもうひとりメンバーを見つけても頑張って上位を狙ってみようかと思います。

解きごたえ抜群で非常に楽しい問題でした！ 運営の方々、本当にありがとうございました！