Phần trước nói về vỏ đựng. Bài này nói về thứ nằm trong đó, và kết thúc bằng một phép đo đi ngược hẳn câu khuyên mà ai cũng nghe: "bật persistent thì chậm".
Một thông điệp có ba phần
| Phần | Là gì | Broker có đọc không |
|---|---|---|
| Body | Mảng byte thuần | Không. Hoàn toàn không |
| Properties | 14 trường có sẵn trong giao thức | Chỉ 4 trường |
| Headers | Bảng khoá–giá trị bạn tự đặt | Không, trừ khi có plugin |
Body là mảng byte — RabbitMQ không biết đó là JSON, Protobuf hay ảnh, và không quan tâm. Đặt contentType là để consumer biết, không phải để broker biết.
Broker chỉ thật sự dùng bốn thuộc tính
Trong 14 trường của BasicProperties, broker chỉ hành động theo deliveryMode, expiration, priority và userId. Mười trường còn lại — messageId, correlationId, contentType, appId, type, replyTo, timestamp… — chỉ được chở đi nguyên vẹn.
Đó không phải điều xấu. correlationId và replyTo là nền của mẫu request–reply; messageId là nền của việc chống xử lý trùng. Chỉ cần nhớ rằng không có gì trong broker tự dùng chúng giúp bạn.
expiration: broker tự dọn
sau 0,3 giây : 5 thông điệp
sau 1,5 giây : 0 thông điệp
Đặt expiration("500") — đơn vị mili giây, kiểu chuỗi — thì thông điệp tự biến mất sau nửa giây. Ghép với dead letter exchange, đây là nền của cơ chế thử lại có độ trễ ở bài 21.
priority: vô tác dụng nếu hàng đợi không khai
Gửi 5 thông điệp priority 0→4 vào hai hàng đợi, một cái khai x-max-priority=10, cái kia không:
khong-uu-tien : [uu-tien-0, uu-tien-1, uu-tien-2, uu-tien-3, uu-tien-4]
co-uu-tien : [uu-tien-4, uu-tien-3, uu-tien-2, uu-tien-1, uu-tien-0]
Hàng đợi thường bỏ qua priority hoàn toàn và giao đúng thứ tự vào. Không có lỗi, không có cảnh báo — bạn chỉ nhận ra khi thấy việc gấp vẫn xếp sau việc thường. Ưu tiên là thuộc tính của hàng đợi, khai lúc tạo, và phần 6 cho thấy khai lại để thêm nó vào sẽ nhận 406.
userId: trường duy nhất broker đem đi đối chiếu
userId="guest" -> gửi được, hàng đợi có 1
userId="ke-mao-danh" -> BỊ CHẶN: 406 PRECONDITION_FAILED - user_id property set to
'ke-mao-danh' but authenticated user was 'guest'
Khai userId khác tài khoản đang đăng nhập là bị chặn ngay. Đây là cách rẻ nhất để consumer tin được thông điệp thật sự đến từ ai — nhưng nhớ rằng nó chỉ đúng khi mỗi dịch vụ có tài khoản riêng, mà phần 2 cho thấy image Docker mặc định cho tất cả dùng chung guest.
Header đi qua broker rồi đổi kiểu
Đây là cái bẫy tôi nghĩ nhiều người Java đã mất một buổi vì nó:
tin-cay giá trị=true kiểu Java=java.lang.Boolean
lan-thu giá trị=3 kiểu Java=java.lang.Integer
nguon giá trị=web kiểu Java=…impl.LongStringHelper$ByteArrayLongString
so sánh nguon.equals("web") -> false
phải viết nguon.toString().equals("web") -> true
Boolean và Integer về nguyên kiểu. Chuỗi thì không. AMQP mã hoá chuỗi dài thành longstr, và client Java trả về một LongString chứ không phải String. In ra thì thấy web, log cũng thấy web, mà equals("web") vẫn false.
.toString() trên giá trị header lấy từ thông điệp trước khi so sánh. Đây là loại lỗi mà mọi câu lệnh in ra đều nói bạn đúng.
deliveryMode=2 nghĩa là gì
Gửi 500 thông điệp persistent và 500 transient vào cùng một hàng đợi durable, rồi khởi động lại broker:
trước khi khởi động lại: 1000 thông điệp
sau khi khởi động lại : 500 thông điệp
Hàng đợi bền giữ được chính nó; deliveryMode=2 giữ được thông điệp. Cần cả hai. Đây là cặp mà phần 6 mới nói một nửa.
…và không nghĩa là "chậm hơn"
Câu khuyên phổ biến là persistent tốn kém vì phải ghi đĩa. Đo 20 000 thông điệp, có publisher confirms (để chờ broker xác nhận thật chứ không chỉ ghi vào socket), hâm nóng trước, ba lượt, đảo thứ tự mỗi lượt:
| Ba lượt (msg/s) | Trung vị | |
|---|---|---|
deliveryMode=1 (transient) |
132 048 · 134 516 · 139 248 | 134 516 |
deliveryMode=2 (persistent) |
146 845 · 176 863 · 192 123 | 176 863 |
Persistent nhanh hơn 31%. Tôi đã nghi đây là lỗi phép đo — chạy lượt đầu chưa hâm nóng thì đúng là transient bị thiệt — nhưng sau khi hâm nóng và đảo thứ tự, kết quả vẫn giữ nguyên. Nên tôi đi hỏi chính broker:
soi-1 (transient) : messages=20000 persistent=0 messages_ram=1
soi-2 (persistent): messages=20000 persistent=20000 messages_ram=1
messages_ram=1 ở cả hai. Hai mươi nghìn thông điệp transient cũng không nằm trong RAM — chúng đã ở trên đĩa rồi. Classic queue của RabbitMQ hiện đại ghi thân thông điệp xuống đĩa bất kể deliveryMode; cờ đó quyết định thứ được khôi phục sau khi khởi động lại, chứ không quyết định có chạm đĩa hay không.
Nghĩa là phần "tiết kiệm" mà transient hứa hẹn phần lớn không tồn tại nữa, còn đường đi của nó thì lại thêm việc sổ sách để đánh dấu "đừng khôi phục cái này".
Và nhớ rằng đây là phép đo trên máy tôi, với thông điệp 23 byte, hàng đợi classic, không cluster. Đĩa chậm hơn hoặc thông điệp lớn hơn sẽ cho hình khác — nhưng hướng của kết luận thì không đổi, vì nó nằm ở chỗ cả hai đều đã ghi đĩa.
Thử ba mươi giây
# hang doi cua ban dang giu bao nhieu thong diep khong the khoi phuc?
curl -su guest:guest "localhost:15672/api/queues/%2F/<ten-hang-doi>" \
| jq '{messages, messages_persistent, messages_ram}'
messages trừ messages_persistent chính là số thông điệp sẽ bốc hơi trong lần bảo trì tới. Nếu hiệu đó khác 0 trên một hàng đợi nghiệp vụ, có ai đó đã quên deliveryMode.