ISUCON2026 参加記:3,450 点 → 59,430 点(279 位)。AI エージェント 3 体と、地域をまたぐ 100ms と戦った 8 時間

ISUCON2026 にチーム「復旧お疲れ様でした」として参加しました。最終スコアは 59,430 点、279 位 でした。実装は Go、DB は PostgreSQL です。

人間 1 人と AI エージェント 3 体(Codex・Claude Code・Antigravity)で改善を進めました。ただ、思っていたようには進まず、時間の使い方にも課題が残りました。この記事では、実際に入れた実装と計測した結果をまとめます。認識が間違っているところがあれば、ぜひ教えてください。

時刻つきのスコア記録をもとに、次の 4 つを振り返ります。

  • 何をして 3,450 点から 59,430 点まで来たか
  • 何で転んだか(DB の OOM と復旧)
  • 上位チームとどこで差がついたか
  • AI エージェントを 3 体動かして分かったこと

1. 体制と事前準備

役割 担当
人間 方針の決定、採否の最終判断、エージェント間の調整
Codex 進行管理、デプロイ、ベンチ、合否の判定、記録
Claude Code 原因の調査(pprof・E​X​P​L​A​I​N・pg_stat_statements)、難しい実装、手順書づくり
Antigravity 手順書どおりの作業、コードの見直しと提案

initialize の申告は、agent・harness を codex,claude-code,antigravity、model を gpt-6-sol,claude-opus-5-5,gemini4-argon にしました。

事前にやっておいたのは次の 3 つです。

  • 練習の記録を「型」として書き出す。 ISUCON14 の練習記録を、エージェントが読む指示ファイル(CLAUDE.md)にまとめました。「計測してから変える」「1 回に 1 つ変える → ベンチ → 記録 → その場でコミット」「下がったらすぐ revert」といった進め方のルールも入れています。
  • Makefile を本番の接続先に合わせる。 deploy・status・envcheck を 1 コマンドで流せるようにしました。
  • タスク管理を Beads(bd)に寄せる。 エージェントのセッションが切れても、発見や続きの作業が残るようにしました。

2. 問題:isureal(地域をまたぐ写真共有アプリ)

  • 毎日の「投稿期間」の通知(SSE)が来たら、写真を投稿します。RealMoji・コメント・Memories(月や日ごとの振り返り)・エクスポートがあります。
  • 台は 4 つです。asia1・europe1・america1 がアプリ、asia2 が PostgreSQL です。ルーターが X-Country-Code で地域の台に振り分けます。
  • 地域間には遅延と帯域制限があります。 europe1・america1 から asia2 の DB までは往復約 100ms でした。asia1 から asia2 は約 1ms です。
  • スコアには地域の倍率があります。 asia ×1、europe・america ×1.5 です。
  • 画像の URL には署名が付きます。 署名は HMAC-SHA256 を 1000 回重ねる計算です。投稿画像は平均約 515KB の JPEG で、壊れた JPEG は 400 で弾く必要があります。
  • 台の種類が違います。asia1・asia2 は c5.large、europe1・america1 は c7i.large でした。後から効いてくる大事な違いです。

3. スコアの推移

時刻 主な変更 スコア
10:17 初期状態 3,450
10:30 DB 接続(sslmode=disable、アイドル接続を 30 に) 6,677
10:47 セッション認証を台ごとにキャッシュ(他の台の変更は 200ms ごとに DB を見て無効化) 11,828
11:37 署名の HMAC を毎回作り直さず、Reset で使い回す 16,440
11:57〜12:07 関係表の逆引きインデックス(19,682)、閲覧者の情報を 5 往復から 1 クエリに(19,891)、友達の友達の検索を OR から UNION に 24,230
12:59 投稿作成を 1 文にまとめる(往復を削る) 28,543
13:19 europe・america の API を DB に近い台(asia1・asia2)で動かす 34,258
13:42〜14:05 asia2 の OOM で FAIL が 3 回、PostgreSQL の復旧 0 → 31,989
14:36 投稿の JPEG 検証を europe1・america1 で済ませてから転送 42,724
15:58 Memories を europe1・america1 で返す 48,679
16:17 画像 URL の署名を 30 秒使い回す 50,856
16:37 インデックス漏れ 6 つを追加 56,634
16:39 画像の本体を europe1・america1 から返す 58,498
17:00 再起動試験の後、変更なしで再走 59,895
17:56 投稿作成のロック順の修正後、最後の走行 59,430

