ISUCON14 に出場したので個人としての振り返りを書く (InfraNinja / 9,966点)

記事タイトルとURLをコピーする

こんにちは。自称ソフトウェアエンジニアの橋本 (@hassaku_63)です。

2024/12/08 に開催された ISUCON14 に同僚と出場してきました。個人としての感想エントリを書きます。

ISUCON って?

限られたリソースで Web アプリケーションの応答性能を最大化する、「チューニングバトル」を謳う競技形式のイベントです。今年は過去最高の 834チーム、1920名(!)が参加したようです。

isucon.net

運営が用意しているベンチマーカーからの HTTP リクエストに対する応答性能、あるいはお題となったサービスが仮想顧客(ベンチマーカー)に提供できたサービスリクエスト数などがスコア要素として考慮されます。

当社メンバー、あるいは読者のみなさんの中には ISUCON を良く知らない方もあると思いますが、より詳しくは以下のスライドをご確認ください。

speakerdeck.com

チューニングという字面のインパクトが強いので未参加の方はそこに印象を引っ張られがち(私調べ)ですが、実際には Web アプリケーションを構成する幅広い要素に対する理解を深められる本当に素晴らしいイベントです。チューニングを通してあらゆる箇所を調べ、変更を行うことになるので、その過程で様々な知識を身に着けられます。チューニングという仕事を縁遠く感じている方でもきっと素晴らしい刺激と学びを得られるはずです。

主催者のみなさま、毎年の開催本当にありがとうございます。

結果

InfraNinja というチームで参加しました。9,966点でした。有効なスコアを獲得できたチームは514チームあり、その中で130位でした。過去イチの善戦でした。

個人的な興味で、この n=514 のスコアを順位ソートした状態でプロットしてみました。上位20%くらいでスコア総和の約50%を占めています。ランキング上位チームのスコアは本当に異次元すぎて、何をしたらそのステージに到達できるのか想像すら及びません。

スコアの分布

お題アプリ

ライドシェアっぽい架空のサービス "ISURIDE" が今回のお題でした。

ISU に乗車(?)したい一般利用者と、ISU に利用する椅子を提供するオーナーのための機能を有します。

ISURIDE 画面サンプル

お題アプリとベンチマーカーの実装は、早速 GitHub で公開されています。どういう挙動か詳しく見てみたい方は以下のレポジトリを参照してください。

github.com

振り返り

十分とはいえないまでも、catatsuy/private-isu を使って多少の予習は実施していました。

事前にチーム内でブリーフィングを実施したのは良かったと思います。当日の初動の分担が明確になりました。特に、同僚の一人が Ansible プレイブックを作成してくれていたので、私を含む他のメンバーはそのあたりの準備を意識する必要がなくなりました。初動の速さは実質的な競技時間を左右する重要要素なので、そこがきっちり整備されつつチーム内の分担として意識共有できていたのは良かった点だと思います。

個人の振り返りとしては、いくつかの反省点と、モヤモヤが残ったポイントがありました。全般的には、ソフトウェア開発のスキル不足と、DB 力(クエリの読み書きや内部構造の理解、データの読み方などなど)の不足を強く実感しています。気にしてたポイントが妥当だったかは別として、反省点を以降で書き出してみます。

反省点

まず、最も反省すべきと思ったのはマニュアルを通読しなかったことです。もっと一般化すると、最初にアプリケーション仕様の理解に十分な時間を確保しなかったことを反省しています。

レギュレーション当日マニュアルはざっと流し読みしたのですが、そこからリンクされている ISURIDE アプリケーションマニュアルの存在を完全に読み飛ばしてしまっていました。過去の出場経験からこれらのドキュメントに目を通す大事さは知っていたのに、それができてなかったのは悔しいです。特にアプリケーションマニュアルの方は、文章でアプリの仕様を説明してくれています。明らかにコードリーディングより先に確認すべき重要度の高い情報でした。さらには、特定のエンドポイント(次の反省点で登場する appGetNotification のロジックです)では SSE(Server-Sent Events) を用いた実装に変更してよい旨が書かれており、改善手法のヒントすら提示されていました*1。さすがに見逃しちゃいけない情報でした。

