이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • queue lag가 늘 때 consumer 증설보다 먼저 봐야 할 신호는 무엇인가?
  • retry 폭주와 정상 backlog를 어떻게 구분하는가?
  • DLQ, producer 제한, replay 조건을 어떻게 설계하는가?

큐 사고의 핵심

Queue는 장애를 없애지 않는다.

사용자 응답 경로에서 느린 작업을 분리하고, 대신 적체와 재처리라는 운영 문제를 만든다.

예시는 이벤트 주문 후 알림 worker가 외부 메시지 provider 429를 만나 retry를 반복하고 queue age가 급증하는 상황이다.

이때 consumer를 늘리는 대응은 provider quota와 DB connection을 더 빨리 소진할 수 있다.

이 runbook은 비동기 설계가 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는지 다룬다.

  • 성능: 사용자 응답은 빨라지지만 backlog가 커지면 후속 반영이 늦어진다.
  • 정합성: 메시지는 중복, 역전, 지연, 유실 가능성을 가진다.
  • 장애 격리: provider 장애를 주문 API에서 떼어낼 수 있다.
  • 운영 복잡도: retry, DLQ, replay, idempotency, poison message 처리가 필요하다.

먼저 봐야 할 네 가지 지표

queue depth만 보면 부족하다.

같은 depth라도 처리 가능한 적체와 폭주가 다르다.

지표의미판단
oldest message age가장 오래 기다린 작업의 나이사용자 기대 시간을 넘으면 장애다.
ingress rate새 메시지가 들어오는 속도producer 제한 필요성을 본다.
egress rate처리 완료 속도consumer나 downstream 병목을 본다.
retry rate재시도 비율같은 실패가 증폭되는지 본다.

error type도 함께 본다.

timeout, 429, validation error, duplicate key, deserialization error는 대응이 다르다.

validation error를 retry하면 영원히 성공하지 않는 poison message가 된다.

초동 대응 순서

첫 단계는 retry 증폭을 멈추는 것이다.

provider 429나 downstream timeout이 많다면 retry 간격을 늘리거나 해당 job type을 일시 중단한다.

두 번째는 producer를 줄인다.

새 메시지를 계속 넣으면 consumer를 늘려도 backlog가 줄지 않는다.

세 번째는 실패 유형을 분리한다.

영구 실패는 DLQ로 보내고, 일시 실패만 제한적으로 retry한다.

네 번째는 핵심 기능과 후속 기능을 분리한다.

주문 확정은 유지하되 알림 발송 지연을 사용자에게 안내할 수 있다.

다섯 번째는 replay 조건을 기록한다.

무엇을 언제부터 다시 처리할지 모르면 복구 후 중복 발송이나 누락이 생긴다.

retry 정책 예시

notification_job:
  max_attempts: 5
  backoff: exponential
  base_delay_seconds: 30
  max_delay_minutes: 30
  jitter: true
  retry_on:
    - timeout
    - 429
  no_retry_on:
    - invalid_phone_number
    - template_not_found

retry는 신뢰성을 높이지만 실패를 증폭할 수도 있다.

따라서 멱등성이 없는 작업에는 자동 retry를 붙이지 않는다.

외부 provider가 429를 주면 바로 다시 때리는 대신 quota 회복 시간을 존중한다.

jitter가 없으면 많은 메시지가 같은 시각에 다시 몰린다.

메시지 계약

{
  "messageId": "msg_01",
  "jobType": "ORDER_NOTIFICATION",
  "aggregateId": "order_123",
  "idempotencyKey": "order_123:paid:v1",
  "occurredAt": "2026-07-01T10:00:00+09:00",
  "attempt": 2
}

messageId는 추적을 위한 값이고, idempotencyKey는 중복 실행 방지를 위한 값이다.

둘을 섞으면 재처리 때 같은 메시지인지 같은 비즈니스 행위인지 구분하지 못한다.

aggregateId는 주문, 결제, 회원처럼 상태 owner를 찾는 데 필요하다.

occurredAt은 지연과 역전 처리 판단에 필요하다.

attempt는 retry 정책과 DLQ 전환 판단에 필요하다.

consumer를 늘려도 되는 경우

consumer 증설은 downstream이 받을 수 있을 때만 유효하다.

