Hình dung bạn ra công chứng để ký một tờ giấy nợ — cho chính mình vay. Vẫn phải xếp hàng, vẫn đóng dấu, vẫn ghi vào sổ có người làm chứng, trả đủ mọi chi phí của thủ tục công chứng. Chỉ có điều tờ giấy ấy chẳng bảo vệ được ai, vì có mỗi một bên. Chạy quorum queue trên một node duy nhất đúng là cảnh đó: bạn trả toàn bộ cái giá của cơ chế đồng thuận Raft — ghi nhật ký, đánh chỉ số, xác nhận — trong khi lợi ích của nó (sống sót khi một node chết) chỉ tồn tại khi có nhiều node để mà chết. Bài này đo cái hoá đơn đó.

Quorum queue là câu trả lời hiện đại của RabbitMQ cho tính sẵn sàng cao, thay cho cơ chế nhân bản classic kiểu cũ. Bài này đo cái giá của nó trong điều kiện dễ chịu nhất có thể: một node duy nhất, không có nhân bản nào qua mạng. Con số ở đây là sàn — cụm thật sẽ đắt hơn.

Quorum queue từ chối những gì

Khai báo Kết quả
durable=false 541 INTERNAL_ERROR - transient_nonexcl_queues is deprecated
exclusive=true 406 - invalid property 'exclusive-owner'
autoDelete=true 406 - invalid property 'auto-delete'
x-max-priority=10 406 - invalid arg 'x-max-priority'
x-message-ttl=1000 cho phép
x-max-length=100 cho phép

Ba dòng đầu hợp lý: hàng đợi nhân bản mà lại tạm thời hoặc chết theo connection thì vô nghĩa. Dòng thứ tư mới đáng nhớ — quorum queue không có ưu tiên, nên nếu bạn dựa vào priority như phần 7 đo, chuyển sang quorum là mất tính năng đó, và phải tách thành nhiều hàng đợi.

Ngược lại, x-delivery-limit chỉ có ở quorum — phần 20 đo được classic từ chối thẳng tham số này.

Thông lượng và độ trễ

50 000 thông điệp 200 byte, persistent, có publisher confirms, trung vị 3 lượt, một node:

Thông lượng Độ trễ một thông điệp
classic 127 800 msg/s p50 0,40 ms · p95 0,49 ms
quorum 81 967 msg/s (64%) p50 0,61 ms · p95 1,66 ms

Thông lượng còn 64%. Độ trễ trung vị tăng 50%, nhưng cột p95 mới là chỗ đáng nhìn: gấp 3,4 lần. Đó là hình dạng đặc trưng của một hệ đồng thuận — phần lớn thao tác nhanh, nhưng thỉnh thoảng phải chờ một vòng ghi nhật ký Raft hoàn tất.

Nhắc lại: đây là một node. Không có gói tin nào qua mạng, không có node nào phải đồng ý. Toàn bộ chênh lệch trên là chi phí của bản thân cơ chế Raft — ghi nhật ký, đánh chỉ số, xác nhận. Trên cụm ba node thật, cộng thêm độ trễ mạng nhân với số lần phải đợi đa số.

Bộ nhớ

100 000 thông điệp, cùng dữ liệu, đo tiến trình hàng đợi:

Bộ nhớ tiến trình hàng đợi messages_ram
classic 0,41 MiB 1
quorum 2,78 MiB 0

Gấp 6,8 lần. Quorum queue giữ nhật ký Raft cùng chỉ mục của nó trong bộ nhớ, và phần trước cho thấy classic thì gần như không giữ gì. Với vài chục hàng đợi thì không sao; với vài nghìn hàng đợi quorum thì đây là con số phải nhân lên trước khi quyết định.

Vậy mua được gì

Không mất thông điệp khi một node chết. Đây là toàn bộ lý do tồn tại của nó. Thông điệp chỉ được xác nhận sau khi đa số node đã ghi, nên mất một node trong cụm ba node không mất gì cả — thứ mà cơ chế nhân bản classic kiểu cũ không bảo đảm được trong mọi tình huống.

