Bốn người khiêng một cái ghế sofa thì nhanh gấp bốn; bốn người cùng khiêng một cọng lông thì chỉ chen chân nhau, chậm hơn một người làm. Số consumer của một listener hành xử đúng như thế: thêm người chỉ mua được thứ mà công việc thật sự cần khiêng. Bài này đo cái nghịch lý đó bằng con số — cùng một cấu hình, tăng consumer khi làm chậm gấp 4,4 lần, khi nhanh gấp 8.
Phần trước đo phía gửi. Bài này đo phía nhận, và mở đầu bằng mặc định mà tôi nghĩ gây thiệt hại nhiều nhất trong toàn bộ Spring AMQP.
Mặc định của @RabbitListener
Một listener trần, không cấu hình gì, đọc thẳng từ container bằng reflection:
| Thuộc tính | Giá trị mặc định |
|---|---|
| Kiểu container | SimpleMessageListenerContainer |
prefetch |
250 |
| Số consumer | 1 |
acknowledgeMode |
AUTO |
defaultRequeueRejected |
true |
Dòng prefetch = 250 đáng chú ý: client thuần mặc định không giới hạn, còn Spring tự đặt 250. Theo bảng của phần 19, 250 nằm ở vùng gần bão hoà — lựa chọn hợp lý cho listener làm việc nhẹ, nhưng quá cao nếu listener của bạn gọi HTTP ra ngoài.
Hai dòng cuối mới là chỗ nguy hiểm.
AUTO không phải autoAck
Tên gọi trùng nhau nhưng nghĩa ngược nhau. acknowledgeMode=AUTO của Spring nghĩa là container tự ack sau khi listener chạy xong mà không ném ngoại lệ — tức là ack thủ công, do framework làm hộ. Nó không phải autoAck=true của AMQP, thứ mà phần 16 đo được là mất 948 thông điệp khi consumer chết.
Nói cách khác, mặc định của Spring ở đây là an toàn. Muốn cái nguy hiểm thì phải khai acknowledgeMode=NONE.
Ném ngoại lệ là dựng lại vòng lặp thông điệp độc
defaultRequeueRejected=true nghĩa là: listener ném ngoại lệ thì thông điệp được requeue. Cho một thông điệp và một listener luôn ném, đo 3 giây:
1 thông điệp, listener ném ngoại lệ -> 18 032 vòng trong 3 giây (6 010 vòng/giây)
còn lại trong hàng đợi: 1 thông điệp
basicNack(requeue=true) mới rơi vào; với Spring thì chỉ cần một ngoại lệ không bắt được trong listener.
Và phần 20 cũng đo được rằng vòng lặp này chặn đứng mọi thông điệp khác trong hàng đợi. Một bản ghi hỏng cộng một NullPointerException là đủ để dừng cả đường ống.
Cách chữa, theo thứ tự nên thử:
spring:
rabbitmq:
listener:
simple:
default-requeue-rejected: false # hỏng thì đi thẳng sang DLX
Cộng một dead letter exchange trên hàng đợi. Cần thử lại thì dùng vòng có độ trễ của phần 21, đừng requeue thẳng.
Simple hay Direct
Hai kiểu container, đo trên 20 000 thông điệp với listener không làm gì:
| Container | Số consumer | Thông lượng |
|---|---|---|
| simple | 1 | 120 887 msg/s |
| simple | 4 | 106 112 msg/s |
| simple | 8 | 27 735 msg/s |
| direct | 1 | 136 969 msg/s |
| direct | 4 | 127 127 msg/s |
Hai điều đọc ra. Direct nhanh hơn Simple khoảng 13% ở mức một consumer — nó không dựng một luồng riêng cho mỗi consumer nên ít chuyển ngữ cảnh hơn. Và tăng số consumer làm mọi thứ chậm đi, tệ nhất là Simple với 8 consumer: chậm gấp 4,4 lần so với 1 consumer.
Nhưng bảng trên chỉ đúng khi listener không làm gì
Đó là lý do tôi đo lại với listener mất 2 ms mỗi thông điệp — tương đương một truy vấn CSDL nhẹ:
| Container | Số consumer | Thông lượng |
|---|---|---|
| simple | 1 | 399 msg/s |
| simple | 4 | 1 631 msg/s |
| simple | 8 | 3 219 msg/s |
| direct | 4 | 1 608 msg/s |
| direct | 8 | 3 211 msg/s |
Đảo chiều hoàn toàn: tăng consumer cho đúng tuyến tính — 4 consumer nhanh gấp 4,1 lần, 8 consumer gấp 8,1 lần. Và Simple với Direct giờ bằng nhau (1 631 so với 1 608; 3 219 so với 3 211).
Ghép hai bảng lại thành một luật, giống hệt kết luận của phần 19 về prefetch:
Số consumer chỉ mua được thứ mà công việc thật sự cần. Listener không làm gì thì thêm consumer chỉ thêm tranh chấp. Listener có việc thì nó chia việc gần như hoàn hảo. Và kiểu container chỉ tạo khác biệt ở vùng không có việc — nơi bạn sẽ không bao giờ ở trong hệ thống thật.
Nên đặt gì
spring:
rabbitmq:
listener:
simple:
default-requeue-rejected: false # đừng dựng vòng lặp độc
concurrency: 4 # theo số việc, không theo số CPU
max-concurrency: 8
prefetch: 20 # hạ xuống nếu listener gọi ra ngoài
Và nhớ rằng concurrency chỉ có nghĩa khi listener thật sự bận. Đo trước khi tăng — bảng đầu bài cho thấy đoán mò có thể làm chậm bốn lần.
Muốn biết listener của mình đang mở mấy consumer thật và giữ prefetch bao nhiêu, hỏi thẳng broker:
# listener cua ban dang giu bao nhieu, va co bao nhieu consumer that su
docker exec -u rabbitmq rmq rabbitmqctl -q list_consumers \
queue_name prefetch_count ack_required
Mỗi dòng là một consumer của container. Số dòng phải bằng concurrency bạn khai — ít hơn nghĩa là container chưa mở hết, mà prefetch_count bằng 250 nghĩa là bạn đang dùng mặc định của Spring.
Mẫu số chung
Song song chỉ trả công khi mỗi đơn vị việc đủ nặng để bõ. Bốn consumer khiêng cọng lông thì phần "khiêng" nhỏ hơn phần "va vào nhau" — chuyển ngữ cảnh, tranh khoá, đồng bộ — nên tổng thể chậm đi; bốn consumer khiêng ghế sofa thì phần khiêng áp đảo, và tốc độ lên gần tuyến tính. Biến quyết định không phải "máy mấy nhân" mà là việc-mỗi-đơn-vị so với chi phí phối hợp: khi việc tiến về 0, chi phí phối hợp vượt lên và thêm người thành phản tác dụng. Cùng đường cong ấy ở thread trên tác vụ nhẹ CPU, ở goroutine chẻ quá vụn, ở một map-reduce mà mỗi task chỉ cộng hai số — Amdahl bằng hình ảnh khiêng đồ. Nên đừng chỉnh concurrency theo số nhân hay theo linh cảm; đo việc-mỗi-đơn-vị trước, rồi mới quyết thêm bao nhiêu người.
Điều thứ hai, sắc hơn: một lần thử lại tức thì và không có trần không phải cơ chế hồi phục — nó là cái máy khuếch đại. defaultRequeueRejected=true biến đúng một thông điệp hỏng cộng một NullPointerException thành 6 010 vòng mỗi giây, quay mãi và chặn đứng mọi thông điệp lành phía sau. Cùng hình dạng ở một client retry không backoff dội bão vào một service đang hấp hối, một pod crash-loop nhai CPU, một job cron chồng lên chính nó vì lần trước chưa xong. Thử lại chỉ chữa được lỗi thoáng qua, và chỉ khi có ba thứ đi kèm: đếm số lần (trần), giãn thời gian (backoff), và một lối thoát cho cái không bao giờ thành công (dead-letter). Thiếu bất kỳ cái nào, "thử lại" chỉ là một vòng lặp vô hạn mang tên tử tế.
Bài sau: chuyển đổi thông điệp sang JSON, và cái header làm hai dịch vụ khác package không đọc được thông điệp của nhau.