2点目は、ロックによって応答性能が低下している可能性を十分に検証できなかったことです。

トランザクションを過剰に取っているのでは?と推測した箇所がありました。そのクエリが実行されているロジック (chairGetNotification) は、クライアントから定期的にコールされるイベント通知用の API でした。おそらく位置情報の更新を通知するための機構だったはず。初期実装ではなんと接続ユーザー1人につき、30ms ごと(!)に該当の API が呼ばれる設定になっており、ぱっとみ負荷が掛かりそうな箇所に見えました。で、このロジックが長いトランザクションを取るのは応答性能に対して不都合であるのではと思ったので、それをうまいこと緩和できないかと思っていました。

結局、このロジックのテコ入れについては、実行インターバル (retryAfterMs) の緩和と、純粋な参照系クエリの部分のみをトランザクションから分離し、SELECT FOR UPDATE と INSERT のクエリのみを囲う形での実装変更する2点を実施しました*2。この変更はある程度スコアにも寄与しましたが、この改修の根拠が十分確信できたうえでの判断ではなかったのでその点を反省しています。

ちなみに、当該ロジックについては SSE (Server Sent Events) による実装に変更してもよいとする旨がISURIDE アプリケーションマニュアルにも記載されていました。対応したか・できたかはさておき、そういう方法があることを把握していなかったのは明確に確認不足だったと思います。

ロックの発生状況に関しては、おそらくシステムテーブルをクエリするのが回答になると思います。そのあたりは後日裏取り調査しつつ、次回までには読み方も押さえておきたいなと思いました。

3点目は、椅子のマッチングのロジック改修で、(エレガントではないが)現実的でコストが掛からない案を選択できなかったこと。

乗車したい利用者と、空いている椅子のマッチングを行うロジック (internalGetMatching) がありました。初期実装では、このロジックをサーバーサイドで 0.5s ごとに定期的に回すようになっていました。このロジックの問題点は、1回の処理あたりでやっているロジックが「今最も待たせている顧客1名に、空いている椅子1つをランダムに利用者1人に割り当てる」になっていたことです。ソースコードにも以下のようにヒントとなるコメントがありました。

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

言うまでもなくこのロジックには改善余地があります。少なくとも次の2点が問題だと思いました。

  1. 最大でも 0.5s ごとに1人までしかマッチングできないので、利用者からすると満足度が下がる(つまりスコアが低下する要因になる)
  2. ランダムマッチなので、より早く到着する椅子を割り当てできていない(よって利用者の満足度が低下し、スコアに影響する)

この対策として、とりあえずの対応としてマッチングロジックの実行頻度を引き上げました。systemd から呼び出している環境変数の定義ファイル ~isucon/env.shISUCON_MATCHING_INTERVAL を変更しました。並行して、カラムとインデックス定義の変更を考えました。つまり、座標のカラム(初期実装では latitude, longitude という INT 型、インデックスなしの2カラムがある)を二次元座標でも持つようにして、それに適したインデックスを持つようにする案がよぎりました。MySQL であればこれは「空間インデックス (Spatial Index)」を使うことになります。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 11.4.10 空間インデックスの作成

実際に私はこのインデックスを活用したアプリケーションを書いた経験があったわけではないので、そもそもの実現性の検証から入る必要がありました。思えばこの時点でコストと不確実性が高い選択をしようとしていたわけで、もっとコストを掛けず改善できる案がないかを検討すべきだったと思います。

実際にコードを変更するまえに手元で MySQL のコンテナを動かしつつ、データの投げ方やクエリの書き方など調べていました。アプリ側の改修を最初から意識すると余計に時間がかかってしまうので検証コストが小さくなるような実現性検証のムーブができたのは良かったと思います。その過程で EXPLAIN の結果が好ましくないことであったり、効率的なマッチングを実現するアルゴリズムだったりが必要になることがわかってきて、それらの対応が競技時間で収まる見通しが持てないことも見えてきて、最終的には断念しました。しかし、もっと早い段階でピボットできていたはずですし、そもそも最初からミニマムコストで効果が期待できる方法を模索する考え方が徹底できていれば...と思います。

