84 lines
4.9 KiB
Markdown
84 lines
4.9 KiB
Markdown
---
|
||
title: Runbook — outbox publish 일시 실패
|
||
category: TRANSIENT_DEPENDENCY
|
||
error_codes: [OUTBOX_PUBLISH_FAILED]
|
||
severity: P2
|
||
owner: oncall
|
||
last_updated: 2026-06-11
|
||
status: stub
|
||
---
|
||
|
||
# Runbook: outbox publish 일시 실패 (`runbook://outbox/publish-failed`)
|
||
|
||
## 1. Trigger
|
||
|
||
이 runbook은 다음 alert에서 발동됩니다.
|
||
|
||
- alert name: `outbox_publish_failed_rate` 또는 `outbox_publisher_lag`
|
||
- alert payload 필수 field: `error.code=OUTBOX_PUBLISH_FAILED`, `event_type`, `correlation_id`, `runbook_link`
|
||
- 임계 (metrics.yaml verbatim):
|
||
- P2: `outbox.publisher.published.total{outcome=FAILED}` rate > 1% for 10m
|
||
- P2: `outbox.publisher.lag` > 60s for 10m / P1: > 300s for 5m
|
||
- P2: `outbox.pending.size{status=PENDING}` growing for 10m
|
||
|
||
## 2. First Response (5분 이내)
|
||
|
||
### Step 1 — 확인
|
||
1. ERROR log에서 `OUTBOX_PUBLISH_FAILED` 라인 확인: `event_type`, `event_id`, `correlation_id`, `attempt_count` 추출
|
||
2. broker 상태 확인: `APP_MESSAGING_BROKER` 값과 broker endpoint 가용성. 이 키는 활성화 스위치가
|
||
아니라 **선택자**다 — messaging을 끄는 것은 `APP_MESSAGING_ENABLED=false`이고, 이 값을 비운다고
|
||
messaging이 꺼지지는 않는다.
|
||
- `APP_MESSAGING_BROKER`가 공백인 채로 relay가 켜져 있으면 **애플리케이션이 기동하지 않는다**
|
||
(`OutboxRelayBrokerRequirementValidator`, MSG-024). 이 조합에서는 publish가 전부
|
||
`AdapterDisabledException`으로 실패하며 PENDING row가 DEAD까지 소진되기 때문이다.
|
||
기동 실패를 보고 있다면 broker를 설정하거나 `ca-skeleton.outbox.relay-enabled=false`로 둔다.
|
||
- 기동은 했는데 실패가 쌓인다면 broker는 설정돼 있고 도달이 안 되는 것이다 — endpoint부터 본다.
|
||
3. `outbox.pending.size` status 분포 확인 (FAILED 누적 vs PENDING 누적)
|
||
|
||
### Step 2 — 임시 격리
|
||
- 일시 실패는 자동 backoff 재시도(30s × 2^(attempt-1) + jitter, max attempts 3)가 동작 — 즉시 수동 개입 불필요
|
||
- broker 장기 다운이면 DEAD 전이 누적 전에 broker 회복을 우선 (max attempts 소진 시 `runbook://outbox/dead-letter`로 이관)
|
||
- relay 자체를 멈춰야 하면 `ca-skeleton.outbox.relay-enabled=false`로 스케줄러 비활성 (이벤트는 outbox 테이블에 안전하게 보존됨 — 유실 없음)
|
||
|
||
## 3. Diagnosis
|
||
|
||
- log query: `{service="app"} | error.code="OUTBOX_PUBLISH_FAILED" | stats count by event_type`
|
||
- metric panel:
|
||
- `outbox.publisher.published.total{outcome}` — FAILED 비율
|
||
- `outbox.publisher.lag{event_type}` — 최고령 미발행 이벤트 age
|
||
- `outbox.pending.size{status}` — 상태별 분포
|
||
- DB 확인: `SELECT status, count(*) FROM outbox_event GROUP BY status;`
|
||
- 가능한 원인:
|
||
- broker outage/네트워크 → broker 측 회복 대기
|
||
- Kafka adapter 미구성(enabled인데 brokers 누락은 기동 시 차단됨) / disabled 상태에서 producer 활성화
|
||
- poison event (직렬화 불가/payload 계약 위반) → 재시도 무의미, attempts 소진 후 DEAD로 흘러감 (의도된 동작)
|
||
- 동일 aggregate head 실패로 후행 이벤트가 FIFO 게이트에 차단되어 lag 증가 (strict per-aggregate FIFO — 설계 의도)
|
||
|
||
## 4. Mitigation
|
||
|
||
- 단기: broker 회복 후 backoff 만료 시 자동 재발행 — `outcome=PUBLISHED` 회복 확인
|
||
- IN_FLIGHT orphan(claim 후 crash)은 in-flight-timeout(기본 PT5M) 경과 후 자동 재claim — at-least-once이므로 중복 발행 가능, consumer dedupe(idempotencyKey)가 흡수
|
||
- 장기: `ca-skeleton.outbox.poll-interval`/`batch-size` 조정, broker 가용성 SLA 점검, 빈발 event_type의 payload 계약 검토
|
||
|
||
## 5. Escalation
|
||
|
||
- P1 lag(>300s 5m) 지속 + broker 회복 불가면 broker/infra 팀에 page
|
||
- DEAD 전이가 발생하기 시작하면 `runbook://outbox/dead-letter` 절차로 이관
|
||
|
||
## 6. Recovery / Verification
|
||
|
||
- 회복 확인 metric: `outcome=FAILED` rate < 1% 10분 지속, `outbox.publisher.lag` < 60s, `outbox.pending.size{status=FAILED}` 감소 추세
|
||
- post-incident: 실패 구간의 DEAD row 유무 확인, consumer 측 중복 처리량 확인(dedupe 동작 검증), backoff/attempts 상수 재평가
|
||
|
||
## 7. Related
|
||
|
||
- error-codes.yaml rows: `OUTBOX_PUBLISH_FAILED` (TRANSIENT_DEPENDENCY, retryable=true, retry_after 30s)
|
||
- metrics.yaml: `outbox.publisher.published.total`, `outbox.publisher.lag`, `outbox.pending.size`
|
||
- 코드: `application-core` `PublishPendingOutboxEventsUseCase`(상태머신), `adapter-persistence` `outbox/OutboxEventJpaRepository`(SKIP LOCKED claim + FIFO 게이트), `adapter-outbound` `messaging/outbox/KafkaOutboxMessagePublishAdapter`(fail-closed)
|
||
- 관련 runbook: [[outbox-dead-letter]]
|
||
- 관련 branch: [[feature-domain-event-outbox-contract]], [[feature-background-job-async-contract]] (retry/DLQ vocabulary SSOT)
|
||
|
||
---
|
||
|
||
> **Stub 상태 안내**: 이 runbook은 skeleton 단계의 stub. 실제 broker 채택·alert 라우팅·대시보드 링크 확정 시 보강 필요.
|