Cách một khách sạn đánh số phòng theo tầng quyết định luôn chuyện phát chìa: đưa ai đó thẻ "tầng 3" là họ mở được mọi phòng bắt đầu bằng số 3, không cần phát từng chìa một. Cách đặt tên exchange trong RabbitMQ đúng là như vậy — tên chính là đơn vị phân quyền, và bạn chọn mô hình bảo mật của mình ngay lúc chọn cách gọi, dù có ý thức điều đó hay không. Bảy bài vừa rồi mổ từng kiểu exchange; bài này ghép chúng thành một quyết định thật: hệ của bạn nên có một exchange hay nhiều, và bao nhiêu hàng đợi thì bắt đầu đau.

Ba cách bố trí

Một exchange cho tất cả Một exchange mỗi miền Một exchange mỗi loại sự kiện
Số exchange 1 ~10 hàng trăm
Thêm bên nghe một queueBind một queueBind một queueBind
Thêm loại sự kiện không đụng gì không đụng gì sửa producer
Phân quyền ghi tất cả hoặc không theo miền theo từng sự kiện
Không gian khoá mọi đội dùng chung mỗi miền một cõi không cần khoá

Cột giữa là mặc định tôi khuyên, và lý do chính không phải hiệu năng — mà là cột "phân quyền ghi".

Phần 12 đã đo: quyền trong RabbitMQ là biểu thức chính quy trên tên tài nguyên. Với một exchange duy nhất, mọi dịch vụ có quyền gửi đều gửi được mọi sự kiện, kể cả sự kiện của đội khác. Với một exchange mỗi miền, set_permissions viết được thành ^don-hang\. và ranh giới tổ chức trở thành ranh giới kỹ thuật. Tên exchange là đơn vị phân quyền — chọn nó trước khi nghĩ tới bất cứ thứ gì khác.

Trong mỗi miền thì dùng topic exchange, vì phần 10 đo được nó chỉ chậm hơn direct 10% mà đổi lại có gom nhóm theo tiền tố.

Cái giá đo được của topology lớn

Dựng broker sạch rồi khai N hàng đợi durable, mỗi cái một binding vào một topic exchange:

0 hàng đợi 1 000 5 000
Thời gian khai báo — 3,0 s 16,4 s
Bộ nhớ broker 201 MiB — 520 MiB
rabbitmqctl list_queues 0,34 s 0,36 s 0,63 s
GET /api/queues tức thì — 0,30 s, 3,8 MiB JSON
Khởi động lại tới khi nhận kết nối 2,8 s 2,6 s 3,7 s

Điều bảng đó nói

Khởi động lại rẻ hơn tôi tưởng. 5 000 hàng đợi durable chỉ thêm khoảng một giây so với broker rỗng. Tôi vào phép đo này với giả định "topology lớn làm restart lâu" và số liệu bác bỏ nó. Đây không phải lý do để giữ topology nhỏ.

Dựng mới mới là chỗ đau. 3,3 mili giây mỗi hàng đợi nghe không đáng gì, nhân với 5 000 thành 16 giây — và đó là thời gian khởi động của ứng dụng nếu bạn khai báo toàn bộ topology lúc bật. Với hàng chục instance cùng khởi động sau một lần deploy, con số đó nhân lên.

Bộ nhớ tuyến tính, khoảng 65 KB mỗi hàng đợi rỗng. 5 000 hàng đợi ăn 319 MiB trước khi có một thông điệp nào. Đây là con số để nhân lên khi ai đó đề xuất "mỗi người dùng một hàng đợi".

Công cụ vận hành chậm dần. 3,8 MiB JSON cho một lần mở trang quản trị là thứ bạn sẽ cảm thấy lúc đang xử lý sự cố. Đây là cái giá thật của topology phình to, và nó không hiện ra trong bất kỳ biểu đồ hiệu năng nào.

Số binding không phải vấn đề, số đích mới là

Gom các phép đo trước lại thành một luật:

Đo ở Kết quả
Phần 8: direct, 1 → 500 binding phẳng (132k → 141k msg/s)
Phần 10: topic, 1 → 500 binding gần phẳng (124k → 113k msg/s)
Phần 11: headers, 1 → 500 binding sụp 93% (100k → 7k msg/s)
Phần 9: fanout, 1 → 100 đích sụp 68 lần (150k → 2,2k msg/s)

Hai dòng cuối hỏng vì hai lý do khác nhau: headers vì không có chỉ mục, fanout vì mỗi đích là một lần ghi thật. Hai dòng đầu cho thấy bạn có thể thoải mái thêm binding vào direct và topic.

