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 | có |
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 autoAck có prefetch_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.