RabbitMQ: durable, persistent và quorum queue
Tiếp đi: RabbitMQ chết thì message có mất không, và quorum queue.
Bài 8.3 lo hai đầu: producer → broker và broker → consumer. Bài này lo phần giữa: broker tự giữ message thế nào khi restart hoặc mất máy.
1. Bài toán: restart RabbitMQ trong giờ cao điểm
shop-app đang có 5.000 OrderPlaced nằm chờ trong notification.order-placed vì email provider chậm. Đội vận hành restart RabbitMQ để nâng version. Rồi một hôm khác, cả VM chạy RabbitMQ chết hẳn ổ đĩa.
2. Vấn đề
- Queue không durable: restart xong queue biến mất, binding mất theo.
- Message không persistent: chỉ nằm trong RAM, restart là mất.
- Durable + persistent nhưng chỉ một node: mất ổ đĩa là mất hết; trong lúc node chết, producer không publish được.
3. Giải pháp
| Thuộc tính | Thuộc về | Ý nghĩa |
|---|---|---|
| Durable | Exchange, queue | Định nghĩa còn sau khi broker restart |
| Persistent (delivery mode 2) | Message | Message được ghi xuống đĩa |
| Replication | Queue | Message có bản sao trên nhiều node |
Cần cả ba nếu không muốn mất message. Spring AMQP mặc định đã gửi message persistent, và QueueBuilder.durable() tạo queue durable.
Lịch sử replication trong RabbitMQ:
- Classic mirrored queue (cũ): một master + các mirror. Có nhiều vấn đề khi mạng chập chờn (đồng bộ lại toàn bộ queue, mất message khi partition). Bị deprecated từ 3.9 và bị gỡ bỏ trong RabbitMQ 4.0.
- Quorum queue (từ 3.8, khuyến nghị hiện nay): replicate bằng thuật toán đồng thuận Raft.
- Stream (từ 3.9): log append-only, cũng replicate, đọc lại được (giống Kafka).
Quorum queue hoạt động thế nào:
- Mỗi queue có một leader và các follower trên các node khác nhau.
- Message được ghi vào log của leader, gửi sang follower. Khi đa số (quorum) đã ghi, broker mới gửi confirm cho producer.
- Leader chết → các follower bầu leader mới, queue tiếp tục chạy.
4. Code từng bước
Bước 1: Dựng cluster 3 node (docker compose, rút gọn). Các node phải dùng chung Erlang cookie, lấy từ secret/biến môi trường, không ghi cứng:
services:
rabbit1:
image: rabbitmq:4-management
hostname: rabbit1
environment:
RABBITMQ_ERLANG_COOKIE: ${RABBIT_COOKIE}
RABBITMQ_DEFAULT_USER: ${RABBIT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBIT_PASS}
volumes: [ "rabbit1-data:/var/lib/rabbitmq" ]
rabbit2:
image: rabbitmq:4-management
hostname: rabbit2
environment:
RABBITMQ_ERLANG_COOKIE: ${RABBIT_COOKIE}
volumes: [ "rabbit2-data:/var/lib/rabbitmq" ]
rabbit3:
# tương tự rabbit2
# join rabbit2, rabbit3 vào cluster của rabbit1:
# docker exec rabbit2 rabbitmqctl stop_app
# docker exec rabbit2 rabbitmqctl join_cluster rabbit@rabbit1
# docker exec rabbit2 rabbitmqctl start_app
Production thường dùng RabbitMQ Cluster Operator trên Kubernetes hoặc dịch vụ managed (Amazon MQ, CloudAMQP) thay vì tự join tay.
Bước 2: Config Spring kết nối nhiều node
spring:
rabbitmq:
addresses: rabbit1:5672,rabbit2:5672,rabbit3:5672
username: ${RABBIT_USER}
password: ${RABBIT_PASS}
publisher-confirm-type: correlated
Client thử lần lượt các địa chỉ, node nào sống thì kết nối. Spring AMQP tự kết nối lại khi mất connection.
Bước 3: Khai báo quorum queue
@Bean
Queue inventoryOrderPlaced() {
return QueueBuilder.durable("inventory.order-placed")
.quorum() // x-queue-type=quorum
.deliveryLimit(5) // chặn requeue loop (bài 8.3)
.build();
}
Không đổi được loại queue của queue đã tồn tại: declare lại với x-queue-type khác sẽ lỗi PRECONDITION_FAILED. Muốn chuyển classic → quorum phải tạo queue mới, chuyển binding, cho consumer đọc hết queue cũ rồi xóa.
Có thể ép mọi queue mới thành quorum ở phía server bằng cấu hình default_queue_type = quorum cho virtual host.
Bước 4: Test failover
docker exec rabbit1 rabbitmq-queues quorum_status inventory.order-placed
# thấy leader trên rabbit1, follower rabbit2, rabbit3
docker stop rabbit1
curl -X POST http://localhost:8081/orders -d '...' -H "Content-Type: application/json"
# vẫn đặt hàng được; leader mới trên rabbit2 hoặc rabbit3; inventory vẫn nhận event
docker stop rabbit2
# chỉ còn 1/3 node → mất quorum → queue không nhận message mới, confirm không về
# producer nhận timeout confirm → Outbox giữ event lại, gửi sau
5. Cách hoạt động
producer ── publish ──> rabbit1 (leader)
│ append log
├──> rabbit2 (follower) ghi ✅
└──> rabbit3 (follower) chậm...
2/3 đã ghi = majority
producer <── confirm ack ──┘
rabbit1 chết:
rabbit2, rabbit3 bầu leader mới (Raft election) → queue tiếp tục
rabbit1 sống lại → thành follower, đồng bộ phần log thiếu
Vì sao cluster 3 node? Quorum của N node là N/2 + 1:
| Số node | Quorum | Chịu được mất |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 (tệ như 1 node) |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
Số lẻ là hợp lý: 4 node vẫn chỉ chịu mất 1, tốn thêm máy mà không thêm khả năng chịu lỗi.
6. So sánh
| Tiêu chí | Classic queue | Quorum queue | Stream |
|---|---|---|---|
| Replication | Không (mirrored đã bị gỡ ở 4.0) | Raft, majority | Có |
| Sau khi consume | Xóa | Xóa | Giữ theo retention, đọc lại bằng offset |
| Delivery limit (chống poison) | Không | Có | Không áp dụng |
| Non-durable / exclusive | Có | Không, luôn durable | Không |
| Priority | Có (x-max-priority) | Có từ 4.0 (2 mức: cao / thường) | Không |
| Phù hợp | Queue tạm, dữ liệu không quan trọng | Event nghiệp vụ cần an toàn | Fan-out lớn, replay, lưu lâu |
7. Khi nào nên dùng / không nên dùng
- Quorum queue: mặc định cho queue nghiệp vụ trong shop-app (
inventory.order-placed,notification.order-placed). - Classic: queue tạm, exclusive (ví dụ reply queue cho RPC), dữ liệu có thể mất.
- Stream: cần nhiều consumer đọc lại lịch sử, hoặc lưu message lâu. Nếu nhu cầu này là chính, cân nhắc Kafka (bài 8.6).
- Quorum queue không phải để chứa hàng triệu message lâu ngày: queue nên được consume đều, backlog lớn làm tốn RAM/đĩa và chậm khôi phục.
Cluster 3 node thì một queue có được chia ra 3 node để xử lý nhanh gấp 3 không?
Không, đây là điểm hay bị nhầm với Kafka partition. Một quorum queue là một Raft group: mọi publish và deliver đều đi qua leader. Follower chỉ giữ bản sao để chịu lỗi, không chia tải.
queue inventory.order-placed
leader : rabbit1 ← mọi read/write
follower : rabbit2, rabbit3 (bản sao)
Client kết nối rabbit2 → rabbit2 chuyển tiếp nội bộ tới leader trên rabbit1
Cluster giúp tăng throughput tổng bằng cách đặt leader của nhiều queue khác nhau lên các node khác nhau:
rabbit1: leader inventory.order-placed
rabbit2: leader notification.order-placed
rabbit3: leader audit.all
Một queue quá nóng thì phải tự chia thành nhiều queue (ví dụ consistent hash exchange theo orderId) hoặc dùng super stream. Đây là chỗ Kafka mạnh hơn vì partition là khái niệm có sẵn.
Vậy cluster bị chia đôi mạng, 2 node một bên, 1 node một bên thì sao? Có bị hai leader không?
Không, đó chính là lý do dùng Raft. Chỉ phía có đa số mới bầu được leader và nhận ghi:
network partition
[rabbit1] ║ [rabbit2 rabbit3]
1/3 node ║ 2/3 node = majority
không ghi được bầu leader, tiếp tục chạy
(không confirm)
Phía thiểu số không confirm message nào, nên không có chuyện hai bên cùng nhận ghi rồi xung đột (split-brain). Khi mạng nối lại, rabbit1 quay về làm follower và đồng bộ log. Đổi lại: nếu mất đa số, queue ngừng nhận ghi (chọn consistency hơn availability). Producer phải chịu được việc publish thất bại, ví dụ nhờ Outbox.
Phỏng vấn hỏi về độ bền message và quorum queue thì trả lời sao?
Q1. Làm sao để RabbitMQ restart không mất message?
🇻🇳 Tiếng Việt:
Cần exchange và queue durable để định nghĩa còn sau restart, message persistent để được ghi đĩa, và publisher confirm để producer biết broker đã lưu. Để chịu được mất cả một node thì dùng quorum queue trên cluster ít nhất 3 node.
🇬🇧 English:
Exchanges and queues must be durable so their definitions survive a restart, messages must be persistent so they are written to disk, and publisher confirms tell the producer the broker has stored them. To survive losing a whole node, use quorum queues on a cluster of at least three nodes.
Q2. Quorum queue là gì, khác mirrored queue thế nào?
🇻🇳 Tiếng Việt:
Quorum queue là queue được replicate bằng Raft: một leader, nhiều follower, message chỉ được confirm khi đa số node đã ghi. Leader chết thì follower bầu leader mới. Classic mirrored queue là cơ chế cũ, có vấn đề khi network partition và đồng bộ lại, đã bị gỡ trong RabbitMQ 4.0. Quorum queue còn có delivery limit để chặn poison message.
🇬🇧 English:
A quorum queue is replicated with Raft: one leader and several followers, and a message is confirmed only after a majority of nodes have written it. If the leader fails, the followers elect a new one. Classic mirrored queues were the old approach; they had problems with network partitions and resynchronisation and were removed in RabbitMQ 4.0. Quorum queues also have a delivery limit that stops poison messages from looping.
Q3. Vì sao cluster 3 node? Quorum queue có tăng throughput không?
🇻🇳 Tiếng Việt:
3 node có quorum là 2 nên chịu được mất 1 node; 2 node thì không chịu được node nào. Quorum queue không chia tải trong một queue, mọi thao tác đi qua leader. Throughput tổng tăng nhờ phân bố leader của nhiều queue lên nhiều node.
🇬🇧 English:
With three nodes the quorum is two, so we can lose one node; with two nodes we cannot lose any. A quorum queue does not spread load within one queue, because every operation goes through the leader. Overall throughput grows by spreading the leaders of different queues across nodes.
Cách nhớ ngắn gọn
- Durable (queue) + persistent (message) + confirm (producer).
- Mirrored đã bị gỡ ở 4.0 → dùng quorum queue (Raft, majority).
- 3 node chịu mất 1; mất đa số thì ngừng ghi, không split-brain.
- Một queue = một leader; muốn scale thì nhiều queue. Cần replay → stream / Kafka.
Tự kiểm tra
Queue durable nhưng message không persistent, restart broker thì sao?
Queue còn, message mất (chỉ nằm trong RAM).
Cluster 2 node chạy quorum queue, một node chết thì sao?
Còn 1/2 không đủ majority, queue ngừng nhận ghi. Vì vậy cần ít nhất 3 node.
Đổi queue classic có sẵn thành quorum bằng cách declare lại được không?
Không, báo PRECONDITION_FAILED. Phải tạo queue mới, chuyển binding, xả queue cũ.
Khi nào publisher confirm về với quorum queue?
Khi đa số replica đã ghi message vào log.
Stream khác quorum queue ở điểm chính nào?
Stream giữ message sau khi đọc, consumer đọc lại bằng offset (giống Kafka); quorum queue xóa message sau ACK.