| name | applying-isucon-arch-isunarabe-rev2 |
| description | Applies the REV2 architecture redesign in docs/isucon-arch/DESIGN-REV2.md to the isunarabe-train-2026 (nrb2026) implementation. REV2 is layered on top of REV1 (applying-isucon-arch-isunarabe) — REV1 must already be merged. Use when the REV1 dispatcher is complete and Grafana shows DB CPU 100% / pool wait dominance is the new bottleneck. |
Applying ISUCON Architecture REV2: isunarabe-train-2026 (nrb2026)
docs/isucon-arch/DESIGN-REV2.md (2026-05-09) の決定 — P0: bench から slow log を外す + POST /join 早期 409 + last_joined_at 単純 index / P1: GetCampaigns JSON_ARRAYAGG 1 SQL 化 + per-app user cache / P2: nginx weight + webhook fan-out 並列度 — を migrate/go/ と app/nginx/ と ansible/remote/Makefile に適用する。
REV1 (applying-isucon-arch-isunarabe) が既に main に merge 済であることが前提。REV2 はその上の delta レイヤ。
各 Phase は 検証ステップを passed させてから次へ進む。実態が DESIGN-REV2 と矛盾したら止まり、docs/isucon-arch/DESIGN-REV2.md を直してから designing-isucon-architecture を再実行して本 skill を再生成する。現場で勝手に逸脱しない。
Pre-flight
適用順 (依存順)
A. DEPLOY-FAST: ansible/remote/Makefile の bench 依存から slow-on を外す (slow log OFF を強制)
↓ [これだけで isu3 disk I/O が劇的に減る → bench 1 走で確認可]
B. ENDPOINT-LIST-REWRITE: schema (last_joined_at seed + index) + INSERT seed + GetCampaigns 単純 ORDER BY
↓
C. ENDPOINT-JOIN-EARLY-409: POST /join pre-tx で current_count >= goal_count 即 409
↓
D. CACHE-USER: per-app user cache (TTL 200ms) 構造体 + warmup フック
↓
E. ENDPOINT-ME-CACHE: GetMe を cache 経由 + POST /join commit 後 invalidation
↓
F. ENDPOINT-WEBHOOK-FANOUT: sendWebhooks の sem 16 → 64
↓
G. DEPLOY-NGINX-WEIGHT: upstream に weight=2 (isu1) / weight=3 (isu2)
↓
H. REBOOT TEST (final)
A だけは設定変更で git 1 行、isucon の機材を一切触らずに DB I/O 半減できる。最優先。
Phase A — bench から slow log を外す (1 commit)
references/DEPLOY.md §A 参照。
ansible/remote/Makefile の bench: ターゲット依存に含まれる slow-on を slow-off に倒す (現状 bench: ... slow-on で全クエリログ有効)。pt-query-digest を取りたい時用に bench-digest: 等を別ターゲットで残す。
期待: isu3 disk_io_time 2,704% → 数百%、CAS UPDATE 130ms → 30ms 級。
Phase B — GetCampaigns の filesort 排除 (1 commit)
references/SCHEMA.md と references/ENDPOINTS.md §#2 参照。
migrate/go/sql/schema.sql に index 追加: CREATE INDEX idx_campaigns_lja ON campaigns (last_joined_at);、CREATE INDEX idx_campaigns_ca ON campaigns (created_at);。
migrate/go/handlers/campaigns.go の PostCampaigns の INSERT に last_joined_at を加え、created_at と同値で seed する (NULL を捨てる)。
migrate/go/handlers/init.go (or cache.go の WarmCaches/PostInitialize) に UPDATE campaigns SET last_joined_at = COALESCE(last_joined_at, created_at) を追記 (REV1 派生列 seed UPDATE と同じ場所)。
GetCampaigns の sort SQL を ORDER BY COALESCE(last_joined_at, created_at) DESC から ORDER BY last_joined_at DESC に単純化。
EXPLAIN 検証:
mysql> EXPLAIN SELECT id, ... FROM campaigns WHERE current_count < goal_count ORDER BY last_joined_at DESC LIMIT 30;
type=index, key=idx_campaigns_lja, Extra=Using where; Backward index scan
Phase C — POST /join 早期 409 (1 commit)
references/ENDPOINTS.md §#1 参照。
migrate/go/handlers/campaigns.go の PostCampaignsIDJoin の pre-tx ブロックを 1 行修正:
if err := h.DB.GetContext(ctx, &cr,
"SELECT goal_count, price FROM campaigns WHERE id = ?", campaignID); err != nil { ... }
var cr struct {
GoalCount int32 `db:"goal_count"`
Price int32 `db:"price"`
CurrentCount int32 `db:"current_count"`
}
if err := h.DB.GetContext(ctx, &cr,
"SELECT goal_count, price, current_count FROM campaigns WHERE id = ?", campaignID); err != nil { ... }
if cr.CurrentCount >= cr.GoalCount {
return echo.NewHTTPError(http.StatusConflict)
}
期待: CAS UPDATE 試行 13,845 → ~4,000、UPDATE campaigns ... CAS の sum 1,805s → 500s 級。
Phase D — per-app user cache 実装 (1 commit)
references/CACHE.md §user 参照。
migrate/go/handlers/cache.go に userByID: sync.Map (or map[string]userCacheEntry + sync.RWMutex)、GetUser(id)/SetUser(...)/InvalidateUser(id)/ResetUsers() を追加。
- TTL は 200ms (固定、sticky routing なしで multi-app stale を抑制)。
- WarmCaches で users 全件 SELECT は不要 (10 万件級になりうる) — lazy fill のみ。Reset only。
PostInitialize で userByID.Reset() を呼ぶ。
Phase E — GET /api/me rewrite + invalidation (1 commit)
references/ENDPOINTS.md §#3 参照。
migrate/go/handlers/users.go の GetMe:
Caches.GetUser(userID) → hit & 期限内なら即 return。
- miss/expired →
SELECT name, credit_limit, credit_used FROM users WHERE id = ? → cache fill → return。
migrate/go/handlers/campaigns.go の PostCampaignsIDJoin の commit 後 に Caches.InvalidateUser(userID) を呼ぶ (caller のみ。約定経路で他 user の credit_used を変えた場合はそれぞれを invalidate; TTL 200ms に頼って省略可)。
Phase F — webhook fan-out 並列度 (1 commit)
references/ENDPOINTS.md §#4 参照。
migrate/go/handlers/campaigns.go の sendWebhooks の sem := make(chan struct{}, 16) を 64 に。1 行修正のみ。
Phase G — nginx upstream weight (1 commit)
references/TOPOLOGY.md 参照。
app/nginx/sites-available/nrb2026 の upstream app ブロックに weight 追加:
upstream app {
server 192.168.0.11:8080 weight=2; # isu1 (nginx 同居)
server 192.168.0.12:8080 weight=3; # isu2
keepalive 64;
}
Phase H — Reboot Test (最終)
references/DEPLOY.md §H 参照。
make access-off slow-off maji で本走行スコア取得。
sudo reboot × 3 を順に実施。
- SSH 復帰確認後、再度
make bench。スコアが maji 直前と ±5% 以内であれば合格。
Rollback
- 各 Phase は 1 commit。
git revert <sha> で個別に戻せる。
- DDL は
CREATE INDEX のみで drop しないので前進方向のみ。/initialize 1 回で last_joined_at 派生は再計算される。
- nginx weight は
git revert で書き戻し → make -C ansible nginx-replace nginx-restart。
Reference Files
- DEPLOY.md — Makefile の bench 修正、reboot test 手順。
- TOPOLOGY.md — nginx upstream weight 変更点。REV1 の topology は据え置き。
- SCHEMA.md —
last_joined_at index と seed UPDATE。
- CACHE.md — per-app user cache 構造体と TTL 規則。
- ENDPOINTS.md — 4 エンドポイントごとの before/after コードスケッチ + 検証。