CPU bound 변환 작업이면 consumer를 늘리는 것이 도움이 될 수 있다.

하지만 DB write, 외부 API, 파일 저장소, 검색 색인처럼 별도 quota가 있는 대상은 먼저 보호해야 한다.

다음 조건을 확인한다.

  • downstream error rate가 낮다.
  • connection pool 대기가 없다.
  • 외부 API quota가 남아 있다.
  • 처리 작업이 멱등적이다.
  • consumer ordering 제약이 없다.

이 조건이 없으면 consumer 증설은 장애 범위를 넓힐 수 있다.

DLQ의 역할

DLQ는 실패 메시지를 버리는 곳이 아니다.

정상 흐름을 막는 메시지를 격리하고, 원인별로 재처리할 수 있게 보관하는 곳이다.

DLQ에 보낼 때는 실패 reason, stack trace 요약, 마지막 attempt, 원본 payload hash를 남긴다.

개인정보 payload를 그대로 오래 보관하면 다른 사고가 된다.

DLQ replay는 전체 재처리보다 조건 재처리가 기본이다.

예를 들어 template_not_found는 템플릿 배포 후 해당 reason만 replay한다.

provider 429는 quota 회복 뒤 시간 구간을 나눠 replay한다.

주문과 알림의 분리

주문 확정과 알림 발송은 같은 성공 기준을 가지지 않는다.

주문은 결제와 재고 정합성이 중요하고, 알림은 지연 허용과 중복 방지가 중요하다.

따라서 주문 API가 알림 provider timeout을 기다리면 안 된다.

주문 성공 후 알림 메시지를 발행하고, 알림 실패는 주문 상태를 실패로 바꾸지 않는다.

대신 사용자에게 주문 조회에서 상태를 확인할 수 있게 한다.

운영자는 알림 지연을 별도 지표와 공지 기준으로 다룬다.

replay 전에 확인할 것

replay는 복구 작업이면서 새로운 장애 원인이 될 수 있다.

다음 질문에 답해야 한다.

  • 같은 idempotencyKey로 이미 성공한 작업은 어떻게 거를 것인가?
  • 오래된 메시지가 현재 상태를 덮어쓰지 않는가?
  • provider나 DB가 replay 속도를 받을 수 있는가?
  • 재처리 중 사용자에게 중복 알림이 갈 수 있는가?
  • replay 실패 메시지는 다시 DLQ로 갈 것인가?

답이 없으면 replay를 시작하지 않는다.

일부 샘플 메시지로 먼저 dry run을 한다.

운영 기록 예시

10:12 signal: notification oldest age 18m, retry rate 62%, provider 429
10:15 action: stop ORDER_NOTIFICATION retry, producer rate 30%로 제한
10:21 action: invalid_phone_number DLQ 분리
10:40 recovery: provider 429 정상화, replay 500/min 시작

이 기록은 사후 회고에서 retry 정책을 바꾸는 근거가 된다.

어떤 조치가 queue age를 낮췄는지, 어떤 조치가 error를 늘렸는지 분리해야 한다.

위험 신호

  • queue depth만 보고 oldest age를 보지 않는다.
  • 모든 error를 같은 retry 정책으로 처리한다.
  • poison message가 같은 partition을 계속 막는다.
  • idempotencyKey 없이 replay를 실행한다.
  • DLQ를 쌓아두기만 하고 owner와 처리 SLA가 없다.
  • consumer 증설 후 downstream DB나 provider error가 증가한다.
  • 알림 실패가 주문 실패처럼 사용자에게 보인다.

확인 질문

확인 질문

  • 확인 질문: queue 적체에서 가장 먼저 볼 지표는 무엇인가?
    • 답변: depth만이 아니라 oldest message age, ingress, egress, retry rate를 함께 본다.
  • 확인 질문: 이 runbook이 바꾸는 설계 축은 무엇인가?
    • 답변: 사용자 응답 성능과 장애 격리는 좋아지지만 중복 처리, 지연 정합성, 운영 복잡도가 늘어난다.
  • 확인 질문: retry를 중단해야 하는 경우는 언제인가?
    • 답변: 실패 원인이 429, quota, validation error처럼 반복 시 성공 가능성이 낮거나 downstream을 더 압박할 때다.