Bỏ qua để đến nội dung
Knowledge Hub

RabbitMQ dead-letter, TTL và retry có đếm: job lỗi đi đâu? Trung cấp

Thời gian đọc ước tính: 7 phút

Consumer gửi email, provider trả lỗi, consumer nack với requeue=true. RabbitMQ giao lại ngay, lỗi lại xảy ra. Trong vài giây, một job chiếm gần hết CPU và log, job hợp lệ bị chậm.

Đổi sang requeue=false thì vòng lặp dừng, nhưng nếu queue không có dead-letter exchange (DLX) thì message bị bỏ hẳn. Cả hai cách đều thiếu một chính sách: lỗi nào retry, bao lâu, bao nhiêu lần, và hết lượt thì lưu ở đâu.

DLX là cơ chế chuyển hướng: khi message rời queue vì một trong bốn lý do dưới đây, RabbitMQ publish lại nó vào DLX. Nó không tự quyết định retry hay không.

reason Khi nào xảy ra Ghi chú
rejected basic.reject hoặc basic.nack với requeue=false Do consumer chủ động
expired Message TTL hết hạn Quorum và classic xử lý khi message tới đầu queue
maxlen Queue vượt giới hạn độ dài Với overflow drop-head (message cũ nhất ở đầu queue) hoặc reject-publish-dlx (message publish bị từ chối); reject-publish không dead-letter
delivery_limit Quorum queue giao lại vượt x-delivery-limit Mặc định 20 từ RabbitMQ 4.0

Khi cả queue hết hạn (queue TTL) thì message không được dead-letter.

Consumer từ chối bad-job; broker route nó sang dead queue
Consumer từ chối bad-job; broker route nó sang dead queueBước 1/4: Main queue giao message cho consumer; message ở trạng thái unacked.Main queueConsumerDead queuebad-job
Main queue → Consumerbad-job
Bước 1/4

Main queue giao message cho consumer; message ở trạng thái unacked.

Docs khuyến nghị cấu hình bằng policy thay vì hardcode argument (nếu cả hai cùng đặt DLX, argument thắng).

Terminal window
# Policy cho queue main: DLX + routing key
rabbitmqctl set_policy dlx-main '^main\.' \
'{"dead-letter-exchange":"dlx","dead-letter-routing-key":"dead"}' \
--apply-to queues

Các tên khóa đã đối chiếu với tài liệu:

Mục đích Policy key Queue argument
DLX dead-letter-exchange x-dead-letter-exchange
Routing key khi dead-letter dead-letter-routing-key x-dead-letter-routing-key
TTL từng message trong queue message-ttl (ms) x-message-ttl
TTL riêng một message thuộc tính expiration (chuỗi ms)
Xóa queue không dùng expires x-expires
Chiến lược dead-letter (quorum) dead-letter-strategy x-dead-letter-strategy
Giới hạn giao lại (quorum) delivery-limit x-delivery-limit

Nếu có cả TTL queue và TTL message thì giá trị thấp hơn được dùng. x-expires là TTL của cả queue khi không được dùng, không phải TTL message; đặt nhầm chỗ sẽ xóa cả topology.

Message hết hạn không bao giờ được giao cho consumer, nhưng broker chỉ dead-letter nó khi nó tới đầu queue. Message hết hạn nằm sau một message chưa hết hạn có thể nằm lại cho tới khi tới lượt. Vì vậy “retry sau đúng 10 giây” chỉ là xấp xỉ.

Hệ quả cho thiết kế: dùng một retry queue cho mỗi mức delay (ví dụ 10s, 1m, 10m), đừng trộn các TTL khác nhau trong cùng một queue.

Mô hình: main queue có DLX trỏ vào retry queue; retry queue có TTL và DLX trỏ ngược về main; dead queue là điểm kết thúc.

Vòng main, retry (TTL), main và điểm dừng ở dead queue
Vòng main, retry (TTL), main và điểm dừng ở dead queueBước 1/5: Lần 1: consumer nhận job, provider lỗi 503. Chưa có x-death nên số lần retry là 0.main.queueDLX = retryConsumerđọc x-deathlỗi, retry = 0dead.queueđiều tra / replayretry.queueTTL 10s, DLX = mainjob
main.queue → Consumerjob
Bước 1/5

Lần 1: consumer nhận job, provider lỗi 503. Chưa có x-death nên số lần retry là 0.

Bộ đếm lấy từ header x-death. Mỗi phần tử gồm queue, reason, count, exchange, routing-keys, time; count là số lần message bị dead-letter từ queue đó vì lý do đó. Consumer đọc count của phần tử (queue=retry, reason=expired):

