Phần trước kết bằng một lời hứa: chi phí định tuyến không nằm ở kiểu exchange hay số binding, mà ở số hàng đợi mà thông điệp rơi vào. Fanout là chỗ chứng minh điều đó rõ nhất, vì nó chép cho tất cả.

Fanout bỏ qua routing key, thật sự bỏ qua

gửi 4 thông điệp với 4 khoá: "", "moi", "bat-ky-chuoi-nao", "!!!"
-> email=4  thong-ke=4  kiem-toan=4

Bốn khoá khác nhau, ba hàng đợi, cái nào cũng nhận đủ bốn. Fanout không đọc routing key. Vẫn nên để chuỗi rỗng khi gửi — không phải vì broker cần, mà vì người đọc mã sau này cần biết bạn cố ý.

Chia việc không phải là phát tán

Đây là chỗ tôi thấy nhiều người mới lẫn nhất, nên đo cả hai cạnh nhau với cùng 100 thông điệp.

Một hàng đợi hai consumer chia 50/50; fanout hai hàng đợi mỗi bên nhận 100

100 thông điệp, 2 consumer chung 1 hàng đợi -> A=50, B=50, TỔNG=100
100 thông điệp, 2 hàng đợi riêng qua fanout -> A=100, B=100, TỔNG=200
Thêm consumer là để làm nhanh hơn — việc được chia ra. Thêm hàng đợi là để nhiều bên cùng nghe — việc được nhân lên. Hai chuyện khác hẳn nhau, và không có tham số nào đổi cái này thành cái kia.

Hệ quả thực tế: nếu dịch vụ gửi thư và dịch vụ thống kê cùng đọc một hàng đợi, chúng sẽ giành thông điệp của nhau — mỗi bên nhận khoảng một nửa, và không bên nào biết mình đang thiếu. Mỗi bên phải có hàng đợi riêng bind vào cùng exchange.

Bind hai lần vẫn chỉ một bản

Bind cùng một hàng đợi vào fanout hai lần với hai khoá khác nhau, rồi gửi một thông điệp:

2 binding, gửi 1 thông điệp -> bind-doi=1 bản
số binding thực sự          : 2

Hai binding tồn tại thật, nhưng thông điệp chỉ vào hàng đợi một lần. RabbitMQ chép theo đích, không theo binding — nên bind trùng không sinh ra bản sao. Yên tâm khi dựng topology bằng mã chạy lại nhiều lần.

Cái giá của phát tán

Cùng cách đo như phần trước — 20 000 thông điệp, có publisher confirms, hâm nóng, trung vị 3 lượt — chỉ đổi kiểu exchange sang fanout và tăng dần số hàng đợi gắn vào:

Số hàng đợi Thông lượng gửi Số bản ghi mỗi giây
1 150 189 msg/s 150 189
10 28 513 msg/s 285 132
100 2 200 msg/s 219 991

Cột giữa tụt 68 lần. Cột phải gần như đứng yên.

Đó là toàn bộ câu chuyện: broker không tính công theo số thông điệp bạn gửi, mà theo số bản nó phải ghi. Gửi một thông điệp vào fanout có 100 hàng đợi là yêu cầu broker làm 100 việc. So với bảng của phần 8 — 500 binding mà thông lượng phẳng lì — khác biệt không nằm ở kiểu exchange, mà ở chỗ direct chỉ rơi vào một đích còn fanout rơi vào tất cả.

Cách đọc bảng này khi thiết kế: đừng hỏi "exchange này nhanh không", hãy hỏi "mỗi thông điệp sẽ được ghi mấy lần". Con số đó mới là thứ nhân với lưu lượng của bạn.

Không hàng đợi nào thì thông điệp đi đâu

mandatory=true, fanout không có hàng đợi nào -> trả về: 312 NO_ROUTE

Giống hệt direct: không có đích thì thông điệp bốc hơi, và chỉ mandatory mới cho bạn biết. Fanout dễ rơi vào cảnh này hơn vì nó thường dùng cho sự kiện mà bên nghe đến rồi đi — hôm nay có ba hàng đợi, mai ai đó dọn dẹp còn không cái nào, và bên gửi vẫn báo cáo thành công.

Khi nào fanout

Khi mọi bên nghe đều cần mọi sự kiện, và bạn không muốn bên gửi biết có bao nhiêu bên nghe. Thêm một dịch vụ mới chỉ cần thêm hàng đợi và một queueBind — không đụng vào producer. Đó là giá trị thật của fanout, chứ không phải tốc độ.

Đừng dùng fanout rồi lọc ở consumer. Nếu mỗi bên chỉ quan tâm một phần sự kiện, bạn đang trả tiền ghi cho tất cả rồi vứt đi phần lớn — bảng ở trên cho biết cái giá. Đó là việc của topic exchange, bài sau.

Thử ba mươi giây

# đếm xem mỗi exchange đang chép sang bao nhiêu đích
docker exec -u rabbitmq rmq rabbitmqctl -q list_bindings source_name destination_name \
  | awk '$1!="" {n[$1]++} END {for (x in n) print n[x], x}' | sort -rn

Dòng đầu tiên là exchange đắt nhất của bạn. Nhân số đó với lưu lượng gửi vào nó, và bạn có số bản ghi mỗi giây mà broker đang gánh — con số đáng theo dõi hơn hẳn tổng số thông điệp.