78 lines
4.4 KiB
Markdown
78 lines
4.4 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(기본 Kafka adapter) 상태 확인: `APP_MESSAGING_KAFKA_ENABLED` 값과 broker endpoint 가용성
|
||
- Kafka disabled(default) 상태에서 outbox 이벤트가 append 되고 있으면 publish 경로가 `AdapterDisabledException`으로 전부 실패하는 구성 오류 — 이 경우 producer use case 쪽 활성화/구성을 먼저 의심
|
||
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 라우팅·대시보드 링크 확정 시 보강 필요.
|