ISUCON13の公式問題環境をWSL2へ構築し、Go参考実装を 12,136点から445,684点 まで改善しました。最終runはpass=trueで、公式ベンチの最終チェックにも成功しています。
この記事では、自作の計測ツールisutoolsを組み込み、SQL、HTTP、nginx access log、DB pool、CPU profileを同じベンチ区間で見ながら改善した過程をまとめます。数十万回呼ばれる短い処理、DBへの往復、JSON生成、静的に近いレスポンス、DNS攻撃への耐性を同じ公式scoreで評価し、局所的に速くても総合scoreが落ちた変更は戻すサイクルを繰り返しました。
POST /reset (X-Isutools-Run-Id 発行)
│
▼
./bench run --dns-port 1053 --enable-ssl
├── Go App (isutools SQL / HTTP / CPU pprof)
├── nginx (LTSV access log / Unix domain socket)
└── MySQL & PowerDNS (connection pool / index)
│
▼
POST /save?score=<score>&pass=<pass>
└── 同一 run_id で JSON / HTML / pprof / git revision を保存・比較スコアの推移
| 状態 | score | pass | 内容 |
|---|---|---|---|
| WSL構築直後 | 12,706 | true | 公式Go実装の起動確認 |
| 計測条件を固定したbaseline | 12,136 | true | 最終構成との比較起点 |
| 最終採用版 | 445,684 | true | 公式最終チェック成功(baseline比 36.72倍) |
make bench再現run | 441,439 | true | save・SCP・JSON検証まで完走 |
| pprof UI修正後の確認run | 433,468 | true | 同一PGO profile、機能確認用 |
各runのscore、pass、git revision、CPU profileを一緒に保存し、どのコミットでどのボトルネックが解消されたかを追跡できるようにしています。
計測環境
- Windows 11 / WSL2(Ubuntu 22.04.3 LTS)
- ISUCON13 Go参考実装(
876bd43a6f6f1048b8d341b2ab62d7afff01efd2) - nginx / MySQL / PowerDNS /
isupipe-go - 公式ベンチマーカー:
./bench run --dns-port 1053 --enable-ssl - 環境構築の参考: matsuu/wsl-isuconのISUCON13環境
管理画面は外部へ公開せず、WSL内のloopbackとSSHポートフォワーディング経由で閲覧しました。
計測の組み込みとベンチ区間の固定
まずアプリケーション側でSQLドライバとEchoミドルウェアをisutoolsへ接続しました。
db, err := sqlx.Open(isutools.SQLDriverName("mysql"), dsn)
echov4.Install(e)
e.Use(echo.WrapMiddleware(isutools.HTTP))nginxは専用LTSVアクセスログを出力し、systemd drop-inから保存先、profile mode、パス正規化規則を渡します。管理ポートは既存の19191との競合を避けて127.0.0.1:19196に設定しました。インデックスやキャッシュに手を入れる前に、まず計測のみを有効化した状態で公式ベンチが通ることを確認しています。
/resetが返すX-Isutools-Run-Idで公式ベンチ結果とisutoolsのスナップショットを対応付けます。高スループット時はアクセスログが大きくなるため、/saveで終了境界を確定して該当世代(generation)のログを回収する構成にしました。
- 公式ベンチ原本:
official-benchmark-<time>-<run-id>.json - isutoolsレポート:
<time>_<seq>_gen<generation>_<rev>_score<score>.json|html - CPUプロファイル:
cpu_<capture-id>.pprofとメタデータ
1. SQLの実行回数と実行計画に基づくインデックス追加
最初に確認したのは「1回あたりが最も遅いSQL」ではなく、実行回数 × 1回あたりの処理時間です。livecomments、reactions、reports、tags、reservation_slots、PowerDNSのrecordsなどに、実際のWHERE・JOIN・ORDER BY条件に合わせたインデックスを追加しました。同時にMySQL接続プールを50へ広げ、interpolateParams=trueでプリペアドステートメントの往復を削減しています。
インデックスは追加後に必ずEXPLAINのkey、rows、Using filesortと総合スコアを確認し、改善が見られないものは残さないようにしました。
2. N+1クエリの一括化とインメモリキャッシュ
次に、ユーザー・テーマ・アイコン、配信・タグ、コメント・リアクション・通報、NGワード、統計、予約枠をinitialize時に構築するインメモリ状態へ移行しました。
1件ごとに短いSQLを発行するN+1クエリは、単発では速く見えても数万〜数十万回重なるとDB接続とJSON生成を占有します。これらを一括取得SQLまたはインメモリ集計へ置き換え、読み取りAPIの不要なトランザクションも外しました。
一方で、予約枠をメモリのみで更新する変更は不採用としました。アプリ側の処理は軽くなったものの、ベンチマーカー側のDNS攻撃の並列度が6から9へ上がり、総合スコアが417,657点へ低下したためです。
3. アイコン配信のETag(HTTP 304)対応
最終スナップショットでは、GET /api/user/:username/icon の281,999リクエストのうち281,642件がHTTP 304で応答しています。アイコンのハッシュ値をETagとして返し、If-None-Matchが一致した場合はレスポンスボディを省略する構成です。
アイコンの更新処理もDELETE + INSERTからINSERT ... ON DUPLICATE KEY UPDATEへ変更し、同時更新時の競合を抑えました。
4. pprofによるJSONインデント処理の特定と削減
CPUプロファイルを確認したところ、JSONレスポンスを整形するインデント処理が累計21.36秒のCPU時間を消費していました。Echoのデバッグ出力を止め、コンパクトなJSONを返すように変更しています。
isutoolsではベンチマーク区間全体のCPUプロファイルを採取し、実行バイナリのSHA-256一致を確認した上で関数・ファイル・行番号ごとのCPU時間をレポートに出力します。レポート上部から「行解析結果」と「CPU pprofフレームグラフ(2,048ノード)」へ直接移動できるようにしました。
5. 実ワークロードのCPUプロファイルを使ったPGOビルド
GoはCPU pprofプロファイルをPGO(Profile-Guided Optimization)の入力として利用できます(Go公式のPGOドキュメント)。公式ベンチマーク区間から採取したプロファイルをそのままビルドへ渡しました。
go build \
-pgo=/home/isucon/isutools-data/cpu_<capture-id>.pprof \
-o isupipe .どのプロファイルを使ってビルドしたかをバイナリのビルド情報と一緒に記録しています。なお、GOAMD64=v3も検証しましたが、441,453点でPGO単体に対する有意な上積みが見られなかったため見送りました。
6. nginx・MySQL・PowerDNSの調整
アプリケーション以外では以下の設定を適用しました。
- nginxとGoアプリ間の通信をsystemd管理のUnixドメインソケットへ変更
- MySQLのバッファサイズとフラッシュ設定(
innodb_flush_log_at_trx_commit=2)を調整 - PowerDNSの過剰なログ出力を停止し、
records検索用インデックスを追加
検証して不採用にした変更
局所的な指標が改善しても、公式ベンチの総合スコアが下がった変更はロールバックしました。
| 変更 | 結果 | 判断 |
|---|---|---|
| PowerDNSのキャッシュ・スレッド数増加 | スコア低下 | DNS攻撃の負荷増加により不採用 |
| 予約枠のインメモリ更新のみ化 | 417,657 | DNS攻撃の並列数が6→9へ増えスコア低下 |
| JSONのgzip圧縮(level 1) | 424,767 | 転送量は約1/4.3に減ったがCPU負荷が増え不採用 |
GOAMD64=v3 ビルド | 441,453 | PGO(v1)に対する優位性を確認できず不採用 |
制御PCからのベンチマーク実行(make bench)
再現用Makefileでは、ホスト固有の設定を別ファイルへ分離しています。
REMOTE_HOST = user@example-host
WSL_DISTRO = isucon13
WINDOWS_USER = your-windows-user
RESULTS_DIR = $(HOME)/isutools-isucon13-results
LOCAL_PORT = 19197make check
make bench
make tunnel
# http://127.0.0.1:19197/make bench1コマンドで、サービスの起動確認、公式ベンチマーク実行、結果JSONの検査、isutoolsへの保存、SCPでの手元PCへの回収までを実行します。
まとめ
12,136点から445,684点までの改善で軸にしたのは、次の6つのステップです。
- 公式ベンチマーク結果とプロファイラの計測値を同じ
run_idで保存する - SQLは単発の実行時間だけでなく、実行回数を含めた累計時間で優先順位を決める
- N+1クエリとDB往復を一括SQLおよびインメモリ状態へ置き換える
- ETag、コンパクトJSON、Unixドメインソケットで高頻度リクエストの小さなコストを削る
- ベンチ区間のCPUプロファイルでホットスポットを特定し、同じプロファイルをPGOへ渡す
- 局所指標が改善しても、総合スコアが落ちた変更は履歴を残して戻す