Khi hiệu năng có vấn đề, phản xạ đầu tiên của nhiều người là đi vặn các núm cấu hình của broker — như người thợ đổ lỗi cho động cơ mỗi khi xe chạy chậm. Ba mươi tám bài, mỗi bài một phép đo, đo được điều ngược lại: khoảng cách 122 lần giữa nhanh và chậm nằm trọn trong cách client dùng broker, không ở một núm nào của broker cả — giống như phần lớn thời gian phí trên đường không phải tại chiếc xe, mà tại cách đi: bắt một chiếc taxi mới cho từng dãy nhà thay vì ngồi yên một chuyến. Bài này gom lại thành thứ dùng được: bảng những gì đáng sửa, xếp theo mức tác động — và bảng những gì đừng mất thời gian.

Phép đo tổng kết

Hai cách viết cùng một việc: gửi và tiêu thụ 2 000 thông điệp persistent, cùng một mức đảm bảo (có publisher confirms, ack thủ công, không mất thông điệp nào).

cách tệ  : connection mới cho mỗi thông điệp, confirm từng cái, prefetch 1, ack từng cái
cách tốt : dùng lại channel, confirm theo lô, prefetch 100, ack theo lô
Thời gian
Cách tệ 6,28 s
Cách tốt 0,05 s
122 lần

Không có tham số broker nào được đổi. Không có phần cứng nào khác. Toàn bộ chênh lệch nằm ở bốn quyết định trong mã ứng dụng — và cả bốn đều là mặc định hoặc thói quen mà người mới hay chọn.

Thứ thật sự đổi được hiệu năng

Xếp theo mức tác động đo được:

Thay đổi Tác động Đo ở
Dùng lại channel thay vì mở connection mỗi thông điệp 1 128× Phần 3
Bỏ retry chặn luồng, chuyển sang hàng đợi chờ ~1 000× thời gian mẻ Phần 29
Confirm bất đồng bộ thay vì confirm từng thông điệp 68× Phần 17
Giảm số hàng đợi đích của fanout (100 → 1) 68× Phần 9
Prefetch 1 → 100 (với ack thủ công) 19× Phần 19
Dùng topic thay headers khi có nhiều binding 15× ở 500 binding Phần 11
Dùng classic thay quorum (nếu chấp nhận được) 3,5× trên cụm Phần 34
Ack theo lô thay vì ack từng cái +32% Phần 16
Tăng số consumer khi listener có việc thật tuyến tính tới 8× Phần 25

Bốn dòng đầu đều nằm trong mã ứng dụng, không phải cấu hình broker. Đó là kết luận lớn nhất của cả sê-ri: gần như mọi vấn đề hiệu năng RabbitMQ mà tôi đo được đều nằm ở cách client dùng nó.

Thứ đo ra không khác gì

Sáu điều thường được khuyên, và con số thật:

Lời khuyên Đo ra Đo ở
"Dùng transient cho nhanh" persistent nhanh hơn 31% Phần 7
"Bớt binding cho exchange nhẹ" 1 → 500 binding: phẳng Phần 8, 10
"Viết client thuần thay vì Spring cho nhanh" chênh 4% Phần 24
"Dùng default exchange cho đỡ một bước" chênh 11%, trong nhiễu Phần 12
"Tắt TLS để lấy thông lượng" chênh 5% Phần 37
"Dùng ảnh không có giao diện cho test nhanh" 2,9 s so với 2,8 s Phần 30

Dòng đầu là dòng tôi thích nhất. Nó đi ngược lời khuyên phổ biến nhất về RabbitMQ, và phần 7 tìm ra lý do bằng một chỉ số: messages_ram = 1 ở cả hai chế độ. Thông điệp transient cũng đã nằm trên đĩa rồi; cờ đó chỉ quyết định thứ được khôi phục sau khi khởi động lại.

Ba dòng cuối đều là đánh đổi an toàn hoặc tiện lợi lấy vài phần trăm — mà vài phần trăm đó nằm trong nhiễu của chính phép đo.

