Bạn để ý chưa: siêu thị có một hàng chung dồn về quầy nào trống trước thì luôn nhanh hơn kiểu mỗi quầy một hàng riêng. Vì khi đã đứng vào hàng của một quầy, mà đúng người phía trước bạn lại thanh toán chậm, thì bạn kẹt — không nhảy sang quầy đang rảnh được nữa. Prefetch của RabbitMQ chính là cái nút quyết định bạn đang chạy kiểu nào: giao trước một xấp việc cho từng consumer (mỗi quầy một hàng), hay chỉ giao khi consumer thật sự rảnh (một hàng chung). Và hai kiểu đó cho hai kết quả ngược nhau.
Phần 16 đã đo một điều về prefetch: nó bị bỏ qua hoàn toàn khi autoAck=true. Bài này đo điều còn lại — với ack thủ công, con số đó đáng giá bao nhiêu.
Prefetch là gì
ch.basicQos(100); // tối đa 100 thông điệp chưa ack tại một thời điểm
Nó không phải kích thước lô, cũng không phải bộ đệm. Nó là trần của cột messages_unacknowledged mà phần 4 đo được: broker đẩy thông điệp tới khi consumer đang giữ đủ số đó chưa ack, rồi dừng lại chờ.
Đường cong thông lượng
20 000 thông điệp, ack từng cái, không xử lý gì thêm, trung vị 3 lượt:
| Prefetch | Thông lượng | So với trần |
|---|---|---|
| 1 | 5 991 msg/s | 4% |
| 5 | 22 063 msg/s | 15% |
| 10 | 34 662 msg/s | 24% |
| 50 | 85 685 msg/s | 58% |
| 100 | 112 249 msg/s | 76% |
| 500 | 140 957 msg/s | 96% |
| 1 000 | 135 528 msg/s | 92% |
| không giới hạn | 147 070 msg/s | 100% |
Từ 1 lên 100 là 19 lần. Từ 100 lên không giới hạn chỉ còn 1,3 lần, và sau 500 thì đường cong nằm ngang — chênh lệch giữa 500 và 1 000 nằm trong nhiễu.
Hình dạng đó có lý do đơn giản: với prefetch=1, mỗi thông điệp cần trọn một vòng broker → consumer → ack → broker trước khi cái tiếp theo được gửi. 5 991 msg/s tương đương 0,17 mili giây mỗi vòng — đó là độ trễ mạng, không phải công sức xử lý. Tăng prefetch là cho phép nhiều thông điệp cùng bay, đúng như confirms bất đồng bộ làm ở chiều ngược lại.
Mặt trái không ai đo
Nếu chỉ có bảng trên thì kết luận sẽ là "đặt prefetch thật cao". Nên tôi đo thêm tình huống thật hơn: hai consumer chung một hàng đợi, một nhanh một chậm. Consumer A xử lý 1 ms mỗi thông điệp, consumer B mất 20 ms. Gửi 400 thông điệp:
| Prefetch | Consumer nhanh | Consumer chậm | Tổng thời gian |
|---|---|---|---|
| 1 | 374 | 26 | 0,60 s |
| 10 | 369 | 31 | 0,71 s |
| 100 | 290 | 110 | 2,58 s |
Với prefetch=1, broker chỉ giao khi consumer thật sự rảnh, nên việc tự chảy về phía người làm nhanh — 374 so với 26. Đó là cân bằng tải thật, và nó chỉ có được khi bạn từ chối đẩy trước.
Chọn số nào
Hai bảng trên là hai đầu của cùng một đánh đổi, và câu hỏi quyết định là thời gian xử lý một thông điệp:
| Tình huống | Prefetch | Vì sao |
|---|---|---|
| Xử lý rất nhanh (dưới 1 ms), một consumer | 100–500 | Vòng mạng là cổ chai, bảng một áp dụng |
| Xử lý lâu (hàng chục ms trở lên) | 1–5 | Bảng hai áp dụng; thông lượng bị giới hạn bởi công sức, không phải mạng |
| Nhiều consumer tốc độ khác nhau | thấp | Chỉ prefetch thấp mới cân bằng tải được |
| Thông điệp lớn | thấp | Prefetch nhân với kích thước là bộ nhớ consumer phải giữ |
Quy tắc thực dụng: nếu xử lý một thông điệp mất lâu hơn nhiều so với một vòng mạng, prefetch cao không mua được gì mà chỉ làm phân phối việc tệ đi. Bảng một chỉ đúng khi consumer không làm gì cả — và consumer thật thì luôn làm gì đó.
Số 20 ms trong phép đo thứ hai không phải cực đoan: một lời gọi HTTP hay một truy vấn CSDL đều nằm quanh đó. Nếu consumer của bạn gọi ra ngoài, bạn đang ở bảng hai.
Prefetch cũng là hạn mức bộ nhớ
Thông điệp chưa ack nằm trong bộ nhớ của cả hai phía: client giữ chúng trong hàng đợi nội bộ, broker giữ chúng ở trạng thái unacked. prefetch=10000 với thông điệp 1 MB là 10 GB mà không có gì báo trước. Đây là lý do "không giới hạn" tuy nhanh nhất trong bảng một nhưng không bao giờ nên dùng ngoài phòng thí nghiệm.
Muốn biết consumer của mình đang đặt prefetch bao nhiêu và giữ bao nhiêu việc, hỏi thẳng broker:
# prefetch that su cua tung consumer, va no dang giu bao nhieu
docker exec -u rabbitmq rmq rabbitmqctl -q list_consumers \
queue_name prefetch_count ack_required
prefetch_count=0 nghĩa là không giới hạn — và nếu cột bên cạnh là false thì như phần 16 đã đo, con số đó chẳng có ý nghĩa gì cả.
Mẫu số chung
Cốt lõi của bài là chuyện giao việc trước so với giao khi rảnh. Đẩy sẵn một xấp việc cho mỗi người làm cho thông lượng đẹp lên (không ai phải chờ vòng mạng), nhưng cái xấp đó đông cứng ngay lúc giao: người làm chậm ôm một đống, người làm nhanh xong sớm rồi ngồi không, và việc đã giao thì không đòi lại. Kéo-khi-rảnh thì ngược lại — việc tự chảy về phía ai đang trống, tức là tự cân bằng tải mà không cần biết trước ai nhanh ai chậm. Cùng một lựa chọn ấy có mặt ở khắp nơi: một load balancer chia theo round-robin (giao trước) so với least-connections (giao cho ai đang rảnh nhất), một scheduler có work-stealing so với chia phần cố định, cách Kafka gán partition cứng cho consumer. Nguyên tắc: khi mỗi đơn vị việc dài không đều nhau, đừng cam kết nó cho một người làm sớm hơn mức cần — để dành quyền giao tới phút chót là để dành khả năng cân bằng.
Điều thứ hai, âm thầm hơn: mọi cái đệm chen giữa hai bên để "cho chạy nhanh hơn" đồng thời là bộ nhớ ai đó phải giữ và độ trễ giấu vào trong. Prefetch cao lấp đầy hàng đợi nội bộ của consumer y như một bộ đệm mạng phình to (bufferbloat) làm ping vọt lên, y như một Go channel dung lượng lớn nuốt áp lực ngược cho tới lúc hết RAM, y như một buffer ghi log ngậm dữ liệu rồi mất khi tiến trình chết. Cái đệm không xoá được nút thắt — nó chỉ dời chỗ và làm nó khó thấy hơn, đổi lấy bộ nhớ và độ trễ đuôi. Nên mỗi lần nới một buffer để lấy tốc độ, hãy hỏi luôn: bộ nhớ này ai giữ, và nó giấu mất bao nhiêu thời gian chờ khỏi tầm mắt.
Bài sau: redelivery và thông điệp độc — chuyện gì xảy ra với những thông điệp bị trả đi trả lại.