ISUCON2026に参加した。チーム名「ブランド・ニュー・炉端焼き」。
メンバーは3人で、実装言語はGoを選んだ。

前回の参加記録はこちら。

» ISUCON13に参加して敗退した

今回の問題

題材はISUReal。つまりBeReal。

  • 利用者はasia、europe、americaのいずれかのリージョンに所属する
  • 管理者がリージョンごとに投稿期間を開始すると、SSEで通知が届く
  • 利用者は通知を受けたら自分の写真を投稿し、友達の写真を閲覧する
  • 投稿にはコメントやRealMoji(画像を使ったリアクション)を付けられる
  • 全リージョンの利用者が椅子の写真を投稿する「世界グランプリ」が開催される

前回参加したISUCON13のDBはMySQLだったが、今回はPostgreSQL 16だった。
サーバ構成も特徴的である。

  • asia1、asia2, europe1、america1、の4台
  • 実際にはAWSのリージョンが分かれているのではなく、ルータサーバーがあり、仮想的に通信遅延、帯域制限が入る構成
  • ルーターへのログインや設定変更は禁止
  • ベンチマークの対象は全台

europeとamericaは、asiaよりもDBまでの通信が遅い。
サーバ内の処理を速くするだけでなく、リージョン間の往復回数を減らす必要があった。
(後で分かったこと)

本番前の準備

過去の練習の知見をまとめた

ISUCON14の問題で事前練習していたので、その時Claude Codeと進めたセッションの内容から、問題に依存しない一般的な知見(計測の進め方、定番の施策、失敗例)をMarkdownに書き出した。
本番用の新しいセッションが最初に読む資料とした。
過去のISUCONで共有してきたドキュメントも読ませた。以前はRubyで参加しており、Ruby用のドキュメントになっていたのでGoに読み替えた。

GitHub Actionsのself-hosted runnerの導入手順を用意した

GitHub Actionsのself-hosted runnerでCI/CDできるように準備した。

本番

開始直後

CloudFormationでスタックを作成し、リポジトリにアプリとSQLを取り込んだ。
続いて、デプロイとベンチのworkflowを用意し、計測用のworkflowを整備した。

MySQLのつもりで準備していたのが蓋を開けてみるとPostgreSQLだったので分析系のツールを切り替えた。

  • PostgreSQL: pgbadger
  • nginxのアクセスログ: kataribe

参考:kataribeでnginxのアクセスログを解析してボトルネックを探す 、pt-query-digestでMySQLのスロークエリを集計する (MySQL版)

最初のベンチは3925点だった。

ここから開始。

主にやったこと

DBコネクションを再利用する

MaxIdleConnsが既定の2のままで、リージョン間遅延の大きいamericaとeuropeでコネクションの張り直しが頻発していた。
SetMaxOpenConns(30)とSetMaxIdleConns(30)にして、PostgreSQLのmax_connectionsを200に上げた。
スコアは9095点から12492点に上がった。

通信回数を減らす

feedや投稿詳細など、最も使われる経路のloadViewerContextを5クエリから2クエリにまとめた。
europeとamericaのfeed中央値は約0.93秒から約0.51秒に縮んだ。

ログインや友達申請の往復をpgxのbatchで1回にする対応も入れたが、中央値はほとんど変わらなかった。

asiaを2台で処理する

asia2はDB専用だがCPUに余裕があったため、asia2でもアプリを動かしてasia1と振り分けた。
同じ構成で複数回計測すると、分散なしが11723〜12839点、分散ありが11506〜14113点で、構成による差よりも同じ構成内のばらつきの方が大きかった。
分散するとasia2のCPUが飽和するため、重みをasia1:asia2 = 2:1にした。
また、ベンチマークは意図的に不正なログインや画像を送り、サーバ上のisureal-guardが失敗を数えてIPをBANする。
asia2にもisureal-guardを入れ、nginxでX-Client-IPをハッシュして同じIPを同じ台に固定した。
最終的には、asia1だけで受ける構成に戻し、asia2はDB専用にした。

SSEをLISTEN/NOTIFYにする

通知のSSEがクエリ全体の約38%を占めていた。
ポーリングを1クエリにまとめたあと、PostgreSQLのLISTEN/NOTIFYに置き換えた。

画像まわり

アプリの実行がリソースを占有していたのでpprofでプロファイルを取得し、ボトルネックを特定した。
その結果、画像URLの署名の計算がボトルネックになっていることが分かったので対応した。

  • 画像URLの署名のHMACを使い回して計算を約2.5倍速くした
  • 署名の計算結果をキャッシュした
  • 投稿時の画像検証でJPEGの全デコードをやめ、DecodeConfigとマーカーの確認にした

JPEGのデコードをやめたことで24221点から35432点に上がった。

その他

  • ユーザー検索の絞り込みをSQLに移した
  • feedの投稿取得を作者で絞り込み、全件ループをやめた
  • エクスポートの画像取得をキャッシュ経由にした

スコア推移

benchワークフローを53回実行した。
主な実行は以下の通り。

時刻スコア主な変更
11:203802計測用のworkflowが動いた最初のPASS
11:513925計測の基準
12:3610130ロック解消と、デプロイの修正
13:0612492DBコネクションの再利用
14:2920748画像署名の高速化、画像APIのクエリ統合
15:5824221SSEのLISTEN/NOTIFY化、ユーザー検索、署名のキャッシュなど
16:1235432JPEGの全デコードをやめる
16:170画像URLの期限の変更でFAIL
17:2940836ピーク(ログインと友達申請の往復削減を含む)
17:3925351ログ出力と計測の停止
17:4517292GitHub Actionsでの最後のベンチ

うまくいかなかったこと

失敗

  • ユーザー検索のトライグラム索引は、スコアが落ちたためrevertした。revertしたがDBには拡張、列、索引が残っていたことに気づかず別の施策のスコア上昇を一時的に判定できない状態になった。
  • PostgreSQLのメモリ設定(shared_buffersなど)は、効果が見えずrevertした。ただし、後述の通りスコアが安定していない時期に入れたものなので、実は効いていた可能性もある。

スコアが運に左右される

同じコードを2回ベンチして、40836点と17495点に割れた。 他にも32810点と16900点、29778点と18462点のようにスコアが大きく割れた。

asia人数(1〜5日目)スコア
18 31 82 179 27640836
18 18 18 18 2317495

スコアは、負荷走行中に増えるasiaの利用者数でほぼ決まるように見えた。
増える条件は最後までわからなかった。

終盤

夕方ごろからベンチごとのばらつきが大きく、施策の効果を測りにくくなって判断が難しかった。

ラスト15分ではログの出力をやめて計測用のジョブも外した。

  • nginxのaccess_log off
  • PostgreSQLのlog_min_duration_statement = -1
  • pprofを削除

あと最後までRedisを使わなかったのでRedisのサービスを停止した。

次回に向けて

goでの参加は初だったのでpprofを入れるタイミングが遅れた。 今後は、最初からpprofを組み込んでおいたほうがよいと思った。