Mọi ví dụ RabbitMQ đầu tiên đều gửi thế này:
ch.basicPublish("", "hang-doi-cua-toi", null, body);
Chuỗi rỗng ở tham số đầu trông như "không có exchange". Nó không phải vậy, và bài này đo xem nó thật sự là gì — kể cả một cánh cửa sau mà phần 10 không đóng được.
Chuỗi rỗng là một exchange thật
Phần 5 đã thấy nó trong danh sách tám exchange có sẵn: một direct exchange, tên rỗng. Và mỗi hàng đợi vừa tạo ra đều tự động có một binding tới nó:
source_name destination_name routing_key
hq hq ← binding ngầm, broker tự tạo
Routing key trùng đúng tên hàng đợi. Đó là toàn bộ phép màu: gửi vào exchange rỗng với routing key là tên hàng đợi thì thông điệp tới đúng hàng đợi đó, vì có một binding direct hoàn toàn bình thường đứng sẵn.
Binding ngầm không gỡ được
Phần 8 đã đo rằng không thêm binding vào nó được. Gỡ cũng không:
ch.queueBind(...) -> 403 ACCESS_REFUSED - operation not permitted on the default exchange
ch.queueUnbind(...) -> 403 ACCESS_REFUSED - operation not permitted on the default exchange
Nghĩa là chừng nào hàng đợi còn tồn tại, nó luôn đến được bằng tên. Không có cách nào tắt.
Cửa sau
Đây là hệ quả mà tôi nghĩ nhiều người chưa từng nghĩ tới. Dựng một hàng đợi chỉ nghe sự kiện Việt Nam:
ch.exchangeDeclare("su-kien", "topic", true);
ch.queueDeclare("chi-nghe-vn", true, false, false, null);
ch.queueBind("chi-nghe-vn", "su-kien", "don.vn.#");
Luật định tuyến hoạt động đúng như thiết kế:
gửi qua 'su-kien' khoá 'don.us.gap' -> hàng đợi có 0 (đúng: mẫu chặn lại)
gửi qua default exchange, khoá = tên hàng đợi -> hàng đợi có 1 <- LỌT VÀO
Trong nội bộ một ứng dụng thì đây là chuyện nhỏ. Khi hàng đợi nhận dữ liệu từ nhiều bên, từ dịch vụ của đội khác, hoặc từ đối tác, thì đó là lỗ hổng xác thực đầu vào.
Chặn nó bằng phân quyền
Cách duy nhất để đóng cửa sau là không cho quyền ghi vào default exchange. Tạo một user với quyền write chỉ khớp exchange được phép:
rabbitmqctl set_permissions -p / han-che "^$" "^su-kien$" "^.*$"
# configure write read
guest qua 'su-kien' -> gửi được
guest qua default exchange -> gửi được
han-che qua 'su-kien' -> gửi được
han-che qua default exchange -> 403 ACCESS_REFUSED - write access to exchange
'amq.default' in vhost '/' refused for user 'han-che'
Chú ý cái tên trong thông báo: amq.default. Trong giao thức nó là chuỗi rỗng, nhưng trong hệ thống phân quyền nó mang tên đó. Muốn cho phép một user dùng default exchange thì regex write phải khớp amq.default, chứ không phải khớp chuỗi rỗng — chi tiết này không hiện ra ở đâu cho tới lúc bạn nhận 403.
Gửi tới tên hàng đợi không tồn tại
mandatory=false -> broker báo: (không gì cả)
mandatory=true -> broker báo: 312 NO_ROUTE key=hang-doi-khong-he-co
Giống mọi exchange khác: gõ sai tên hàng đợi thì thông điệp bốc hơi im lặng. Với default exchange, cái này nguy hiểm hơn bình thường vì routing key là tên hàng đợi — sai một ký tự trong tên là mất dữ liệu, và không có mẫu binding nào để đối chiếu lúc gỡ lỗi.
Nó không nhanh hơn đáng kể
Có người dùng default exchange vì nghĩ bỏ qua bước tra exchange sẽ nhanh hơn. Đo 20 000 thông điệp có publisher confirms, hâm nóng trước, trung vị 3 lượt:
| Đường gửi | Thông lượng |
|---|---|
default exchange "" |
138 541 msg/s |
| direct exchange tự khai | 125 179 msg/s |
Chênh 11% — nằm ngay ở ranh giới nhiễu của bộ đo này, và nhỏ hơn nhiều so với khác biệt giữa các kiểu exchange mà phần 11 đo được. Đây không phải lý do để chọn.
Vì sao mã ví dụ chạy được mà topology thật thì không
Vì default exchange bỏ qua đúng cái phần mà exchange sinh ra để làm. Gửi bằng tên hàng đợi nghĩa là bên gửi phải biết tên hàng đợi của bên nhận — chính là sự ràng buộc mà hệ thống nhắn tin dùng để tránh.
Hệ quả rất cụ thể: thêm một bên nghe thứ hai thì phải sửa mã bên gửi, vì một thông điệp chỉ có một routing key và nó đang là tên của một hàng đợi cụ thể. Với exchange thật, thêm bên nghe chỉ tốn một queueBind và bên gửi không hay biết.
Nên dùng default exchange khi: viết ví dụ, thử nghiệm, hoặc gửi việc vào đúng một hàng đợi công việc mà bạn chắc chắn sẽ không bao giờ có bên nghe thứ hai. Ngoài ra thì hãy khai một exchange, kể cả khi lúc này nó chỉ có một binding.
Bài sau: khi thông điệp không khớp binding nào, có cách hứng nó lại thay vì để bốc hơi.
Thử ba mươi giây
# xem toan bo binding ngam cua broker
docker exec -u rabbitmq rmq rabbitmqctl -q list_bindings source_name destination_name routing_key \
| awk '$1==""'
Mỗi dòng là một hàng đợi mà bất kỳ ai có quyền ghi vào amq.default đều tiêm thẳng vào được, bất kể bạn đã dựng luật định tuyến công phu đến đâu.