Hình dung headers exchange như một nhân viên phân loại cầm một chồng phiếu điều kiện, mỗi phiếu ghi "loại = đơn VÀ vùng = vn". Với mỗi kiện hàng tới, anh ta lật từng phiếu từ đầu tới cuối, đối chiếu từng ô. Không có tủ xếp theo mã (như direct), không có sắp theo vần (như topic) — chồng phiếu để nguyên, nên trăm phiếu là trăm lần lật cho mỗi kiện. Đó là mấu chốt của cả bài. Ba bài trước đi qua direct, fanout và topic. Còn một kiểu nữa, và nó là kiểu duy nhất bạn có thể đi cả sự nghiệp mà không gặp. Bài này đo xem vì sao.
Khớp theo bảng header, không nhìn khoá
Headers exchange bỏ qua routing key hoàn toàn — không phải "bỏ qua như fanout để cho tiện", mà là không có chỗ nào trong thuật toán khớp đọc tới nó:
routing key "" -> KHỚP (bỏ qua khoá)
routing key "bat-ky" -> KHỚP (bỏ qua khoá)
routing key "loai.don" -> KHỚP (bỏ qua khoá)
Thay vào đó, binding mang theo một bảng khoá–giá trị, và thông điệp khớp khi bảng header của nó thoả bảng đó.
ch.queueBind("kho", "hx", "", Map.of(
"x-match", "all", // hoặc "any"
"loai", "don",
"vung", "vn"));
x-match: all hay any
Bốn binding nhân bảy thông điệp, mỗi ô một lần gửi thật:
binding loai=don loai=don vung=vn loai=don uu-tien=1 uu-tien="1" (không
vung=vn vung=us (số) (chuỗi) header)
──────────────────────────────────────────────────────────────────────────────────────────────────
all: loai=don, vung=vn ✓ · · · · · ·
any: loai=don, vung=vn ✓ ✓ ✓ ✓ · · ·
all: loai=don ✓ ✓ · ✓ · · ·
all: uu-tien=1 (số) · · · · ✓ · ·
Đọc ra ba luật:
all là phép AND, any là phép OR. Dòng một khớp đúng một thông điệp; dòng hai khớp bốn.
Header thừa trong thông điệp không sao. Dòng ba khớp cả loai=don,vung=us — binding chỉ đòi những khoá nó nêu, phần còn lại kệ. x-match bản thân nó cũng không bị coi là một khoá phải khớp.
Không có ký tự đại diện. Không có *, không có #, không so tiền tố, không so khoảng. Chỉ có bằng hoặc không bằng. Đây là khác biệt lớn nhất so với topic, và nó chỉ theo một chiều: mọi thứ headers làm được thì topic cũng làm được bằng cách nhét các chiều vào khoá, còn ngược lại thì không — topic so được tiền tố, headers thì không.
1 không khớp "1"
Dòng cuối bảng là cái bẫy đáng nhớ nhất:
binding: {x-match: all, uu-tien: 1} ← số nguyên
thông điệp uu-tien = 1 (số) -> KHỚP
thông điệp uu-tien = "1" (chuỗi) -> không khớp
Phép so khớp tính cả kiểu dữ liệu. Một bên gửi bằng Java đặt Map.of("uu-tien", 1), một bên khác đọc cấu hình từ YAML rồi đặt "1" — hai thông điệp trông giống hệt nhau trong log, một cái tới đích và một cái biến mất.
LongString chứ không phải String — và bạn có một vùng mà mắt thường không phân biệt nổi đúng sai. Với headers exchange, hãy khai kiểu của từng header ở một chỗ và dùng chung.
Ba kiểu exchange, cùng một phép đo
20 000 thông điệp, 50 hàng đợi gắn vào, mỗi thông điệp khớp đúng một đích, có publisher confirms, hâm nóng trước, trung vị 3 lượt:
| Kiểu | Thông lượng | So với direct |
|---|---|---|
| direct | 120 725 msg/s | 100% |
| topic | 108 049 msg/s | 90% |
| headers | 50 454 msg/s | 42% |
Headers chậm hơn direct 2,4 lần. Nhưng con số đáng lo hơn nằm ở chỗ khác: nó không phẳng theo số binding. Cùng phép đo, chỉ đổi số hàng đợi gắn vào exchange, mỗi thông điệp vẫn chỉ khớp đúng một đích:
| Số binding | topic | headers |
|---|---|---|
| 1 | 123 691 msg/s | 99 753 msg/s |
| 50 | 118 696 msg/s | 51 819 msg/s |
| 500 | 113 361 msg/s | 7 413 msg/s |
Số binding tăng 500 lần: topic mất 8%, headers mất 93% — tụt 13 lần.
Lý do nằm ở cấu trúc dữ liệu. Direct tra bảng băm theo khoá; topic đi trên cây tiền tố — phần 8 đã đo direct phẳng lì tới 500 binding, và bảng trên cho thấy topic cũng vậy. Headers thì phải duyệt qua từng binding và so từng cặp khoá–giá trị. Đó là kiểu duy nhất trong bốn kiểu không có chỉ mục thật sự, và cột phải chính là hình dạng của việc không có chỉ mục.
Và phép đo này mới chỉ dùng một header mỗi binding. Nhiều header thì mỗi binding tốn nhiều phép so hơn nữa.
Vì sao gần như không ai dùng
Gom lại thì có ba lý do, và không lý do nào là "nó hỏng":
Kém diễn đạt hơn topic — không ký tự đại diện, không gom nhóm theo tiền tố.
Chậm hơn 2,4 lần ở quy mô nhỏ, và tệ dần theo số binding trong khi hai kiểu kia đứng yên.
Khó đọc khi vận hành. Mọi binding headers đều có routing key rỗng, nên muốn biết nó khớp gì thì phải đi đọc cột arguments:
source_name destination_name routing_key arguments
hq hq []
hxx hq [{"loai","don"},{"x-match","all"}]
Lúc gỡ lỗi nửa đêm, don.vn.# nói ngay được điều nó làm; một bảng như trên thì không. Muốn nhìn toàn bộ binding headers của mình một lần:
# xem binding headers cua ban that su doi gi
docker exec -u rabbitmq rmq rabbitmqctl -q list_bindings \
source_name destination_name routing_key arguments
Cột routing_key rỗng còn arguments có nội dung nghĩa là binding headers. Nếu bảng đó dài hơn màn hình, bạn vừa tìm ra lý do thứ ba ở trên.
Khi nào nó vẫn đúng
Khi các chiều phân loại thật sự độc lập và không có thứ bậc. Khoá topic ép bạn chọn một thứ tự từ trái sang phải; nếu không có chiều nào tự nhiên đứng trước, mọi thứ tự đều sai với một nhóm người dùng nào đó. Headers không bắt bạn chọn.
Khi thuộc tính để lọc đã nằm sẵn trong header vì lý do khác — content-type, phiên bản schema, mã nguồn gửi — và bạn không muốn nhân đôi chúng vào routing key, nơi chúng sẽ lệch nhau sớm muộn.
Ngoài hai trường hợp đó, hãy dùng topic. Và nếu bạn đang cân nhắc headers vì khoá topic trông xấu, thì vấn đề nằm ở cách đặt khoá — phần 10 có phần bàn về chuyện đó.
Mẫu số chung
Cách bạn khớp quyết định có thể đánh chỉ mục được hay không. Bằng-chính-xác là một cú tra bảng băm, tiền-tố là một lượt đi trên cây — nhưng một phép AND/OR tuỳ ý trên cả một tập thuộc tính thì không có cách nào đánh chỉ mục, nên nó thoái hoá thành quét từng luật (tệ đi 13 lần ở 500 binding trong khi topic đứng yên). Đây là một căng thẳng gặp ở khắp nơi: truy vấn càng linh hoạt càng khó đánh chỉ mục. Một WHERE trên cột có chỉ mục trả lời tức thì; một biểu thức tuỳ ý hay LIKE '%x%' buộc quét toàn bảng; một document store không trả nổi câu hỏi mà nó không có secondary index. Chọn một mô hình định tuyến/truy vấn là lặng lẽ chọn luôn câu chuyện chỉ mục của mình — và "khớp được bất cứ thứ gì bạn muốn" gần như luôn đồng nghĩa "quét tất cả".
Hai bài học nhỏ hơn cũng đáng mang theo. Thứ nhất, một phép khớp nhạy với kiểu mà mắt không thấy (1 so với "1", String so với LongString) là một quả mìn khớp-hụt-trong-im-lặng — hãy khai kiểu của từng trường ở một chỗ dùng chung, vì "trông giống hệt trong log" không phải là "bằng nhau với broker". Thứ hai, "gần như không ai dùng" là dữ liệu, không phải bí ẩn: một tính năng hiếm người đụng thường hiếm vì những lý do đo được — ở đây là kém diễn đạt, chậm gấp 2,4 lần, và một bảng arguments không ai đọc nổi lúc nửa đêm trong khi don.vn.# tự nói ra nó làm gì. Ưu tiên công cụ đọc được; một lối đi ít người qua thường vắng vì đã có người đo ra lý do.
Bài sau: default exchange, cái bẫy ẩn sau một chuỗi rỗng.