Hình dung một hàng đợi như một cái kho có quầy tiếp nhận nhỏ phía trước và dãy kệ rộng phía sau. Nỗi sợ kinh điển khi vận hành RabbitMQ là cái quầy (RAM) chất đống tới lúc sập. Nhưng một cái kho hiện đại chuyển hàng thẳng ra kệ (đĩa) và chỉ giữ đúng một kiện trên quầy — nên tồn đọng không làm ngập cái quầy, nó làm đầy sàn kho, âm thầm, ở khoảng gấp đôi khối lượng hàng thật. Bài này đẩy thật một triệu thông điệp vào rồi đo xem cái quầy hay cái sàn mới là chỗ vỡ.

Bảng đo

Thông điệp 200 byte, persistent, hàng đợi classic mặc định:

Số thông điệp Bộ nhớ broker Đĩa message_bytes messages_ram Tốc độ gửi
0 202 MiB 1 MB — — —
100 000 — 38 MB 19 MiB 1 128 554 msg/s
500 000 221 MiB 188 MB 95 MiB 1 135 022 msg/s
1 000 000 221 MiB 376 MB 191 MiB 1 127 659 msg/s

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

Bộ nhớ đứng yên. Từ 500 nghìn lên một triệu thông điệp, mem_used không nhúc nhích: 221 MiB cả hai lần, so với 202 MiB lúc broker trống. Một triệu thông điệp chứa 191 MiB dữ liệu chỉ làm broker tốn thêm khoảng 19 MiB RAM.

messages_ram luôn bằng 1. Phần 7 đã thấy con số này ở quy mô 20 nghìn và tôi đã dùng nó để giải thích vì sao persistent không chậm hơn transient. Ở quy mô một triệu nó vẫn là 1: classic queue hiện đại giữ đúng một thông điệp trong RAM, phần còn lại nằm trên đĩa.

Thông lượng không giảm. 128k, 135k, 128k msg/s — hàng đợi có một nghìn hay một triệu thông điệp thì tốc độ ghi vào như nhau.

Nỗi sợ "hàng đợi dồn lại làm broker hết RAM" thuộc về RabbitMQ đời cũ, khi classic queue giữ thông điệp trong bộ nhớ cho tới lúc bị áp lực mới đẩy xuống đĩa. Với bản hiện tại, cái phình lên là đĩa, không phải RAM.

Rút hết một triệu

rút hết trong 9,0 s (110 792 msg/s)

Không có hiện tượng "hàng đợi quá lớn nên chậm". Chín giây cho một triệu thông điệp, với prefetch 500 và ack theo lô — đúng vùng thông lượng mà phần 19 đo được cho hàng đợi rỗng.

Đĩa mới là thứ tăng

Cột đĩa tăng tuyến tính và gấp khoảng hai lần dữ liệu thật: 191 MiB thông điệp chiếm 376 MB trên đĩa. Phần dôi ra là chỉ mục, siêu dữ liệu mỗi thông điệp và các đoạn tệp chưa được dọn.

Con số để mang đi: hàng đợi tốn khoảng gấp đôi kích thước dữ liệu của nó trên đĩa. Một triệu thông điệp 1 KB là khoảng 2 GB — và đó mới là một hàng đợi.

x-queue-mode: lazy giờ không còn nghĩa gì

khai x-queue-mode=lazy -> chấp nhận (không lỗi)

Broker vẫn nhận tham số này, nhưng bảng đo ở trên cho thấy vì sao nó thừa: hàng đợi thường đã hành xử như lazy rồi. Chế độ lazy sinh ra để ép thông điệp xuống đĩa thay vì giữ trong RAM; giờ đó là mặc định.

Đây là kiểu thay đổi khó nhận ra nhất khi nâng cấp: cấu hình cũ không báo lỗi, chỉ lặng lẽ trở thành vô nghĩa. Nếu bạn còn đặt x-queue-mode ở đâu đó, hãy bỏ đi — giữ lại chỉ làm người sau tưởng nó đang có tác dụng.

Vậy còn phải lo gì

Bộ nhớ không còn là mối lo hàng đầu, nhưng ba thứ khác thì có:

Đĩa đầy. RabbitMQ có ngưỡng đĩa trống; chạm ngưỡng là broker chặn mọi producer lại. Đó là cách nó tự vệ, và với ứng dụng thì nó biểu hiện thành basicPublish treo — chứ không phải một lỗi rõ ràng.

Thời gian rút. Chín giây cho một triệu là nhanh, nhưng nếu consumer của bạn mất 20 ms mỗi thông điệp như phần 25, thì một triệu thông điệp là năm tiếng rưỡi với 8 consumer. Tồn đọng không giết broker; nó giết SLA.

Hàng đợi bị bỏ quên. Phần 15 đo được khoảng 65 KB mỗi hàng đợi rỗng, và gợi ý cách tìm: hàng đợi có consumers=0 mà messages lớn. Đó mới là chỗ đĩa của bạn biến mất.

Cách chặn từ đầu là x-max-length hoặc x-max-length-bytes cộng một dead letter exchange — phần 14 đo được rằng thiếu DLX thì hàng đợi đầy sẽ âm thầm vứt thông điệp cũ nhất.

Muốn biết đĩa của mình đang chảy đi đâu, hỏi broker hàng đợi nào nặng nhất và có ai rút không:

# hang doi nao dang an dia nhieu nhat, va co ai rut khong
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues \
    name messages message_bytes consumers | sort -k3 -rn | head

Nhân cột message_bytes với hai để ước lượng chỗ nó chiếm trên đĩa. Dòng nào consumers=0 là dòng sẽ còn lớn mãi.

Mẫu số chung

Một nỗi sợ — và cái nút cấu hình sinh ra để dỗ nó — có thể sống lâu hơn cái thay đổi từng làm nó có thật. "Tồn đọng sẽ vắt kiệt RAM" và x-queue-mode: lazy đều thuộc về một RabbitMQ đời cũ; cách chữa đã thành mặc định, và cái nút giờ lặng lẽ chẳng làm gì. Đó là cấu hình theo tập tục (cargo-cult): một thiết lập được chép tới đời sau mà không còn tác dụng, và nó tệ hơn cả việc vắng mặt, vì nó nói dối người đọc sau về thứ đang bảo vệ họ. Cùng loại hoá thạch nằm rải khắp mọi ngăn xếp — một cờ JVM cho bộ GC đã biến mất, một tinh chỉnh nhân cho phần cứng đã nghỉ hưu, một cấu hình retry cho client giờ tự retry bên trong. Định kỳ kiểm lại xem mỗi cái nút phòng thủ có còn ứng với một hành vi thật không; cái nào không thì xoá.

Điều thứ hai: khi một tài nguyên thôi làm nút thắt, áp lực không biến mất — nó dời chỗ. RAM ngừng tăng, nên ràng buộc dời sang đĩa (gấp đôi dữ liệu), sang thời gian rút (tồn đọng không giết broker vẫn giết SLA — năm tiếng rưỡi ở đây), và sang chi phí mỗi-hàng-đợi. "Đã giải quyết xong" gần như luôn có nghĩa "đã dời đi"; việc phải làm là đi tìm lại giới hạn mới, đừng tưởng không còn giới hạn nào. Và cái giới hạn mới hỏng theo một kiểu mới: chạm ngưỡng đĩa thì broker chặn producer, hiện ra thành basicPublish treo chứ không phải một lỗi — nên cùng với nút thắt, bạn thừa kế luôn một kiểu hỏng-trong-im-lặng mới phải canh.

Bài sau: quorum queue — cùng phép đo này, với con số khác hẳn.