Thứ tự nên làm

1. Sửa vòng đời connection và channel. Một connection cho ứng dụng, một channel cho mỗi luồng. Đây là 1 128 lần, và nó miễn phí.

2. Chọn đúng chế độ confirm. Gửi ít thì confirm từng cái cho đơn giản; gửi nhiều thì bất đồng bộ. Đừng để mặc định "không confirm" trên dữ liệu quan trọng — phần 24 đo được rằng Spring Boot tắt nó theo mặc định.

3. Đặt prefetch theo thời gian xử lý. Listener nhanh thì 100–500; listener gọi ra ngoài thì 1–5. Phần 19 cho thấy đoán sai theo cả hai hướng đều đắt.

4. Đừng chặn luồng consumer để chờ. Mọi Thread.sleep trong listener — kể cả cái nằm trong thư viện retry — là thời gian chết nhân với số luồng.

5. Đếm số đích, không đếm số binding. Câu hỏi đúng khi thiết kế topology là "mỗi thông điệp sẽ được ghi mấy lần", không phải "exchange này nhanh không".

6. Chỉ sau khi làm hết năm việc trên mới đụng tới broker. Trong toàn bộ sê-ri này, tôi không sửa một dòng cấu hình broker nào để lấy hiệu năng — mọi con số ở bảng đầu đều đến từ phía client.

Muốn biết mình đang đứng ở đâu trên bảng này, hỏi broker hai câu:

docker exec -u rabbitmq rmq rabbitmqctl -q list_connections user channels
docker exec -u rabbitmq rmq rabbitmqctl -q list_consumers queue_name prefetch_count ack_required

Lệnh đầu: số connection tăng theo tải nghĩa là bạn đang ở dòng số một của bảng. Lệnh sau: prefetch_count bằng 0 hoặc bằng 250 mặc định của Spring nghĩa là chưa ai chọn nó theo thời gian xử lý thật.

Mẫu số chung

Cái nút thắt hiệu năng gần như không bao giờ là cái núm bí hiểm bạn định đi vặn — nó là cách phần mã của bạn dùng hệ thống, và những cú thắng lớn nhất lại miễn phí: dùng lại cái tài nguyên đắt thay vì mở mới liên tục, gộp nhiều lượt khứ hồi thành một. Đúng một khuôn ấy ngoài RabbitMQ: truy vấn N+1 của ORM, một API "lắm mồm" gọi trăm lượt thay vì một lượt gộp, mở connection CSDL cho mỗi request, HTTP không keep-alive. Tất cả sống trong mã ứng dụng, không trong cấu hình máy chủ — nên trước khi đụng vào động cơ, hãy đo thời gian thật sự trôi đi đâu; nó gần như luôn ở chỗ bạn lặp lại một việc đắt mà lẽ ra chỉ phải trả một lần.

Điều thứ hai: phần lớn "mẹo hiệu năng" truyền miệng chỉ là nhiễu. Transient-cho-nhanh, tắt-TLS, client-thuần-hơn-framework — cả ba đo ra nằm trong sai số. Một niềm tin nghe hợp lý và được nhắc đi nhắc lại không phải là bằng chứng; nó chỉ là một giả thuyết chưa ai đo lại. Và giả thuyết đáng ngờ nhất chính là cái vừa phổ biến vừa chưa từng bị kiểm chứng — như "transient nhanh hơn persistent", hoá ra ngược lại, vì messages_ram=1 ở cả hai chế độ nên cái cờ ấy mang nghĩa khác hẳn điều người ta tưởng. Đừng trả giá bằng an toàn (tắt TLS) hay bằng độ tin cậy (bỏ persistent) để đổi lấy "vài phần trăm" mà bạn còn chưa đo — rất có thể vài phần trăm đó không tồn tại.

Bài sau là bài cuối: RabbitMQ hay Kafka, và nhìn lại bốn mươi bài.