終了後の解説でも言及がありましたが、「マッチング可能な椅子をスキャンしてアプリ側で強引に全件探索し、より近い椅子を割り当てる」のような単純なアプローチを発想できてればよかったです。エレガントでなくとも、少なくとも今よりはマシになる見込みがありました。DB を巻き込む大きな改修を入れることなく、アプリ実装で完結できる改善施策を打てていたはずです。自分の提案は、チームメンバー全体を巻き込み、少なくない時間を投資してまで押し通す方針ではなかったなと思いました。

4点目は、非効率なクエリの書き換えに圧倒的に慣れていないこと。

代表的なのは N+1 の解消ですが、ロジック全体を見つつクエリもしくは該当機能の返す結果が等価になるよう書き換えるスキルがまだまだ私には不足していると思いました。このへんはもしかすると生成AIがある程度解消してくれる課題かもしれませんが、とはいえ効果的にインデックスが効くような JOIN クエリはどう書く?みたいな前提知識の部分がまだ私には圧倒的に不足しています。特にクエリ最適化の領域において、もし今私が生成AIに頼ったとしても、おそらく入力として与える問いの仮説であったり、戻ってきた回答を正しくレビューする力が私にはありません(実際、今回がそうでした)。

この辺は過去問でしっかり場数をこなしていくことが必要そうです。クエリの等価性の話だけではなく、そもそもこれは何を実現するためのロジックなのか?といった機能自体の理解を素早く行えることも重要になると思います。こればかりは圧倒的に場数不足だなと思いました。

感想

何年前までの ISUCON における採点基準は、純粋な応答性能をスコアに反映している印象がありました。対して今回の ISUCON では、ベンチマークのログを見ていてもサービス利用者の満足度に意識を向けさせるような表現がされており、チューニングを行う「その先の目的」を意識させる作りになっていたように思います。その点で、個人的にはすごくよい問題だったと思います。AWS の "GameDay" っぽい採点基準だなと思いました。

Ansible で最初の環境設定が省力化できたのは物凄く大きかったです。私個人のアンテナの範囲外にあるツールだったので、キャッチアップして来なかったのです。今回初めてその威力を味わいました。簡単にでも復習して、来年自分でも同じことができるようにしたいなと思いました。

Go の pprof の利用方法を前もって復習できたのは良かった点でした。前回参戦時はロクに使えなかったので、その点は進展がありました。ただ、今回に関しては目に見えてこれとわかるほどのボトルネックは出ていない印象でしたので、個人的にはそれほど活用しませんでした。OS や DB からメトリックを拾って考察する必要があったように思いますが、そのへんの勘所は色々な経験を積むしかないと思うので、今回の問題を個人環境で再現して後日やってみたいなと思います*3

あとは、DB レイヤの座学も進めたいところですね。長らく積読している「SQLパフォーマンス詳解」もちゃんと完走したいです。まだまだ RDB のことが何もわかっていないな、と。EXPLAIN の読解ももう少し場数を踏んで自分の手に馴染むようにしたいです。

色々と学びたいことは尽きませんが、こういう気持ちを抱けるのも Web アプリケーションの総合格闘技たる ISUCON というイベントだからこそです。改めて、主催のみなさま、開催ありがとうございました。ぜひ来年もよろしくお願いします。

*1:ちなみに、ISUCON では原則的にサーバーサイドがレスポンスの仕様を初期実装の仕様から変えることは許されませんので、SSE 対応のように例外がある場合はマニュアルに明記されます。ちなみに、SSE 対応は仕組みすらロクに知らないので競技中気付いたとしても私は対応できなかったと思います

*2:意味があったのかも含めて自信はありません

*3:と言いつつ、やらずに来年...のパターンを何度も繰り返しているわけですが。。。

橋本 拓弥(記事一覧)

マネージドサービス部

内製開発中心にやってます。普段はサーバーレス関連や CDK を触ることが多いです