ISUCON14に参加しましたが失格になりました

この記事は、[Supership Advent Calendar](https://qiita.com/advent-calendar/2024/supership)の13日目の記事になります。

12/8に開催された、ISUCON14に参加しました！

学生時代のアルバイト仲間2人と、計3人チームで参加しました。
3人とも昨年開催のISUCON13が初参加であり、2回目の参加となります。

###### [ISUCON14 問題](https://github.com/isucon/isucon14)

ISURIDEという椅子のライドシェアアプリです。

# 結果

最終的なスコアは20401点で、Leaderboard上では831チーム中61位となり、そこそこ良い結果にチーム全員喜んでいました。

...が、翌日の正式結果では失格となってしまいました。

### 失格になった原因

失格理由は`データ保持NG : 追試における負荷走行により作成されたデータが、再起動後に取得できませんでした`とのこと。

これ以上詳しい理由は記載されていなかったのですが、`chair_locations`にインメモリキャッシュを仕込んだ際、DBにデータをINSERTする処理を削除してしまったことが原因だと思われます。レギュレーションにも

`ベンチマーク実行時にアプリケーションに書き込まれたデータは再起動後にも取得できること`

という記載がありました。

INSERTを消してしまったので、インメモリキャッシュ上にしかないデータは再起動時に消えてしまい、データの整合性が失われたということです。

なので、インメモリキャッシュを導入する際、INSERT, UPDATEなどのDBを更新する処理は消さないように気をつけなければいけません。

# やったこと

準備期間〜本番終了までを振り返っていきます。

### 準備期間

計３回、チームで過去問(ISUCON12予選、ISUCON13)を使って練習を行いました。過去問演習には[ISUNARABE](https://isunarabe.org/)というプラットフォームを用いました。ベンチマークの実行や結果の確認をWeb上で行うことができ、とても便利でした。

練習は基本的に、本番の流れを意識して行なっていました。まずセットアップを行い、計測→ボトルネック発見→改善→計測... を繰り返す形です。

### 直前会議

本番は10:00からなのですが、30分早く集まって役割分担や最初にやることの確認をしました。
一人がインフラ周りのセットアップやMakefileの調整、もう一人がデバッガやpprofの導入、そして自分はアプリケーションマニュアルを読み込んだり実際に触ったりして題材を理解する担当になりました。

### 開始直後の動き

直前会議で決めた役割で各々動き始めました。初期セットアップが30分くらいで完了したので、計測に入りました。自分たちのチームは計測ツールとしてalp, pt-query-digest, fgprofを使用しました。makeコマンドでalpとpt-query-digestの結果がSlackに飛ぶようにしています。初期ベンチの結果は938点でした。

### テーブルにインデックスを貼る

topコマンドの計測結果を見ると、圧倒的にDBのCPU負荷が高かったので、DBの改善から着手しました。一番重かったクエリがかなり複雑そうだった(後述します)ので、2番目、3番目あたりにでてきたすぐに終わりそうなものから作業していきました。この時点で3000点位になりました。

### キャッシュの導入

DBの計測結果で最も負荷がかかっていたクエリは以下のようなものでした。

```
SELECT id,
       owner_id,
       name,
       access_token,
       model,
       is_active,
       created_at,
       updated_at,
       IFNULL(total_distance, 0) AS total_distance,
       total_distance_updated_at
FROM chairs
       LEFT JOIN (SELECT chair_id,
                          SUM(IFNULL(distance, 0)) AS total_distance,
                          MAX(created_at)          AS total_distance_updated_at
                   FROM (SELECT chair_id,
                                created_at,
                                ABS(latitude - LAG(latitude) OVER (PARTITION BY chair_id ORDER BY created_at)) +
                                ABS(longitude - LAG(longitude) OVER (PARTITION BY chair_id ORDER BY created_at)) AS distance
                         FROM chair_locations) tmp
                   GROUP BY chair_id) distance_table ON distance_table.chair_id = chairs.id
```

かなり複雑なクエリです。インデックスを張る作業は分担して行っていましたが、これはチーム全員でどのように対処するか考えました。

結論、キャッシュを導入してこのクエリを使わなくてよいようにするという方針で行きました。チームの一人がキャッシュを実装し、それがうまくいかなかったときのための二の矢として、クエリ自体を改善する方法を自分が検討しました。

結果的にキャッシュの導入がうまくいき、5000点位に伸びました。

ちなみに、キャッシュは[こちらのライブラリ](https://github.com/catatsuy/cache)を使用しました。ISUCON用に作成されたライブラリで、とても使いやすいらしいです。

### 通知のポーリング間隔を伸ばす

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929447/08a9a587-3a29-bca8-9143-cabfb0f2c96f.png)

alpの計測結果を見ていると、通知を取得するAPIに時間がかかっていたので、`retryAfterMs`というパラメータを調整することでポーリング間隔を伸ばしてみました。もともとの値は30msだったのですが、マニュアルを読むと、3秒まで遅れてもよいと記載されていたので、`retryAfterMs`を2500msに変更しました。そうするとDBの負荷がかなり下がり、スコアは7000点位になりました。

### DB分割

すこしキャッシュにバグがあり、パフォーマンスを上げるとエラー数が上限に達しベンチが落ちてしまうという現象が起こったのですが、キャッシュを実装してくれたメンバーがすぐにそのバグを直してくれ、晴れてDBとアプリのサーバーを分ける作業に入りました。分割はすんなり完了しましたが、それほど点数は伸びず8000点位となりました。

### トランザクションの削除

この時点で、DBのクエリの82.5%が`COMMIT`になっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929447/489c4b2c-8005-aa06-4798-89a3aea0e3f2.png)

`COMMIT`にコメントをつけて、どのトランザクションのコミットか区別して調べた結果、`COMMIT`の9割以上がchair_locationsテーブルにINSERTするためのトランザクションであることがわかりました。

そこで、chair_locationsテーブルのデータをインメモリキャッシュ上だけで管理することによって、chair_locationsテーブルを一切参照しないように変更を加えました。そうすることでトランザクションを張る必要がなく、結果的に`COMMIT`を削除することができます。

ただ、ここでINSERTを削除してしまったので、最終的に失格になってしまいました...

ここでも点数はそれほど伸びず、8500点位になりました。

### 通知のポーリング期間の再調整

topコマンドの結果をみてもDBの負荷が小さく、またDB分割を行ったことで今までより大きな負荷がかけられるようになったはずなので、先ほど伸ばしていたポーリングの間隔を縮めてみることにしました。

`retryAfterMs`の値を2500msから400ms秒に変更すると、一気に点数が21818点まで上昇しました。この瞬間、順位が15位にまで登りテンションが上がりました。

ただ、`retryAfterMs`の値を小さくしすぎるとなぜかエラーが大量発生してベンチが落ちることがあったので、400msから500msに伸ばして再度ベンチを実行したところ20401点となり、これが最終スコアとなりました。

# 振り返り

##### 準備期間の振り返り

今回は一つの過去問を突き詰めるというより、基本的な改善に慣れることを優先し、ISUCON13の改善はそこそこまでで終わらせ、ISUCON12予選の練習も行いましたが、振り返ると、一つの問題を突き詰める方がさまざまな改善手法の知見を得られて学びが多いと思いました。その分、基本的な手法は個人練習でブラッシュアップするのが良いと感じました。

##### 当日の振り返り

通知を取得するAPIの改善手法の一例として、SSE(Server-Sent Events)というリアルタイム通知を行うための技術が紹介されていました。通知を取得するAPIがボトルネックになったタイミングで、メンバーの一人がSSEの実装に取り掛かってくれました。しかし、初見の技術ということもあり、時間内に実装しきることができませんでした。

SSEを実装した方曰く、実装してもそれほどスコアは伸びなかったそうです。

SSEの実装にメンバーの一人がそれなりの時間を割いてしまったので、この時間の一部を他の作業に充てていれば、最後に見つかったN+1のボトルネックも解消できていたかもしれないなあと思いました。

マニュアルで紹介されているからと言って、その実装を行えば必ずスコアが伸びると決めつけるのではなく、あくまで計測結果に従って改善を行うのが大事だと学びました。

# さいごに

ほとんど基本的な改善だけでここまでスコアが伸びるのかと驚きました。今回失格にはなってしまいましたが、次回以降のモチベーションにつながりました。

今回は悔しい結果となったので、次回までに基本的な改善をもっとサクサク実装できるようになり、題材に固有の難しい改善について考える時間を十分に確保できるようになりたいです。

:::note
**最後に宣伝です。**

Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。
:::