4.1 KiB
4.1 KiB
title, category, error_codes, severity, owner, last_updated, status
| title | category | error_codes | severity | owner | last_updated | status | |
|---|---|---|---|---|---|---|---|
| Runbook — outbox dead letter | INTERNAL |
|
P1 | oncall | 2026-06-11 | stub |
Runbook: outbox dead letter (runbook://outbox/dead-letter)
1. Trigger
이 runbook은 다음 alert에서 발동됩니다.
- alert name:
outbox_dead_letter - alert payload 필수 field:
error.code=OUTBOX_DEAD_LETTER,event_type,event_id,correlation_id,runbook_link - 임계:
outbox.publisher.published.total{outcome=DEAD}> 0 (DEAD 전이는 자동 회복이 없으므로 단건도 점검 대상)
2. First Response (5분 이내)
Step 1 — 확인
- ERROR log에서
OUTBOX_DEAD_LETTER라인 확인:event_id,event_type,correlation_id, 마지막 실패 원인 예외 추출 - DB에서 DEAD row 확인:
SELECT * FROM outbox_event WHERE status = 'DEAD' ORDER BY occurred_at; - 차단 영향 파악 (중요): strict per-aggregate FIFO 정책상 DEAD row는 같은
aggregate_id의 후행 이벤트를 계속 차단함 —SELECT count(*) FROM outbox_event b WHERE b.status <> 'PUBLISHED' AND EXISTS (SELECT 1 FROM outbox_event d WHERE d.status='DEAD' AND d.aggregate_id=b.aggregate_id AND d.occurred_at < b.occurred_at);
Step 2 — 임시 격리
- DEAD는 max attempts(3) 소진의 최종 상태 — 자동 재시도 없음, 수동 개입 필수
- 차단된 aggregate가 비즈니스 크리티컬하면 아래 §4의 수동 처분(재발행 또는 skip)을 우선 수행
3. Diagnosis
- log query:
{service="app"} | error.code="OUTBOX_DEAD_LETTER" | stats count by event_type - 마지막 실패 원인 분류:
- poison event (payload 직렬화/계약 위반) → payload 자체 결함, 재발행해도 실패 — 수정 후 재발행 또는 skip
- broker 장기 outage 중 attempts 소진 → broker 회복 후 재발행으로 해결 가능
- 구성 오류 (Kafka disabled 상태에서 producer 활성) → 구성 수정 후 재발행
- 가능한 원인 우선순위: 구성 오류 > broker outage > poison payload
4. Mitigation (수동 처분 — 둘 중 하나)
- 재발행 (기본): 원인 해소 후 해당 row를 다시 claim 가능 상태로 되돌림 —
UPDATE outbox_event SET status = 'PENDING', attempt_count = 0, next_attempt_at = now() WHERE event_id = '<id>' AND status = 'DEAD';(consumer는 at-least-once + idempotencyKey dedupe 의무가 있으므로 중복 발행은 안전) - skip (영구 폐기): 이벤트가 더 이상 유효하지 않으면 PUBLISHED로 마킹해 FIFO 차단을 해제 —
UPDATE outbox_event SET status = 'PUBLISHED' WHERE event_id = '<id>' AND status = 'DEAD';⚠️ skip은 다운스트림에 영구 이벤트 갭을 만든다 — 비즈니스 오너 승인 후에만 수행하고 incident 기록에 남김 - 장기: poison event 재발 방지(payload 계약 테스트 보강), DEAD 빈발 event_type의 producer 검증 강화
5. Escalation
- 처분 판단(재발행 vs skip)이 불가하면 해당 이벤트의 비즈니스 오너에게 escalate
- DEAD 누적이 특정 event_type에 집중되면 producer 코드 오너에게 page
6. Recovery / Verification
- 회복 확인:
SELECT count(*) FROM outbox_event WHERE status='DEAD';= 0, 차단됐던 aggregate의 후행 이벤트가 PUBLISHED로 전이 outbox.publisher.lag정상화(< 60s), 재발행분의 consumer dedupe 동작 확인- post-incident: DEAD 원인 분류 기록, 같은 원인의 재발 방지 테스트 추가
7. Related
- error-codes.yaml rows:
OUTBOX_DEAD_LETTER(INTERNAL, retryable=false) - metrics.yaml:
outbox.publisher.published.total{outcome=DEAD},outbox.pending.size{status=DEAD},outbox.publisher.lag - 코드:
application-corePublishPendingOutboxEventsUseCase(FAILED→DEAD 전이),adapter-persistenceoutbox/OutboxEventJpaRepository(FIFO 게이트 — DEAD가 후행 차단) - 관련 runbook: outbox-publish-failed
- 관련 branch: feature-domain-event-outbox-contract, feature-background-job-async-contract (max attempts/DLQ vocabulary SSOT)
Stub 상태 안내: 이 runbook은 skeleton 단계의 stub. 실제 broker·DLQ 토픽·승인 체계 확정 시 보강 필요.