Repository navigation
Conversation
BattleProcessor.ValidateUsedTxId looks up battles by tx_id twice per battle, and no index starts with tx_id, so each lookup seq-scanned the whole battles table. The index already exists in production (created manually with CREATE INDEX CONCURRENTLY), so the migration uses CREATE INDEX CONCURRENTLY IF NOT EXISTS to stay idempotent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
배경 (2026-10-08 장애)
odin / heimdall 아레나 DB(
odin_arena_db,heimdall_arena_db)의 디스크 읽기가 약 8000 blocks/s까지 올라갔다. 원인은battles테이블(약 950MB)을 배틀 1건당 약 2번씩 풀 seq-scan 한 것이다.BattleRepository.GetBattleByTxId→Battles.SingleOrDefaultAsync(b => b.TxId == txId && b.Id != battleId)BattleProcessor.ValidateUsedTxId에서 배틀 1건당 2번 호출된다.tx_id로 시작하는 인덱스가 없었다. 기존ix_battles_id_tx_id (id, tx_id)는 선두 컬럼이id라서 이 조회에 쓰이지 않는다.수정
Battle모델에[Index(nameof(TxId))]를 추가했다. snake_case 규칙에 따라ix_battles_tx_id가 생성되고, 이름은dotnet ef로 확인했다.dotnet ef migrations add로AddBattleTxIdIndex를 생성했다. 모델 스냅샷에는 인덱스 1개만 추가됐다.CreateIndex/DropIndex는 멱등 SQL로 바꿨다.CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_battles_tx_id ON battles (tx_id);(suppressTransaction: true)DROP INDEX CONCURRENTLY IF EXISTS ix_battles_tx_id;(suppressTransaction: true)프로덕션 상태
odin / heimdall 에는 이미 수동으로 만들어 두었다 (2026-10-08,
CREATE INDEX CONCURRENTLY ix_battles_tx_id ON battles (tx_id);). 그 뒤로 디스크 읽기는 약 8000 → 약 10 blocks/s 로 떨어졌다. thor 에는 아직 만들지 않았다.dotnet ef database update로 수동 적용한다.IF NOT EXISTS때문에 인덱스는 건너뛰고__EFMigrationsHistory에 기록만 남는다.CONCURRENTLY로 만들기 때문에 테이블 쓰기를 막지 않는다. 그래서 테이블 크기와 무관하게 안전하다.검증
dotnet build ArenaService.sln: 오류 0dotnet test: ArenaService.Tests 39/39, ArenaService.BackOffice.Tests 44/44 통과dotnet ef migrations has-pending-model-changes: "No changes have been made to the model since the last migration."dotnet ef migrations script "20250620050806_Add RoundIndex": CREATE INDEX 문은 트랜잭션 밖에서 실행되고, 그 다음 트랜잭션에서 history INSERT 가 실행되는 것을 확인했다.리스크
CONCURRENTLY빌드가 도중에 실패하면 INVALID 인덱스가 남는다. 그 상태로 다시 실행하면IF NOT EXISTS가 그 인덱스를 건너뛰므로, 적용 전후에pg_index.indisvalid를 확인해야 한다. 프로덕션에 수동으로 만든 인덱스도 valid 인지 확인할 것을 권장한다.tx_id컬럼 하나짜리 인덱스라 부담은 작다.후속 (이 PR 범위 밖)
ix_battles_battle_status_reviewed와ix_battles_id_tx_id는 쓰이지 않거나 중복으로 보인다.id가 PK 라서(id, tx_id)는 PK 와 기능이 겹친다. 프로덕션에는 아직 남아 있으므로 이 PR 에서는 삭제하지 않았다.pg_stat_user_indexes로 확인한 뒤 별도 PR 에서 정리한다.🤖 Generated with Claude Code