4. 効いた施策

4.1 「どこで処理するか」を変える(いちばん効いた)

最初は europe・america のリクエストも、それぞれの台から asia2 の DB を直接見ていました。全 API の応答が 100ms の倍数で、DB を何往復するかがそのまま遅さになっていました。11:51 の計測では、europe のフォロー 2.5 秒、コメント 1.77 秒、投稿 0.84 秒でした。

往復を 1 つずつ削る作業(ログイン 7 → 4 往復、友達申請 約 10 → 5 往復など)も地道に効きました。ただ、決定打は置き場所を変えたことでした。

  • 13:08 europe1 の nginx で /api を asia1 上の europe 用プロセスへ転送しました。地域をまたぐのは nginx から asia1 への 1 往復だけになり、europe の人数は最大 36 人から 64〜66 人に増えました。
  • 13:19 america 用プロセスは asia2(DB と同じ台)に置きました。asia1 に 3 プロセス目を置いたら CPU を取り合って europe が落ちたので、台を分けました。

ところが、これで asia1 と asia2 に処理が集まり、両方とも CPU が 100% に張り付きました。そこで午後は、重い処理の一部を、空いている europe1・america1 へ戻す作業になりました。

  • 投稿の JPEG 検証(14:30〜14:36)
  • Memories(15:50〜15:58)
  • 画像の配信(16:39)

最後に分かった損得の基準は単純です。地元の台で返して得をするのは、DB を 1 往復で済ませられる API だけでした。コードを数えると DB を 2〜4 回続けて往復するフィードを地元に戻したら、Europe の 5 日目の人数が 232 人から 124 人に半減しました(16:43 に反映、16:48 に revert)。

4.2 SHA-NI の有無:署名の計算は台の種類で 7 倍違う

pprof を取ると、asia1 の CPU の半分以上が画像署名の SHA-256 でした。同じ計算でも、SHA-NI 命令のない c5.large は 1 件 1.07ms、ある c7i.large は 0.14ms でした(自分たちの計測)。

やったことは 4 つです。

  1. HMAC のオブジェクトを使い回す(11:37、+20〜30%)
  2. 同じ (鍵, 画像, exp) の署名 URL を、同じ秒の間だけ使い回す(11:54)
  3. 使い回しの幅を「同じ秒」から「30 秒以内」に広げる(16:17、48,679 → 50,856)。120 秒にしたら下がったので戻しました
  4. 署名の重い Memories を c7i 側で返す(15:50〜)

4.3 インデックス漏れは pg_stat_user_tables で探す

16:00 過ぎ、pg_stat_user_tables で全件走査(seq scan)の回数を見て驚きました。

  • login_sessions:seq scan 約 38 万回、i​n​d​e​x scan 0 回
  • users:seq scan 約 19.7 万回(延べ約 10 億行)

セッションの確認と、5 プロセスが 200ms ごとに回すセッションのポーラーが、毎回全件を読んでいました。まず login_sessions(token_hash)・(user_id)・(invalidated_at) と users(updated_at) の 4 つを付けました(この回は 46,493 点)。続けて、投稿作成の副問い合わせ用に posts(author_id, posting_window_id) と posting_windows(local_date) も付けました。6 つ全部入れた回で 56,634 点(それまでの最高 50,856 点から +11%)になりました。

教訓:最初の 1 時間で見るべきでした。 スロークエリの上位には出てこない種類の漏れでした。

