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
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”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.
Tổng quan: message rời queue vì lý do gì
Phần tiêu đề “Tổng quan: message rời queue vì lý do gì”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.
bad-jobMain queue giao message cho consumer; message ở trạng thái unacked.
Khai báo đúng
Phần tiêu đề “Khai báo đúng”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).
# Policy cho queue main: DLX + routing keyrabbitmqctl set_policy dlx-main '^main\.' \ '{"dead-letter-exchange":"dlx","dead-letter-routing-key":"dead"}' \ --apply-to queuesCá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.
TTL không phải đồng hồ chính xác
Phần tiêu đề “TTL không phải đồng hồ chính xác”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.
Retry có đếm
Phần tiêu đề “Retry có đếm”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.
jobLầ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 đã retryconst 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ượtelse channel.nack(msg, false, false); // sang retry.queueVì 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 |
Dead-letter an toàn: at-least-once
Phần tiêu đề “Dead-letter an toàn: at-least-once”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):
- Queue nguồn là quorum queue.
- Policy
dead-letter-strategy = at-least-once(mặc địnhat-most-once). overflow = reject-publish(mặc địnhdrop-head; vớidrop-headchế độ này rơi về at-most-once).- Đã đặt
dead-letter-exchange; routing key là tùy chọn. - Feature flag
stream_queuephải được bật (kiểm tra bằngrabbitmqctl list_feature_flags). - Nên đặt
max-lengthhoặcmax-length-bytes.
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.
Poison message và x-delivery-limit
Phần tiêu đề “Poison message và x-delivery-limit”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.
Demo đã chạy
Phần tiêu đề “Demo đã chạy”docker compose -f examples/docker-compose.yml up -d rabbitmqnpx --prefix examples ts-node examples/src/rabbitmq-dlx/dlx-demo.tsOutput 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”.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Khi nào dùng / không dùng
Phần tiêu đề “Khi nào dùng / không dùng”- Dùng DLX + retry queue: lỗi tạm thời cần backoff mà không muốn consumer
sleepgiữ 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).
Checklist production
Phần tiêu đề “Checklist production”- Mọi queue job đều có DLX và dead queue có owner, retention, runbook.
- Test binding bằng một message thật, không chỉ nhìn config.
- Quorum queue quan trọng:
at-least-once+reject-publish+max-length. - Không dùng
requeue=truelàm chiến lược retry. - Retry có đếm bằng
x-deathhoặc header ứng dụng, có ngưỡng và điểm kết thúc. - Một retry queue cho mỗi mức delay.
- Replay giữ nguyên event ID; consumer idempotent.
- Giám sát: độ sâu và tuổi dead queue, tỉ lệ
delivery_limit.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”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?