Phần 4 đã cho thấy vòng đời Ready → Unacked → xong, và nói rằng autoAck=true là "chấp nhận mất thông điệp". Bài này đo xem mất bao nhiêu, bằng cách giết thật tiến trình consumer bằng kill -9.

Ba chế độ

Chế độ Broker coi là xong khi Mất thông điệp nếu consumer chết
autoAck=true vừa gửi đi
basicAck consumer gọi ack không
basicNack / basicReject consumer từ chối không — vào DLX hoặc quay lại

Khác biệt nằm ở chỗ "gửi đi" và "xử lý xong" là hai thời điểm khác nhau, và khoảng cách giữa chúng chính là cửa sổ mất dữ liệu.

Giết consumer giữa chừng

Nạp 1 000 thông điệp, chạy consumer xử lý mất 20 ms mỗi cái, kill -9 sau 1,5 giây rồi đếm:

Cấu hình Xử lý xong Còn trong hàng đợi Mất
autoAck=true, prefetch không giới hạn 52 0 948
autoAck=false, prefetch không giới hạn 51 949 0
autoAck=false, prefetch = 1 51 949 0
autoAck=true, prefetch = 1 51 0 949

Hai dòng giữa là điều bạn mong đợi: không mất gì, mọi thứ chưa ack quay lại hàng đợi.

prefetch không cứu được autoAck

Dòng cuối bảng là chỗ tôi nghĩ nhiều người sẽ ngạc nhiên. Đặt basicQos(1) — nghĩa là "mỗi lần chỉ đưa tôi một thông điệp" — mà vẫn mất 949 cái, y hệt như không đặt gì.

basicQos bị bỏ qua hoàn toàn khi autoAck=true. Prefetch là cơ chế đếm số thông điệp chưa được ack; với autoAck thì không có cái nào chưa ack cả, nên không có gì để giới hạn. Broker đẩy hết công suất, và consumer của bạn ngồi trên một đống thông điệp mà broker đã coi là xong.

Đây là cái bẫy đúng nghĩa: bạn viết basicQos(1) với ý làm hệ thống an toàn hơn, mã chạy không lỗi, và bạn không nhận được gì cả. Prefetch chỉ có ý nghĩa cùng với ack thủ công — chi tiết ở bài 19.

Cái giá của an toàn

Tiêu thụ 20 000 thông điệp, trung vị 3 lượt:

Cách ack Thông lượng So với autoAck
autoAck=true 234 343 msg/s 100%
ack từng cái 152 531 msg/s 65%
ack theo lô 100 (multiple=true) 201 415 msg/s 86%

An toàn tốn 35% thông lượng nếu ack từng cái. Nhưng dòng thứ ba lấy lại phần lớn:

if (deliveryTag % 100 == 0) ch.basicAck(deliveryTag, true);   // ack cả lô tới tag này

Cờ multiple=true nghĩa là "ack mọi thông điệp có tag nhỏ hơn hoặc bằng cái này". Một lời gọi thay cho một trăm, và bạn vẫn không mất gì khi consumer chết — chỉ là tối đa 100 thông điệp phải làm lại.

Đó là đánh đổi đúng để cân nhắc: không phải "an toàn hay nhanh", mà là "làm lại bao nhiêu là chấp nhận được".

Hai cách làm channel chết

ack hai lần cùng một tag -> 406 PRECONDITION_FAILED - unknown delivery tag 1
ack nhầm channel         -> 406 PRECONDITION_FAILED - unknown delivery tag 1

Hai lỗi khác nhau, thông báo giống hệt nhau, và cả hai đều đóng channel. Lý do chúng giống nhau: delivery tag chỉ có nghĩa trong phạm vi channel đã giao thông điệp đó. Gửi tag sang channel khác thì channel kia thấy một con số nó chưa từng cấp — đúng như khi bạn ack lại một tag đã dùng.

Hệ quả thực tế: đừng gom việc ack sang một luồng hay một channel dùng chung. Ack phải quay về đúng channel đã nhận, và phần 3 đã nói channel không an toàn luồng — hai ràng buộc đó cộng lại thành một quy tắc: một channel cho một luồng consumer, ack tại chỗ.

Khi nào autoAck vẫn đúng

Khi mất một thông điệp thật sự không sao: số liệu đo đạc, tín hiệu nhịp tim, log gửi đi để hiển thị. Bảng trên cho thấy bạn đổi lấy 54% thông lượng, và với loại dữ liệu đó thì đó là đánh đổi hợp lý.

Ngoài ra thì không. Đặc biệt đừng dùng autoAck cho hàng đợi công việc rồi trấn an bằng basicQos — bảng đầu bài cho biết điều đó không có tác dụng gì.

Bài sau: publisher confirms — đầu bên kia của cùng câu hỏi, và con số ở đó còn khắc nghiệt hơn.

Thử ba mươi giây

# consumer nao dang chay khong ack
docker exec -u rabbitmq rmq rabbitmqctl -q list_consumers \
    queue_name consumer_tag ack_required prefetch_count
queue_name  consumer_tag       ack_required  prefetch_count
lc2         consumer-autoack   false         0
lc          consumer-cua-toi   true          7

Hai dòng đó là toàn bộ bài này thu gọn. Consumer autoAckprefetch_count=0 — không phải vì tôi quên đặt, mà vì broker không ghi nhận basicQos cho nó. Nếu một consumer ack_required=false đang đọc hàng đợi công việc của bạn, đó là chỗ dữ liệu biến mất trong lần deploy gần nhất.