4.4 画像を地元の台のキャッシュから返す

Codex にとってもらった計測(#28281)では、america の画像取得は平均 0.50 秒かかっていました。本体(数百 KB)が帯域制限のある回線を毎回通っていたためです。

画像の取得はセッションを使わず、署名と DB の可否判定だけで答えられます。そこで europe1・america1 のローカルで返すようにしました。可否判定は毎回 DB に聞くので、「削除後 5 秒を過ぎたら取れない」という規則は守れます。本体はその台のメモリとディスクから返します。この 1 行の nginx 設定で 58,498 点になりました。

4.5 JPEG 検証の分担

投稿画像の全画素デコードは、c5 で 1 枚 239〜848ms かかりました(負荷により変動)。europe・america の投稿は、地元の c7i で先に検証し、検証済みの印を付けて asia1・asia2 へ転送する形にしました。転送先でのデコードは 0 回になりました。

5. 事故と復旧

13:42〜14:05、asia2 の OOM。 europe・america の投稿を地元で受ける実験の途中で、asia2(メモリ約 4GB)で America 用アプリ(RSS 約 1GB)と PostgreSQL が順に OOM kill されました。

  • FAIL が 3 回続きました。原因はそれぞれ違います。 – 1 回目(#14032):America 用アプリの OOM – 2 回目(#14664):PostgreSQL の OOM(接続が切れ、recovery mode の 500) – 3 回目(#14878):PostgreSQL が deactivating (stop-sigterm) のまま固まり、initialize が通らない
  • SIGQUIT で止めて WAL から復旧し、主要テーブルの件数と envcheck を確かめてから再開しました。
  • 以降は America 用アプリに GOMEMLIMIT=512MiB を付けて、再発を防ぎました。

後で他チームの記事を読むと、asia2 の PostgreSQL の OOM は、確認できた範囲でも 学生 2 位のチーム と 135 位のチーム で起きていました。メモリの少ない DB の台にアプリを相乗りさせるときの落とし穴だったようです。

他にも、次のことを試して戻しました。

  • 3 プロセスで CPU を取り合った(13:13)
  • 署名の使い回しを 120 秒にした(16:20)
  • asia1 で asia 用プロセスの CPUWeight を上げた(16:03)
  • フィードと検索を地元で返した(16:43〜16:48)

下がったら 1 回で revert を守れたのは良かった点です。

6. 最後まで分からなかった「asia が 18 人で止まる」問題

午後のほとんどのベンチで、asia の人数が 1 日目の 18 人から増えませんでした。ベンチの出力には「グランプリ: アジア 0 人」という回すらありました。asia1 の CPU の取り合いを疑って優先度を上げましたが、変わりませんでした。

原因が分かったのは 17:47 です。投稿作成のトランザクションが、投稿期間の行に共有ロック(FOR SHARE)を持ったまま、500KB の画像を I​N​S​E​R​T していました。そのため、期間を開く U​P​D​A​T​E が待たされ、asia の新しい投稿期間がなかなか開かず、タイムアウトや 409 になっていました。

画像の I​N​S​E​R​T をロックを取る前の文に分けて直し、最後の走行(59,430 点)に間に合わせました。同じトランザクションの中なので、409 のときは画像もロールバックされます。後で読んだ 135 位のチームの記事 にも、画像の I​N​S​E​R​T をロックの前に移す近い修正がありました。

反省: ベンチの「地域ごとの人数」と「グランプリの人数」を毎回表にして、異常を数字で追えばもっと早く気づけたはずです。CPU の取り合いという仮説を、一次情報(ロック待ち)で確かめずに試してしまいました。

7. 上位チームとの差

参加記を 10 本ほど読んで、20 万点前後のチームと自分たちの差を整理しました。上位のチームは、ほぼ次の 4 つを入れていました。

  1. データを各地域の台に複製し、読み取りは地元で返す(自分たちは、DB に近い台へ転送するところで止まった)
  2. 画像の本体を DB の外(ファイル・mmap など)に出す(自分たちはキャッシュ止まり)
  3. 署名を SIMD で並列に計算し、初期画像の分を事前に計算し、期限を丸める(自分たちは 30 秒の使い回しまで)
  4. JPEG 検証を、画素を復元しない方式にする

4 は判断が分かれたところです。自分たちは「DecodeConfig にすると中身だけ壊れた JPEG を通してしまうので、仕様違反」と判断して見送りました。

  • 優勝チーム は DecodeConfig を採用していました。
  • 学生 2 位のチーム は自分たちと同じ理由で見送り、標準ライブラリを複製した専用の検証器を作っていました(競技後に手元で測って 27.6ms → 1.15ms とのこと)。

「見送る」ではなく「仕様を守ったまま速くする」まで踏み込むべきでした。

全体として、自分たちは「DB 往復を減らす」「重い処理を空いている台へ移す」までを積み上げた形でした。上位のチームは早い段階で「この延長線上に上の点数はない」と見切り、構成そのものを変えていました。

8. AI エージェントを 3 体動かして分かったこと

良かったこと

  • 実装・反映・判定を分けられました。「1 回に 1 つ変える → ベンチ → 記録 → コミット」を 8 時間守れたのは、記録係のエージェントがいたからです。
  • 戻り先のコミットを常に 1 つ決めておけました。 終盤の「試して、下がったら戻す」を、1 回 3〜4 分で回せました。
  • 片付けや再起動試験は手順書にして渡せました。トークンの安いエージェントに任せられました。

気をつけたこと・失敗

  • エージェントの「有望な案」には、仕様違反の案や読み違いが混ざっていました。例えば「JPEG 検証を DecodeConfig にすれば一発」「CPUWeight の結果から CPU の取り合いが証明された」(ぶれを読み違えていた)といった案です。pprof・E​X​P​L​A​I​N・pg_stat_statements・ベンチの出力で確かめてから採用しました。
  • 計測を外したり戻したりするときに、別のエージェントの未コミットの変更とぶつかりかけました。サーバーを操作する担当は 1 人に絞るべきでした。 10 以上のセッションを同時に動かしたチーム も、途中でデプロイの担当を 1 つに決めたそうです。
  • 自分の書いたシェルの $? が $(hostname) で上書きされ、「ルーターに届いている」と一瞬誤読しました。終了前の確認ほど、コマンドの取り違えに気をつけたいところです。

9. 終盤の運用

  • 16:11 に片付けのリハーサルをしました。pprof と pg_stat_statements を外す → 4 台を再起動 → サービスの自動起動とデータの一致を確認 → envcheck → ルーター迂回が無いことを curl で確認(Timeout になれば OK)→ ベンチ、の順です。
  • 16:43 と 16:57 に本番の手順を流しました。
  • 17:00 にリーダーボードは止まりますが、最終スコアは 18:00 までの最後の走行です。 17:00 以降も粘り、asia の問題を直した版を最後の走行にしました(59,430 点)。点数だけなら 17:00 の 59,895 点のほうが高く、ぶれの範囲の差でした。

10. 次回への持ち越し

  • 最初の 1 時間で、台ごとの CPU 特性(SHA-NI など)と、pg_stat_user_tables の seq scan を確認する
  • ベンチの地域別の人数を毎回表にして、伸びない地域を最優先で調べる
  • 「地元で返す」を、API ごとの DB 往復回数から事前に見積もる
  • 仕様のグレーな箇所は、「見送る」の前に「仕様を守ったまま速くする」方法を考える
  • AI エージェントには、サーバー操作の担当を 1 人だけにする。判断材料(一次情報)を必ず添えさせる

おわりに

8 時間、毎回 100ms の往復を数え続けた大会でした。運営のみなさま、楽しい問題をありがとうございました。そして、OOM から復旧してくれた自分たち(とエージェントたち)、お疲れ様でした。