Phần 25 đo được rằng ném ngoại lệ trong @RabbitListener dựng lại vòng lặp thông điệp độc — 6 010 vòng mỗi giây. Bài này đo tiếp và tìm ra một chuyện tôi không ngờ: điều đó chỉ đúng với một số loại ngoại lệ.

Hai ngoại lệ, hai số phận

Cùng một hàng đợi có DLX, cùng một container mặc định, chỉ đổi loại ngoại lệ mà listener ném ra:

Ngoại lệ Số lần giao trong 2 giây Còn ở hàng đợi Vào nghĩa địa
IllegalStateException (nghiệp vụ) 11 189 1 0
MessageConversionException 1 0 1

Một cái quay vô hạn, một cái ra khỏi đường đi ngay lần đầu. Cùng cấu hình.

Vì sao: danh sách lỗi "chí mạng"

Container mặc định dùng ConditionalRejectingErrorHandler. Nó phân loại ngoại lệ thành hai nhóm:

Chí mạng — thử lại bao nhiêu lần cũng vô ích: lỗi chuyển đổi thông điệp, lỗi không tìm thấy phương thức listener, lỗi ép kiểu tham số. Những cái này bị reject không requeue bất kể defaultRequeueRejected đang bật hay tắt.

Không chí mạng — mọi thứ khác, tức là toàn bộ ngoại lệ nghiệp vụ của bạn. Những cái này đi theo defaultRequeueRejected, mà phần 25 đo được mặc định là true.

Cách phân loại này hợp lý: JSON hỏng thì thử lại một triệu lần vẫn hỏng, còn "CSDL đang bận" thì đáng thử lại. Nhưng nó tạo ra một hệ quả khó chịu khi gỡ lỗi: hai lỗi khác nhau trong cùng một listener sẽ hành xử ngược nhau, và bạn phải biết trước để không đi tìm nhầm chỗ.

Tắt requeue là đủ cho phần lớn trường hợp

container.setDefaultRequeueRejected(false);
defaultRequeueRejected=false -> 1 lần giao | nghĩa địa 1

Ngoại lệ nghiệp vụ giờ hành xử như ngoại lệ chuyển đổi: ra khỏi hàng đợi ngay, không chặn ai. Đây là thay đổi một dòng có giá trị cao nhất trong toàn bộ Spring AMQP.

Thông điệp trong nghĩa địa mang theo:

x-death               = [{reason=rejected, count=1, exchange=, queue=le-viec, ...}]
x-first-death-reason  = rejected
x-first-death-queue   = le-viec
x-last-death-reason   = rejected
x-last-death-queue    = le-viec

Ngoài x-deathphần 14 đã mổ, RabbitMQ hiện đại còn thêm bốn header x-first-death-*x-last-death-* — tiện hơn nhiều khi viết truy vấn hay cảnh báo, vì không phải bới vào mảng lồng nhau.

DLX cho biết chuyện gì, không cho biết vì sao

Nhìn kỹ bảng header ở trên: có reason=rejected, có count, có hàng đợi gốc. Không có gì về ngoại lệ. Bạn biết thông điệp bị từ chối, không biết vì sao — phải đi lục log ứng dụng, khớp theo thời gian.

RepublishMessageRecoverer sinh ra để lấp chỗ đó. Nó không dùng DLX; nó gửi lại thông điệp sang một exchange khác, kèm chẩn đoán:

thân giữ nguyên: than-goc
  x-exception-message    = CSDL khong tra loi
  x-original-routingKey  = le-viec
  x-original-exchange    =
  x-exception-stacktrace = java.lang.IllegalStateException: CSDL khong tra loi
                             at lab.Loi.lambda$main$4(...

Cái giá của stack trace

tổng kích thước header: 1 688 byte (thân chỉ 8 byte)

Header lớn gấp 211 lần thân thông điệp. Với thông điệp thật vài trăm byte thì tỉ lệ dễ chịu hơn, nhưng con số tuyệt đối vẫn thế: mỗi thông điệp hỏng kéo theo khoảng 1,5 KB stack trace nằm trong RAM của broker và trên đĩa.

Sự cố lớn — hạ nguồn chết, mười nghìn thông điệp cùng hỏng — là 15 MB stack trace gần như giống hệt nhau đổ vào hàng đợi lỗi, ngay lúc broker đang chịu tải bất thường.

Chọn cái nào

DLX thuần RepublishMessageRecoverer
Cấu hình một tham số hàng đợi một bean + một exchange
Cho biết vì sao hỏng không có, kèm stack trace
Chi phí mỗi thông điệp hỏng vài chục byte header ~1,5 KB
Giữ nguyên thân

Thực dụng: DLX cho hàng đợi thông lượng cao, Republish cho hàng đợi nghiệp vụ quan trọng nơi mỗi thông điệp hỏng đáng được điều tra riêng. Và dù chọn cái nào cũng nhớ tắt defaultRequeueRejected — không có nó thì hai cơ chế trên đều không bao giờ được gọi tới.

Bài sau: thử lại có backoff ngay trong listener, và phép đo cho thấy nó làm một mẻ việc chậm đi gấp một nghìn lần.

Thử ba mươi giây

# hang doi loi cua ban dang nang bao nhieu so voi so thong diep
curl -su guest:guest 'localhost:15672/api/queues/%2F/<hang-doi-loi>' \
  | jq '{messages, message_bytes, byte_moi_thong_diep: (.message_bytes / (.messages+0.001))}'

Nếu mỗi thông điệp lỗi nặng hơn thông điệp gốc vài trăm lần, bạn đang dùng RepublishMessageRecoverer — hãy chắc rằng hàng đợi đó có giới hạn độ dài, kẻo một sự cố hạ nguồn sẽ lấp đầy đĩa của broker.