RabbitMQ의 정체 메시지 심층 방어
증상
소비자가 여전히 연결되어 있어도 RabbitMQ 작업 대기열의 진행이 중지되면 이 가이드를 사용하세요.
- 사용자는 채점 또는 보고서 작업 중단을 무기한으로 볼 수 있습니다.
messages_unacknowledged는0위에 유지됩니다.- 소비자 포드가 계속 실행 중이므로 브로커가 메시지를 다시 대기열에 넣지 않습니다.
- 소비자 콜백이 완료되기 전에 로그에
Task exception was never retrieved, 콜백 실패 또는 예외가 표시됩니다.
현재 저장소가 이미 방어하고 있는 것
현재 워커 및 인프라 레이아웃은 이미 이 사건의 복구 교훈을 바탕으로 하고 있습니다.
lumie-worker/libs/common/mq.py는universal_process(...)의 메시지 수명주기를 중앙 집중화합니다.lumie-infra/platform/rabbitmq/base/topology/policies.yaml는delivery-limit: 5를 요청 및 콜백 대기열에 적용합니다.lumie-infra/platform/rabbitmq/base/podmonitor.yaml는/metrics및/metrics/per-object를 모두 스크랩합니다. 이는 대기열별 정체 메시지 경고에 필요합니다.lumie-infra/observability/prometheus/helm-values.yaml는OMRQueueStuck및ReportQueueStuck를 정의합니다.- 소비자 콜백이 실행되기 전에 이전
aio_pika래퍼 충돌이 발생했기 때문에lumie-worker/libs/common/observability.py는AioPikaInstrumentor를 명시적으로 방지합니다.
lumie-worker/libs/common/mq.py의 대표적인 모양:
async def universal_process(message, *, handler, callback, ...):
try:
...
await callback(payload)
await message.ack()
except Exception:
if not message.processed:
await message.nack(requeue=True)
이 실패 모드가 발생한 이유
두 가지 개별 오류 클래스로 인해 동일한 대기열 중단 증상이 나타날 수 있습니다.
- 소비자 콜백이 시작되지만
ack(),nack()또는reject()가 실행되기 전에 예외가 발생합니다. - 워커 콜백이 시작되기 전에 래퍼가 충돌하므로 워커 자체
try/except가 실행될 기회를 얻지 못합니다.
첫 번째 사건은 콜백 측 네트워크 오류 이후 발생한 비동기 작업 예외였습니다. 두 번째 사건은 process_message(...)가 실행되기 전에 충돌이 발생한 OpenTelemetry aio_pika 호환성 버그였습니다. 공유된 교훈은 애플리케이션 코드만으로는 충분하지 않다는 것입니다. 브로커는 재전송을 제한해야 하며, 모니터링은 워커 콜백이 시작되지 않는 경우에도 정체된 큐를 감지해야 합니다.
진단 경로
먼저 브로커를 확인하세요
대기열에 연결된 소비자와 0이 아닌 messages_unacknowledged가 있는 경우 브로커는 일반적으로 작업을 선택하고 수명 주기를 완료하지 않은 소비자를 기다리고 있습니다.
rabbitmqctl list_queues name messages consumers messages_unacknowledged
대기열 상태를 처리 지표와 비교
현재 Prometheus 규칙은 "처리 속도가 0인 확인되지 않은 메시지"를 기본 정체 신호로 처리합니다. 문제가 큐 깊이 스파이크일 뿐이라고 가정하기 전에 lumie-infra/observability/prometheus/helm-values.yaml에서 경고 표현식을 검사하십시오.
워커 로그에서 실패 클래스를 검사합니다
다음을 찾으세요:
Task exception was never retrievedBackground job callback failedBackground job unexpected failure- 콜백 전달 중 연결 또는 DNS 오류
- 콜백 코드 경로 앞의 래퍼 또는 계측 예외
universal_process(...)가 실행되기 전에 오류가 발생하면 queue.consume(process_message) 주변의 코드와 process_message(...) 이전에 실행되는 모든 계측 또는 래퍼 레이어에 집중하세요.
서비스가 여전히 공유 수명 주기 기본 요소를 사용하는지 확인하세요.
채점 및 보고서 소비자는 모두 lumie-worker/libs/common/mq.py에서 universal_process(...)에 위임합니다. 새로운 소비자가 사용자 정의 라이프사이클 코드를 다시 도입한 경우 공유 라이브러리가 수정된 경우에도 이전의 확인되지 않은 메시지 실패 모드가 반환될 수 있습니다.
수정
- 라이브 소비자가 메시지를 보유하고 있고 복구할 수 없는 경우 AMQP 채널이 닫히고 RabbitMQ가 메시지를 다시 대기열에 추가하도록 영향을 받은 포드를 다시 시작합니다.
- 각 소비자를
universal_process(...)에 유지하여 모든 반환 경로가 정확히ack,nack또는reject중 하나로 끝나도록 합니다. - 워커 내부에서 재시도를 제한하고 브로커 소유의
delivery-limit가 poison 또는 연결할 수 없는 콜백 메시지를 DLQ로 이동하도록 합니다. - 사용 중인 정확한
aio_pika버전이 래퍼에 대해 확인되지 않는 한AioPikaInstrumentor를 다시 활성화하지 마십시오. 현재 저장소에서는 의도적으로 이를lumie-worker/libs/common/observability.py에서 안전하지 않은 것으로 문서화합니다.
예방
- 모든 RabbitMQ 소비자에 대해 "반환 전 정확히 하나의 수명 주기 결정"을 하드 불변으로 처리합니다.
/metrics/per-object를 통해 대기열별 지표를 활성화된 상태로 유지합니다./metrics만으로는 정체된 대기열을 감지하는 데 충분하지 않습니다.delivery-limit는 브로커에 속하므로 앱 코드가 아닌 토폴로지 운영자 CR에 대기열 정책을 유지합니다.- 경고를 애플리케이션 예외 처리와 별도의 방어 계층으로 처리합니다. 애플리케이션 코드가 실행되지 않는 경우에도 큐가 중단될 수 있습니다.
확인
cd Lumie
rg -n "universal_process|message\\.processed|AioPikaInstrumentor" \
lumie-worker/libs/common lumie-worker/services
rg -n "delivery-limit|metrics/per-object|OMRQueueStuck|ReportQueueStuck" \
lumie-infra/platform/rabbitmq/base \
lumie-infra/observability/prometheus/helm-values.yaml
성공은 워커 수명주기가 여전히 universal_process(...)를 통해 라우팅되고, 브로커 정책이 여전히 재전송을 제한하며, 큐별 경고가 애플리케이션 프로세스 외부에 여전히 존재한다는 것을 의미합니다.
원본 사고 세부사항
2026년 4월 18일 원본 기록은 stuck RabbitMQ message에 대한 defense-in-depth 대응을 남겼습니다. 핵심 교훈은 어떤 한 계층도 전체 실패 모드를 혼자 소유하지 않는다는 점입니다. Consumer code, broker policy, metrics, alert가 모두 guard를 가져야 합니다.
| 세부사항 | 값 |
|---|---|
| 영향 pattern | message가 deliver됐지만 정상 완료되지 않는 worker message |
| Application defense | consumer return 전에 정확히 하나의 lifecycle decision |
| Broker defense | redelivery가 무한히 반복되지 않도록 delivery-limit policy |
| Observability defense | aggregate /metrics가 아니라 per-object RabbitMQ metric |
| Alert defense | app process 외부의 queue-specific stuck-message alert |
원본 기록의 불변조건은 다음입니다.
every consumed message must end in exactly one ack, nack/requeue, reject/dead-letter, or explicit failure path
이 불변조건은 각 service handler가 아니라 shared worker code에 있어야 합니다. Broker policy는 application layer가 그래도 실패할 때의 결과를 제한하고, Prometheus는 code나 broker-level behavior가 forward progress를 만들지 못하는 경우를 잡습니다.
Stuck queue를 디버깅할 때는 consumer log, RabbitMQ message state, policy/redelivery setting, per-queue metric 네 가지 view를 모두 보존합니다.