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
Đây chính xác là vòng lặp mà phần 20 đo được với client thuần — 5 834 vòng/giây ở đó, 6 010 ở đây. Khác biệt duy nhất: với client thuần bạn phải tự viết 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.

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.

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.

Thử ba mươi giây

# 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.