Hình dung ba chế độ acknowledgement như ba cách giao hàng. autoAck là anh giao hàng ném gói qua hàng rào, đánh dấu "đã giao" rồi phóng đi — bạn không có nhà thì gói mất, nhưng sổ vẫn ghi đã giao. Ack thủ công là anh đứng chờ bạn ký nhận; không có chữ ký thì gói quay về kho để giao lại. 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 — khoảng giữa lúc ném gói qua rào và lúc bạn thật sự cầm được nó — và khoảng cách đó 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.
Nó như dặn "mỗi lần chỉ đưa tôi một gói, chờ tôi ký rồi hẵng đưa gói sau" — nhưng chế độ này không đòi chữ ký nào, nên chẳng có gì để chờ, và anh giao hàng đổ nguyên cả xe xuống. Đâ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 chữ ký cho cả chồng gói thay vì ký từng cái. 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 đó — tờ phiếu ký nhận chỉ có giá trị ở đúng cái cửa đã phát ra nó. 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ì.
Nếu chỉ soi một thứ sau bài này, hỏi thẳng broker xem consumer nào đang đọc mà không ký nhận, trong 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.
Mẫu số chung
"Đã giao" và "đã xử lý" là hai khoảnh khắc khác nhau, và chỗ bạn đặt cái ack vào giữa hai khoảnh khắc đó chính là bảo đảm giao nhận của bạn: ack lúc nhận là nhiều nhất một lần (nhanh, có thể mất); ack sau khi xử lý là ít nhất một lần (an toàn, có thể giao lại). Hệ thống nhắn tin nào cũng là cùng lựa chọn dưới những cái tên khác nhau: commit offset tự động so với thủ công của Kafka, visibility-timeout-rồi-xoá của SQS, AUTO so với CLIENT acknowledge của JMS; ít nhất một lần là mặc định thực tế, còn "đúng một lần" là thứ bạn xấp xỉ, không mua được. Không có độ tin cậy miễn phí — bạn chọn chỗ đặt cái ack, và mặt trái của "có thể giao lại" là consumer phải bất biến với việc lặp: xử lý cùng một thông điệp hai lần mà không sao, vì sẽ có ngày nó đến hai lần.
Điều thứ hai: một cái nút an toàn chỉ chạy bên trong đúng cơ chế nó thuộc về, và một cái nút âm thầm không làm gì còn tệ hơn không có — vì nó cho bạn một niềm tin giả. basicQos đếm thông điệp chưa ack, nên dưới autoAck — nơi chẳng có cái nào chưa ack — nó bị bỏ qua trong im lặng; bạn viết dòng trông-an-toàn, mã chạy trơn, và bạn được con số không. Cùng cái bẫy cấu-hình-trơ với @Valid bị quên hay một tag struct gõ sai: "tôi đã đặt" không phải "nó đã có hiệu lực". Nên hãy kiểm hiệu lực, không kiểm ý định — đúng cái bài này làm khi liệt kê consumer đang chạy và thấy prefetch_count=0 — vì những mặc định nguy hiểm núp sau các tiện lợi biên dịch trơn tru và chạy không một tiếng động.
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.