// Giả mã: số lần đã retry
const deaths = msg.properties.headers?.['x-death'] ?? [];
const retries = deaths.find(d => d.queue === 'retry' && d.reason === 'expired')?.count ?? 0;
if (retries >= 3) { publishToDead(msg); channel.ack(msg); } // hết lượt
else channel.nack(msg, false, false); // sang retry.queue

Vì sao vòng này không bị cắt: RabbitMQ phát hiện dead-letter cycle và chỉ bỏ message nếu không có lần rejected nào trong vòng. Ở đây consumer nack mỗi lượt nên vòng hợp lệ.

Mô hình trên dựa vào tài liệu, repo chưa có demo chạy cho retry loop. Chỉ phần nack tới dead queue ở dưới là đã chạy.

Phân loại lỗi trước khi retry:

Loại lỗi Hành động
Tạm thời (503, timeout mạng) Nack, đi vòng retry có đếm
Vĩnh viễn (payload sai schema) Publish thẳng dead queue, không retry
Nghiệp vụ (“resource đã xóa”) Ack và ghi nhận theo policy riêng

Mặc định dead-lettering là at-most-once: không dùng confirm nên có thể mất message nếu target không sẵn sàng. Quorum queue có chế độ at-least-once với đủ các điều kiện sau (đã đối chiếu tài liệu quorum queues):

  1. Queue nguồn là quorum queue.
  2. Policy dead-letter-strategy = at-least-once (mặc định at-most-once).
  3. overflow = reject-publish (mặc định drop-head; với drop-head chế độ này rơi về at-most-once).
  4. Đã đặt dead-letter-exchange; routing key là tùy chọn.
  5. Feature flag stream_queue phải được bật (kiểm tra bằng rabbitmqctl list_feature_flags).
  6. Nên đặt max-length hoặc max-length-bytes.
Terminal window
rabbitmqctl set_policy dlx-safe '^orders\.' \
'{"dead-letter-exchange":"dlx","dead-letter-strategy":"at-least-once","overflow":"reject-publish","max-length":100000}' \
--apply-to quorum_queues

Đổi lại, queue nguồn giữ message cho tới khi target xác nhận, tốn thêm tài nguyên. Vẫn cần giám sát dead queue có phình ra hay không.

Từ RabbitMQ 4.0, quorum queue có x-delivery-limit mặc định 20. Message bị giao lại quá 20 lần (consumer chết hoặc requeue=true) bị dead-letter với reason delivery_limit, hoặc bị bỏ nếu không có DLX. Đặt -1 tắt giới hạn nhưng docs không khuyến nghị. Từ 4.3 giới hạn so với delivery-count thay vì acquired-count.

Đây là lưới an toàn cuối cho vòng requeue=true, không thay retry có đếm: 20 lần giao lại liên tiếp vẫn là 20 lần nóng.

Terminal window
docker compose -f examples/docker-compose.yml up -d rabbitmq
npx --prefix examples ts-node examples/src/rabbitmq-dlx/dlx-demo.ts

Output trên rabbitmq:4-management-alpine: dead queue: bad-job và x-first-death-reason: rejected. Demo chỉ kiểm chứng đường nack, DLX, dead queue. Nó không test TTL, quorum queue, at-least-once hay broker restart.

Thử đổi routing key dead-letter sang key không có binding: dead queue trống. Topology thật quyết định nơi message đi, không phải tên “DLX”.

  • Dùng DLX + retry queue: lỗi tạm thời cần backoff mà không muốn consumer sleep giữ delivery unacked.
  • Dùng thẳng dead queue: lỗi vĩnh viễn.
  • Không dùng TTL cho SLA thời gian chặt: expiry chỉ xấp xỉ; dùng scheduler.
  • Cần retry theo topic lớn, replay lâu dài: cân nhắc stream hoặc Kafka (xem so sánh Kafka và RabbitMQ).
  1. Mọi queue job đều có DLX và dead queue có owner, retention, runbook.
  2. Test binding bằng một message thật, không chỉ nhìn config.
  3. Quorum queue quan trọng: at-least-once + reject-publish + max-length.
  4. Không dùng requeue=true làm chiến lược retry.
  5. Retry có đếm bằng x-death hoặc header ứng dụng, có ngưỡng và điểm kết thúc.
  6. Một retry queue cho mỗi mức delay.
  7. Replay giữ nguyên event ID; consumer idempotent.
  8. Giám sát: độ sâu và tuổi dead queue, tỉ lệ delivery_limit.

Tự kiểm tra: (1) Nack requeue=true có tới DLX không? (2) Vì sao vòng main, retry, main không bị cycle detection cắt? (3) Overflow drop-head làm gì với at-least-once?