Phần 3 đo được 424 291 msg/s và tôi đã ghi ngay bên dưới rằng con số đó là tốc độ phía client — basicPublish chỉ ghi vào socket rồi trả về, broker chưa hứa gì. Bài này trả nốt lời hứa đó: đo xem biết chắc broker đã nhận thì tốn bao nhiêu.
basicPublish không hứa gì cả
Mặc định, gửi xong là xong. Broker chết trong lúc thông điệp còn trên đường, mạng đứt, hàng đợi đầy — bên gửi không hay biết. Phần 4 đã cho một nửa cách chữa (mandatory báo khi không có đường đi), nhưng nó không nói gì về việc thông điệp có được ghi xuống hay không.
Publisher confirms là nửa còn lại:
ch.confirmSelect(); // bật cho channel này
ch.basicPublish("", "cf", props, body);
ch.waitForConfirmsOrDie(30_000); // chờ broker xác nhận
Bốn cách chờ, đo cạnh nhau
20 000 thông điệp persistent vào hàng đợi durable, trung vị 3 lượt:
| Cách | Thông lượng | So với không confirm |
|---|---|---|
| Không confirm (chỉ ghi vào socket) | 326 083 msg/s | 100% |
| Confirm đồng bộ, từng thông điệp | 2 453 msg/s | 1% |
| Confirm theo lô 100 | 73 575 msg/s | 23% |
| Confirm theo lô 1 000 | 150 482 msg/s | 46% |
Confirm bất đồng bộ (ConfirmListener) |
167 617 msg/s | 51% |
Vì sao đồng bộ từng cái thảm hại
waitForConfirms chặn cho tới khi broker trả lời. Với thông điệp persistent, broker chỉ trả lời sau khi đã ghi xuống đĩa. Nghĩa là mỗi thông điệp phải đi trọn một vòng: client → broker → đĩa → client, và trong suốt vòng đó không có thông điệp nào khác đang bay.
2 453 msg/s tương đương khoảng 0,4 mili giây mỗi vòng — con số hợp lý cho một vòng mạng nội bộ cộng một lần ghi. Vấn đề không phải nó chậm, mà là bạn đã biến một đường ống thành một chuỗi tuần tự.
Ba cách còn lại đều dựa trên cùng một ý: giữ nhiều thông điệp đang bay cùng lúc.
Vì sao bất đồng bộ nhanh
Đăng ký một ConfirmListener và tự theo dõi những số thứ tự chưa được xác nhận:
ch.confirmSelect();
ch.addConfirmListener((tag, multiple) -> {
if (multiple) chuaXong.headSet(tag + 1).clear(); // xác nhận cả lô tới tag này
else chuaXong.remove(tag);
}, (tag, multiple) -> { /* nack: gửi lại */ });
chuaXong.add(ch.getNextPublishSeqNo());
ch.basicPublish("", "cf", props, body);
Phép đo cho một chi tiết tôi thấy rất đáng nhớ:
ConfirmListener được gọi 88 lần cho 20 000 thông điệp
Broker không xác nhận từng cái một. Nó gom lại và gửi về một xác nhận kèm cờ multiple=true, nghĩa là "mọi thông điệp có số thứ tự nhỏ hơn hoặc bằng cái này đều đã xong". Trung bình mỗi lần gọi xác nhận 227 thông điệp.
Đó là lý do cách bất đồng bộ nhanh, và cũng là lý do bạn bắt buộc phải xử lý cờ multiple. Bỏ qua nó — chỉ remove(tag) — thì 19 912 thông điệp trong phép đo trên sẽ nằm mãi trong danh sách chờ, và mã gửi lại của bạn sẽ gửi lại toàn bộ chúng.
Chọn cái nào
| Tình huống | Cách |
|---|---|
| Dữ liệu mất được | không confirm |
| Gửi từng thông điệp rời rạc, độ trễ không quan trọng | đồng bộ từng cái — đơn giản, và 2 453/s vẫn thừa cho một API |
| Gửi hàng loạt, muốn ít mã | theo lô 1 000 |
| Đường ống thông lượng cao | bất đồng bộ |
Đừng bỏ qua dòng thứ hai vì con số xấu. Nếu ứng dụng của bạn gửi vài chục thông điệp mỗi giây, waitForConfirmsOrDie ngay sau basicPublish là mã đúng đắn nhất bạn viết được — nó biến mọi lỗi thành một ngoại lệ ngay tại dòng gửi. Con số 133 lần chỉ có ý nghĩa khi bạn thật sự cần thông lượng.
Confirm hứa gì và không hứa gì
Hứa: broker đã nhận trách nhiệm với thông điệp. Với thông điệp persistent vào hàng đợi durable, nó đã nằm trên đĩa. Với thông điệp gửi vào exchange không có đích, confirm vẫn báo thành công — vì broker đã xử lý xong, và việc không ai nhận là chuyện của mandatory hoặc alternate exchange.
Không hứa: consumer đã nhận, càng không phải đã xử lý. Đó là chuyện của acknowledgement ở đầu bên kia.
Hai cơ chế ghép lại mới thành một đường không mất dữ liệu, và giữa chúng vẫn còn một khoảng: broker đã nhận nhưng chưa ghi xong lúc mất điện. Bài 33 về quorum queue nói về khoảng đó.
Bài sau: transaction AMQP — cơ chế cũ hơn confirms, và phép đo giải thích vì sao tài liệu chính thức khuyên tránh nó.
Thử ba mươi giây
# channel nao dang bat confirm, va co bao nhieu thong diep dang cho xac nhan
docker exec -u rabbitmq rmq rabbitmqctl -q list_channels \
name confirm messages_unconfirmed
Cột confirm là false trên một channel gửi dữ liệu quan trọng nghĩa là bên gửi đang không biết gì cả. Cột messages_unconfirmed lớn và không tụt nghĩa là broker đang không theo kịp.