# 운영 Runbook ## 배포 전 체크 ```bash ./gradlew verifyCleanArchitectureDependencies --console=plain ./gradlew verifyRuntimeModuleMembership --console=plain ./gradlew checkstyleMain --console=plain ``` destination profile은 startup에서 검증된다. 아래는 **부팅 실패**다. - ordered destination + reorder 가능 retry - `ordering=KEY` + key resolver 없음 - payload 상한 > 8,388,608 bytes - DLQ 자기 참조 / retry 자기 참조 - retry·DLQ 그래프 cycle - 미등록 retry·DLQ destination - M1 destination + manual settlement - `AT_LEAST_ONCE` + confirmation `NONE` - production profile + topology auto-create - broker topology가 manifest와 불일치 ## 증상별 대응 ### publish가 AMBIGUOUS로 쏟아진다 broker confirm 경로 문제다. 실패가 아니다. 1. `PublishEvidence.transmission`이 `MAY_HAVE_BEEN_TRANSMITTED`인지 확인 2. Kafka: `delivery.timeout.ms`, ISR 상태, leader election 확인 3. Rabbit: confirm timeout, channel 상태 확인 4. Outbox를 쓰고 있다면 `status='AMBIGUOUS'` row가 같은 messageId로 재시도 중이다. **정상이다.** 5. consumer 쪽 Inbox가 중복을 흡수하는지 확인 `AMBIGUOUS`를 실패로 취급해 새 messageId로 재발행하지 말 것. 중복이 복구 불가능해진다. ### DLQ가 비어 있는데 메시지가 사라졌다 DLQ publish 실패 시 source는 settlement되지 않는다. 메시지는 source에 남아 재전달된다. 1. `msg.failure-code`가 `DEAD_LETTER_*`인 로그 확인 2. DLQ destination이 실제로 존재하는지 (topology validation) 3. DLQ credential에 publish 권한이 있는지 ### consumer lag이 한 partition에서만 증가한다 `ContiguousPartitionOffsetTracker`가 gap에서 멈춘 것이다. 설계된 동작이다. commit은 **연속** 완료 offset까지만 전진한다. offset 11이 아직 실행 중이면 10과 12가 끝나도 watermark는 10에 머문다. 12를 commit하면 consumer가 죽었을 때 11을 잃는다. 1. 해당 partition의 in-flight를 확인 2. 느린 handler를 찾는다 (`handlerTimeout` 초과 여부) 3. 필요하면 `PAUSE_PARTITION` retry가 걸려 있는지 확인 ### 재시도 폭풍 `RetryPolicy.jitter=false`인지 확인한다. jitter 없이는 같은 초에 실패한 모든 consumer가 같은 초에 재시도한다. ### shutdown이 오래 걸린다 `GracefulShutdownCoordinator`가 in-flight를 기다리는 중이다. - `inFlight()`가 0이 되면 즉시 종료 - drain deadline(기본 30초) 초과 시 남은 작업을 **unsettled로 포기**한다 → broker가 재전달 - draining 시작 후 새 retry attempt는 만들지 않는다 ## Destructive 작업 전부 `DestructiveOperationGuard`를 통과해야 한다. | 조건 | 요구 | |---|---| | admin credential | application runtime은 보유하지 않음 | | `AdminApproval` | 유효기간 내 | | dry-run | 항상 허용 | ### Replay ```text 기본: 격리된 consumer group (replay-) 기존 group 대상: 승인 티켓 필수 ``` 기존 production group으로 replay하는 것은 "다시 읽기"가 아니라 **live consumer를 되감는 것**이다. 그 사이의 모든 것이 재처리된다. ### Redrive ```text dry-run으로 후보 수 확인 → 승인 획득 → batch 100건 이하로 실행 → republish CONFIRMED 인 것만 DLQ에서 settlement ``` `redriveId`로 재구동 루프를 추적한다. 같은 메시지가 반복해서 redrive되면 근본 원인이 해결되지 않은 것이다. ### Offset reset `KafkaOffsetResetExecutor`는 승인 predicate를 **생성자 인자**로 받는다. 승인 소스 없이 조립된 runtime은 물리적으로 reset을 수행할 수 없다. ## Topology production topology는 IaC가 만들고 애플리케이션은 **검증만** 한다. `TopologyValidationRuntime`은 모든 불일치를 한 번에 보고하고 startup을 실패시킨다. partition 수가 다르면 destination이 광고하는 ordering 보장이 달라지고, `min.insync.replicas`가 없으면 `acks=all`의 의미가 달라진다.