Nên khi thiết kế, câu hỏi đúng là: mỗi thông điệp sẽ được ghi vào bao nhiêu hàng đợi? Con số đó nhân với lưu lượng ra tải thật của broker. Số exchange và số binding gần như không tính.

Quy ước đặt tên

<miền>.<phiên bản>            exchange:  don-hang.v1
<miền>.<thực thể>.<hành động>  routing:   don-hang.don.tao
<dịch vụ>.<mục đích>          hàng đợi:  ke-toan.ghi-so

Ba điều làm khổ người sau nếu bỏ qua:

Phiên bản trong tên exchange. Đổi schema sự kiện mà không có chỗ để chạy song song hai bản thì buộc phải deploy đồng thời mọi bên — và phần 6 cho thấy khai lại exchange với tham số khác là 406, tức không sửa tại chỗ được.

Tên hàng đợi mang tên dịch vụ sở hữu. Nhìn messages_unacknowledged tăng là biết ngay gọi ai. Hàng đợi tên emails thì không nói được gì.

Đừng nhét dữ liệu vào tên. don-hang.KH-8842 làm số hàng đợi phình theo số khách hàng — và bảng ở trên cho biết cái giá.

Ai khai báo topology

Một nơi duy nhất, không phải mọi dịch vụ. Phần 6 đo được rằng hai bên khai cùng một hàng đợi với tham số lệch nhau sẽ nhận 406 PRECONDITION_FAILED và channel đóng — lỗi nổ ra ở bên xui xẻo khởi động sau, chứ không phải bên khai sai.

Cách chắc chắn: một script hoặc một dịch vụ dựng topology; các dịch vụ khác chỉ dùng queueDeclarePassive để kiểm tra sự tồn tại. Chúng sẽ thất bại rõ ràng khi topology chưa sẵn sàng, thay vì âm thầm tạo ra một hàng đợi hơi khác.

Muốn tìm những hàng đợi đang phình bộ nhớ mà chẳng ai đọc, hỏi thẳng broker:

# hang doi nao dang ton bo nho nhat, va co ai doc chung khong
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues name messages consumers memory \
  | sort -k4 -rn | head

Dòng nào có consumers=0 mà memory lớn là hàng đợi đã bị bỏ quên — thứ chiếm phần lớn trong 319 MiB của bảng trên ở một hệ thống thật.

Mẫu số chung

Điều dễ coi nhẹ nhất ở đây là: đặt tên không phải chuyện thẩm mỹ — một quy ước đặt tên âm thầm trở thành mô hình phân quyền, bán kính ảnh hưởng, và ranh giới tổ chức của bạn. Vì quyền trong RabbitMQ là regex trên tên, cái tên exchange chính là đơn vị bảo mật; chọn cách gọi là chọn luôn ai được ghi vào đâu. Đây không phải nét riêng của RabbitMQ: một ARN của IAM phân quyền theo tiền tố đường dẫn, một namespace Kubernetes cô lập theo tên, tên package quyết định ranh giới module, tiền tố khoá S3 quyết định policy áp lên nó. Bài học: thiết kế tên như thiết kế một API ngay từ đầu, vì công cụ sẽ thực thi cấu trúc bạn mã hoá trong tên đó — đổi tên về sau đắt ngang đổi một API công khai.

Điều thứ hai, cụ thể và hay bị vấp: đừng bao giờ nhét dữ liệu không có trần vào một định danh cấu trúc. don-hang.KH-8842 biến mỗi khách hàng thành một hàng đợi, và số hàng đợi phình theo số khách — mỗi cái ăn 65 KB trước khi có một thông điệp, cộng thêm công cụ vận hành chậm dần. Đây là "bùng nổ lực lượng phần tử" (cardinality explosion) kinh điển, và nó rình ở khắp nơi người ta lấy một giá trị làm một cái tên: một nhãn metric mang user-id làm Prometheus nổ tung, một bảng-mỗi-tenant làm schema phình vô hạn, một khoá cache gắn tham số tuỳ ý. Quy tắc: định danh cấu trúc (tên hàng đợi, nhãn chỉ số, tên bảng) phải đến từ một tập hữu hạn, biết trước — còn dữ liệu biến thiên thì nằm bên trong, không nằm trên nhãn. Hỏi trước mỗi cái tên: "cái này mọc theo N nào?" — nếu N là số người dùng, bạn vừa dựng một quả bom cardinality.

Bài sau: acknowledgement — vì sao autoAck=true là chấp nhận mất thông điệp, và mất bao nhiêu.