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 clientbasicPublish 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%
Cách viết trực giác nhất — gửi một cái, chờ xác nhận, gửi cái tiếp — chậm hơn 133 lần. Đây không phải chi phí của độ tin cậy; đó là chi phí của việc chờ một vòng mạng cho mỗi thông điệp. Cùng mức độ đảm bảo, cách bất đồng bộ nhanh gấp 68 lần.

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 confirmfalse 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.