Hành vi rõ ràng khi phân vùng mạng. Raft có luật đa số; bên thiểu số biết mình là thiểu số và ngừng phục vụ, thay vì hai bên cùng nhận thông điệp rồi phải hoà giải sau.

x-delivery-limit chạy thật. Phần 20 đo được cơ chế này bắt đúng trường hợp consumer sập — thứ không bộ đếm nào trong mã của bạn bắt được, vì mã đó đã chết.

Khi nào vẫn dùng classic

Khi chỉ có một node. Quorum trên một node là trả toàn bộ chi phí Raft mà không nhận được gì — bảng trên chính là hoá đơn đó.

Khi cần priority. Không có đường vòng nào ngoài việc tách thành nhiều hàng đợi và tự chọn.

Khi hàng đợi là tạm hoặc chết theo connection — hàng đợi trả lời trong mẫu RPC, hàng đợi theo dõi của một client. Quorum từ chối thẳng những tổ hợp cờ đó.

Khi có rất nhiều hàng đợi. Nhân 2,78 MiB với số hàng đợi trước khi quyết định.

Ngoài bốn trường hợp trên, với hệ thống nhiều node thì quorum là mặc định đúng: cái giá 36% thông lượng đổi lấy việc không mất dữ liệu khi một máy chết là món hời ở gần như mọi nơi.

Kiểm nhanh xem mình có đang trả giá Raft mà không cần không — hàng đợi thuộc kiểu nào, ngốn bao nhiêu bộ nhớ:

# hang doi cua ban thuoc kieu nao, va ton bao nhieu bo nho
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues name type messages memory \
  | sort -k4 -rn | head

Thấy hàng đợi quorum trên một broker chỉ có một node nghĩa là bạn đang trả giá Raft mà không nhận được tính sẵn sàng nào.

Mẫu số chung

Cái hoá đơn ở bài này dạy một điều dễ quên: chi phí của một cơ chế chịu lỗi thì trả trước và trả luôn, còn lợi ích của nó chỉ hiện ra khi đúng cái sự cố nó phòng thật sự xảy ra. Quorum trên một node là trả 100% cái giá cho 0% lợi ích, vì không có node thứ hai nào để mà chết. Cùng cái vô nghĩa đó ở RAID trên một đĩa, replication factor = 1, two-phase commit với đúng một bên tham gia, hay chạy đồng thuận cho một cấu hình chẳng bao giờ đổi. Nguyên tắc: khớp cơ chế với mô hình hỏng bạn thật sự có, đừng bật vì nó nghe "an toàn hơn" — an toàn trước một sự cố không thể xảy ra chỉ còn lại mỗi phần chi phí.

Điều thứ hai, sâu hơn: khi mạng phân vùng, một hệ có trạng thái buộc phải chọn giữa nhất quán và sẵn sàng — không có lựa chọn thứ ba. Raft chọn nhất quán: bên thiểu số tự biết mình thiểu số và ngừng phục vụ, thà từ chối còn hơn nhận rồi mâu thuẫn. Cơ chế mirror kiểu cũ nghiêng về sẵn sàng: cả hai bên cứ nhận, rồi để lại một mớ phải hoà giải mà đôi khi hoà giải nghĩa là có dữ liệu bị vứt. Đây chính là cái đánh đổi CAP mà mọi CSDL phân tán đều phải tuyên bố lập trường — một Postgres đồng bộ chờ replica ack, một Cassandra cho chọn mức nhất quán theo từng truy vấn, một etcd đứng hẳn về phía nhất quán như Raft. Chọn công cụ phân tán mà không biết nó bỏ gì khi mạng đứt là chọn mù: câu hỏi không phải "nó có HA không", mà là "khi phân vùng, nó thà sai hay thà đứng?"

Bài sau: stream — kiểu hàng đợi thứ ba, với mô hình hoàn toàn khác hai cái này.