Harbor

RabbitMQ

RabbitMQ là message broker theo mô hình AMQP: producer gửi vào exchange, exchange định tuyến sang queue, consumer nhận và ack. Bài này đi qua các loại exchange, ack/nack/requeue, prefetch, queue bền, dead-letter, TTL, competing consumers, quorum queue và so sánh với Kafka.

15 phút đọcCông nghệ chủ chốt

RabbitMQ là gì và mô hình broker

RabbitMQ là một message broker: một dịch vụ trung gian nhận message từ bên gửi và chuyển cho bên nhận. Microservices Patterns §3.3.4 nêu hai lợi ích chính của broker: bên gửi không cần biết địa chỉ mạng của bên nhận, và broker giữ đệm message tới khi consumer xử lý được.1 Node.js Design Patterns ch.11 bổ sung rằng broker tách người nhận khỏi người gửi, và còn cung cấp queue bền, định tuyến, biến đổi message, giám sát.2 RabbitMQ được sách nêu là broker hỗ trợ AMQP, MQTT và STOMP.2

Mô hình AMQP có ba thành phần cốt lõi, theo Node.js Design Patterns ch.11:3

  • Queue: cấu trúc lưu message chờ consumer. Nếu nhiều consumer cùng gắn vào một queue, message được chia đều giữa họ.
  • Exchange: nơi producer publish message. Exchange định tuyến message sang một hoặc nhiều queue theo thuật toán của nó.
  • Binding: liên kết giữa exchange và queue, kèm routing key hoặc pattern dùng để lọc.

Producer không gửi thẳng vào queue (trừ trường hợp point-to-point, xem phần competing consumers). Luồng đầy đủ là producer, exchange, queue, consumer.

Đang vẽ sơ đồ…

Cũng cần phân biệt hai kiểu kênh. Microservices Patterns §3.3.1 định nghĩa kênh point-to-point giao message cho đúng một consumer, còn kênh publish-subscribe giao mỗi message cho mọi consumer đang gắn vào.4 Trong RabbitMQ, hai kiểu này đều dựng được bằng exchange và queue: một queue có nhiều consumer là point-to-point, một exchange nối tới nhiều queue là publish-subscribe.3

Các loại exchange và routing key

Node.js Design Patterns liệt kê ba loại exchange phổ biến.3 Loại thứ tư là headers.

LoạiCách định tuyếnVí dụ dùng
DirectSo khớp nguyên routing key với binding keypayment.failed chỉ vào queue xử lý lỗi thanh toán
TopicSo khớp pattern kiểu glob trên routing key (chat.#)Log theo service.level, nhận mọi order.*
FanoutGửi tới mọi queue đã bind, bỏ qua routing keyBroadcast: cập nhật cache, thông báo realtime
HeadersKhớp theo header của message thay vì routing keyLọc theo nhiều thuộc tính không gói nổi vào một chuỗi

Sách mô tả direct, topic, fanout như trong bảng.3 Exchange headers và chi tiết ký tự đại diện (* một từ, # nhiều từ) là hành vi riêng của RabbitMQ chưa có trong sách thư viện.5

Routing key là một chuỗi do producer gắn khi publish, quy ước thường là các từ nối bằng dấu chấm: order.created.vn. Binding quyết định queue nào nhận message nào. Hệ quả thiết kế: producer chỉ cần biết tên exchange và routing key, không biết có bao nhiêu consumer. Thêm một hệ thống mới (ví dụ analytics) chỉ là tạo queue mới và bind vào exchange, không sửa producer.

Mẹo: Mặc định mỗi nhóm consumer có queue riêng, bind vào exchange. Muốn thêm consumer vào nhóm đó thì gắn vào cùng queue. Muốn thêm nhóm mới thì tạo queue mới.

Ack, nack, requeue và prefetch

Ack

Consumer nhận message rồi xử lý, sau đó gửi ack cho broker. Nếu broker không nhận được ack, message vẫn nằm trong queue để xử lý lại. Node.js Design Patterns minh hoạ đúng điều này: ví dụ lịch sử chat chỉ gọi channel.ack(msg) sau khi đã lưu message vào database, và nếu không có ack thì broker giữ message để xử lý lại. Sách cũng nói có thể bỏ ack bằng tuỳ chọn {noAck: true}.6

Hệ quả là ngữ nghĩa giao nhận at-least-once. Node.js Design Patterns mô tả mức QoS này: message chắc chắn tới nơi ít nhất một lần, nhưng có thể trùng nếu receiver chết trước khi báo lại cho bên gửi.7 Microservices Patterns §3.3.6 khuyên xử lý trùng bằng handler idempotent hoặc theo dõi message đã xử lý trong bảng riêng.8

Nack và requeue

Nack (hoặc reject) báo broker rằng consumer không xử lý được. Có hai lựa chọn: đưa message về queue (requeue) hoặc bỏ nó đi, hay chuyển sang dead-letter exchange nếu có cấu hình. Cẩn thận với requeue vô hạn: một message gây lỗi sẽ quay lại đầu queue và bị nhận lại liên tục. Building Microservices kể đúng sự cố này, một request độc làm worker chết, lock hết hạn, message quay lại queue rồi làm worker khác chết. Bài học của sách là phải đặt giới hạn số lần retry và có dead letter queue kèm công cụ xem và gửi lại.9

import amqp from "amqplib";

const conn = await amqp.connect("amqp://localhost");
const ch = await conn.createChannel();
await ch.assertQueue("jobs", { durable: true });
await ch.prefetch(10); // tối đa 10 message chưa ack mỗi consumer

await ch.consume("jobs", async (msg) => {
  if (!msg) return;
  try {
    await handleJob(JSON.parse(msg.content.toString())); // idempotent
    ch.ack(msg);
  } catch (err) {
    const retryable = isTransient(err);
    ch.nack(msg, false, retryable); // requeue chỉ khi lỗi tạm thời
  }
});

Prefetch

Không giới hạn, broker có thể đẩy rất nhiều message cho một consumer trước khi nó ack cái nào. Consumer chậm giữ một đống message trong khi consumer khác ngồi rảnh, và crash thì cả đống bị giao lại. Prefetch (QoS) giới hạn số message chưa ack mỗi consumer. Cơ chế này thuộc riêng RabbitMQ, chưa có trong sách; sách chỉ mô tả việc message được chia đều giữa các consumer của một queue.35 Quy tắc kinh nghiệm của khoá học (giả định, cần thử): job nặng và thời gian không đều thì prefetch nhỏ (1 tới 5), job nhẹ và nhanh thì prefetch lớn hơn (vài chục) để đỡ chờ qua lại mạng.

Sai lầm hay gặp: Dùng noAck: true cho công việc quan trọng. Broker coi message là đã giao ngay lúc đẩy đi, consumer chết là mất message. Đây là mức at-most-once, "set and forget" trong cách gọi của sách.7

Độ bền: queue durable, message persistent, dead-letter, TTL

Durable queue và persistent message

Hai khái niệm này hay bị nhầm. Node.js Design Patterns ghi rõ: queue durable được tạo lại tự động khi broker khởi động lại, nhưng điều đó không có nghĩa nội dung được giữ; chỉ message đánh dấu persistent mới được ghi xuống đĩa và khôi phục sau restart.3 Muốn không mất message khi broker sập cần cả hai: queue durable và message persistent.

Sách cũng liệt kê hai kiểu queue khác: exclusive (gắn với một kết nối, đóng kết nối là xoá) và auto-delete (xoá khi consumer cuối ngắt). Durable subscriber đạt được bằng bất kỳ queue nào không exclusive và không auto-delete.3 Durable subscriber là consumer nhận được mọi message, kể cả khi nó đang offline lúc message được gửi, vì queue giữ message hộ.10

Dead-letter exchange

Grokking ch.17 mô tả dead-letter queue là nơi chứa message không được consume sau số lần thử tối đa, cùng các trường hợp queue không còn tồn tại, vượt giới hạn độ dài queue, hoặc message hết hạn theo TTL. Nó giúp tìm nguyên nhân lỗi.11 Trong RabbitMQ, cấu hình này là dead-letter exchange (DLX): queue chính khai báo DLX, message bị reject không requeue hoặc hết hạn sẽ được publish sang đó, rồi tới một queue riêng để người vận hành xem. Tên gọi và cách cấu hình DLX là của RabbitMQ.5

TTL và retry có độ trễ

TTL (time to live) là thời hạn sống của message, đặt theo message hoặc theo queue. Hết hạn mà chưa ai lấy thì message bị bỏ hoặc chuyển sang DLX, như Grokking ch.17 nêu (message hết hạn do per-message TTL).11 Một mẹo phổ biến: dùng TTL trên một queue chờ rồi cho DLX trỏ về queue chính, tạo thành retry có trễ (thử lại sau 30 giây, 5 phút). Đây là kỹ thuật cộng đồng, chưa có sách.5

Competing consumers, pipeline và work queue

Node.js Design Patterns ch.11 giải thích cách làm phân phối công việc với broker. Nếu exchange có thể bind tới nhiều queue thì không đảm bảo mỗi message chỉ tới một consumer. Giải pháp là gửi thẳng vào một queue đích (point-to-point), và đặt nhiều consumer trên queue đó. Message được chia đều, mô hình này gọi là competing consumers.12 Trong ví dụ hashsum cracker của sách, hàng đợi jobs_queue có nhiều worker cạnh tranh, kết quả đẩy về một hàng đợi riêng cho bộ thu thập, tạo thành pipeline phân tán.12

Đang vẽ sơ đồ…

Cần nhớ giới hạn về thứ tự. Microservices Patterns §3.3.5 chỉ ra: nhiều instance cùng đọc một kênh có thể xử lý các message theo thứ tự lộn xộn (ví dụ xử lý Order Cancelled trước Order Created). Cách thường dùng là kênh chia shard theo shard key, kiểu consumer group của Kafka.13 Với RabbitMQ chuẩn, một queue một consumer giữ được thứ tự, nhưng nhiều consumer thì thứ tự xử lý không được bảo đảm, và requeue làm message nhảy vị trí. Phần này là suy luận từ hai nguồn trên, kèm hành vi riêng của RabbitMQ.5 Nếu cần thứ tự theo khoá, chọn một trong hai: một queue cho mỗi nhóm khoá (consistent-hash exchange, tính năng của RabbitMQ), hoặc chuyển sang Kafka.

Quorum queue và độ sẵn sàng

Một queue trên một node là điểm lỗi duy nhất. Grokking ch.17 mô tả hai hướng nhân bản queue trong cụm: mô hình primary-secondary và mô hình quorum, mỗi hướng có cân bằng giữa độ nhất quán, độ trễ và khả năng chịu lỗi.14 Quorum queue của RabbitMQ đi theo hướng thứ hai: queue được nhân bản trên nhiều node, ghi cần đa số node xác nhận (Raft), mất một node nhỏ vẫn hoạt động. Chi tiết sản phẩm là kiến thức chưa có sách trong thư viện.5

Lý thuyết nền tảng về đa số:15

  • Cụm N bản nhân bản chịu được f = floor((N - 1) / 2) node lỗi mà vẫn còn đa số.
  • Ví dụ N = 3: đa số là 2, chịu 1 node lỗi. N = 5: đa số là 3, chịu 2 node lỗi.

Với dữ liệu quan trọng (đơn hàng, thanh toán), chạy 3 node với quorum queue. Đổi lại ghi chậm hơn một chút vì phải chờ đa số.

So sánh với Kafka

Hai hệ thống giải hai bài toán khác nhau. Grokking ch.17 mô tả hai cách xoá message: cách một là không xoá khi consume, consumer tự theo dõi vị trí, một job xoá khi hết hạn, và nói Kafka chủ yếu dùng ý tưởng này; cách hai là ẩn message trong một khoảng thời gian rồi consumer xoá bằng API.16 RabbitMQ thuộc mô hình queue truyền thống: ack xong thì message biến mất.

Tiêu chíRabbitMQKafka
Mô hìnhBroker thông minh, queue, đẩy message tới consumerLog phân vùng, consumer kéo theo offset
Sau khi đọcAck xong thì xoáGiữ tới hết retention
ReplayKhông (queue thường)Có, đặt lại offset
Thứ tựTheo queue, mất khi nhiều consumer hoặc requeueTheo partition, ổn định theo key
Định tuyếnRất linh hoạt: exchange, binding, routing keyChỉ topic và key
Retry, delay, priority từng messageCó sẵn hoặc làm được qua TTL và DLXPhải tự dựng (retry topic)
Thông lượngVài nghìn tới vài chục nghìn msg/s mỗi node (ước lượng)Hàng trăm nghìn msg/s trở lên mỗi cụm (ước lượng)
Nhiều hệ thống đọc cùng dữ liệuMỗi nhóm một queue, message bị sao chépMỗi nhóm một offset, không sao chép

Bảng trên là nhận định của khoá học; con số thông lượng là ước lượng cần load test.5 Về cách đọc: Grokking ch.18 mô tả nhược điểm của việc dùng một queue cho từng subscriber, tức phải nhân bản message vào mọi queue và duy trì hàng triệu queue khi có hàng triệu subscriber.17 Đó chính là chi phí của mô hình "mỗi nhóm một queue" khi số nhóm lớn, và lý do Kafka mạnh hơn ở fan-out lớn. Chi tiết xem Kafka.

Về push và pull, Grokking ch.18 nêu pub-sub phân phối kiểu push giúp người nhận khỏi phải hỏi liên tục, giảm độ trễ giao.18 Node.js Design Patterns ghi message trong queue AMQP có thể được đẩy hoặc kéo tới consumer.3 Trên thực tế RabbitMQ đẩy message cho consumer có prefetch còn chỗ, nên độ trễ thấp, nhưng consumer chậm không tự điều chỉnh tốc độ nếu bạn quên prefetch.

Sizing và vận hành

Mọi con số trong phần này là giả định của khoá học, cần load test; thư viện không có số liệu hiệu năng của RabbitMQ.5

Chỉ sốƯớc lượng
Một node 4 vCPU · 8 GBVài nghìn tới ~20.000 msg/s với message nhỏ, persistent, có ack
Độ trễ giaoVài ms tới vài chục ms khi queue gần như rỗng
Số queue mỗi nodeHàng chục nghìn là thoải mái; hàng trăm nghìn cần cân nhắc
Kích thước messageGiữ nhỏ (dưới vài trăm KB); file lớn để ở blob store, gửi tham chiếu
Cụm quorum3 node là cấu hình tối thiểu cho dữ liệu quan trọng

Cách ước lượng: tải đỉnh = trung bình × 3 tới 10 (quy tắc của khoá học).19 Ví dụ gửi email 2 triệu/ngày: 2.000.000 / 86.400 ≈ 23 msg/s trung bình, đỉnh khoảng 230 msg/s. Một node dư sức; điểm nghẽn là tốc độ nhà cung cấp email chứ không phải broker.

Điều cần theo dõi ở môi trường thật:

  • Độ dài queue và tốc độ tăng. Queue dài dần nghĩa là consumer không theo kịp, tương đương consumer lag của Kafka.
  • Số message unacked. Số lớn bất thường nghĩa là consumer treo hoặc prefetch quá cao.
  • Bộ nhớ và đĩa. Queue phình to làm broker chạm ngưỡng cảnh báo bộ nhớ và chặn producer (backpressure). Đặt giới hạn độ dài queue và TTL để chặn từ đầu.
  • Dead-letter queue. Có cảnh báo khi DLQ có message, vì mỗi message ở đó là một lỗi cần người xem. Building Microservices cũng nhắn: cần monitoring tốt trước khi dùng kiến trúc bất đồng bộ.9

Nhỏ: dùng dịch vụ managed (nhà cung cấp cloud hoặc CloudAMQP, giá tuỳ nhà cung cấp) hoặc một container đơn kèm volume bền. Vừa và lớn: cụm 3 node, quorum queue, tách queue nặng ra cụm riêng. Microservices Patterns nhắc broker là một thành phần phải cài, cấu hình và vận hành, nên chọn managed khi nhóm nhỏ.1

Khi nào chọn RabbitMQ

Nên chọn khi:

  • Cần task queue với retry, delay, priority theo từng message, cùng DLQ rõ ràng.
  • Cần định tuyến linh hoạt (topic, header) giữa nhiều dịch vụ.
  • Muốn kết nối nhiều giao thức (AMQP, MQTT, STOMP), như Node.js Design Patterns nêu.2
  • Lưu lượng vừa phải, cần độ trễ thấp và không cần replay.

Không nên chọn khi:

  • Cần replay hoặc nhiều hệ thống đọc lại cùng luồng sự kiện dài hạn: dùng Kafka.
  • Thông lượng rất lớn (hàng trăm nghìn msg/s liên tục) hoặc cần giữ sự kiện nhiều ngày.
  • Chỉ cần job nền đơn giản cho Node.js: BullMQ trên Redis hoặc SQS ít việc vận hành hơn. Xem lựa chọn đầy đủ ở So sánh Message Queue.

Điểm nói trong phỏng vấn

  • Vẽ đúng luồng producer, exchange, binding, queue, consumer. Nói rõ producer không gửi thẳng vào queue.3
  • Độ bền cần đủ hai lớp: queue durable và message persistent.3
  • At-least-once nghĩa là phải idempotent. Nêu ack chỉ sau khi đã lưu kết quả.68
  • Có giới hạn retry và dead-letter queue.9
  • Các bài dùng queue kiểu này: Thông báo, Flash sale, Đặt vé. Liên hệ tác vụ dài và quy trình nhiều bước khi queue điều phối các bước của một saga.

Trong phỏng vấn: Một câu gọn: "Exchange topic định tuyến order.* vào queue billing durable với DLX, 3 worker prefetch 5 cạnh tranh trên queue đó, ack sau khi ghi DB idempotent, quorum queue 3 node cho dữ liệu quan trọng."

Nguồn trích dẫn

  1. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.4 Using a message broker (Overview of broker-based messaging, các tiêu chí chọn broker, lợi ích và nhược điểm). ↩ ↩2

  2. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 Messaging and Integration Patterns · mục "Peer-to-peer or broker-based messaging" (tr. 440–442). ↩ ↩2 ↩3

  3. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 · mục "Introducing AMQP" (queue durable, exclusive, auto-delete; exchange direct, topic, fanout; binding; channel, tr. 457–459). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  4. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.1 Overview of messaging · "About message channels" (kênh point-to-point và publish-subscribe). ↩

  5. Chưa có nguồn trong thư viện: thư viện không có sách riêng về RabbitMQ; exchange headers, prefetch, DLX, TTL, quorum queue, consistent-hash exchange, sizing và cấu hình vận hành chỉ nêu ở mức khái niệm cơ bản, mọi con số là giả định của khoá học cần load test. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  6. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 · mục "Implementing a reliable history service using AMQP" (ack sau khi lưu, tuỳ chọn noAck, tr. 461–462). ↩ ↩2

  7. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 · mục "Durable subscribers" (các mức QoS: at most once, at least once, exactly once, tr. 455). ↩ ↩2

  8. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages (tr. 95–97). ↩ ↩2

  9. Building Microservices (file .zip, tải về giải nén) · ch.4 Integration · mục "Implementing Asynchronous Event-Based Collaboration" (giới hạn retry, message hospital hay dead letter queue, tr. 57). ↩ ↩2 ↩3

  10. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 · mục "Durable subscribers" (tr. 454–456). ↩

  11. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Requirements of a Distributed Messaging Queue" và "Evaluation of a Distributed Messaging Queue's Design" (dead-letter queue, các nguyên nhân gồm queue bị xoá, quá giới hạn độ dài, per-message TTL). ↩ ↩2

  12. Node.js Design Patterns 2e (file .zip, tải về giải nén) · ch.11 · mục "Pipelines and competing consumers in AMQP" (point-to-point, competing consumers, hashsum cracker, tr. 470–472). ↩ ↩2

  13. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.5 Competing receivers and message ordering (tr. 94–95). ↩

  14. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" · mục "Back-end service" (nhân bản queue theo primary-secondary hoặc quorum). ↩

  15. Lý thuyết · Quorum đa số: một nhóm N bản nhân bản cần đa số W = floor(N / 2) + 1 bản đồng ý để một thao tác thành công. Số node lỗi tối đa mà vẫn còn đa số là f = floor((N - 1) / 2). Ví dụ N = 3 thì W = 2, f = 1; N = 5 thì W = 3, f = 2; N = 4 thì W = 3, f = 1, nên thêm node thứ 4 không tăng khả năng chịu lỗi so với 3. ↩

  16. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" · mục "Message deletion" (hai cách xoá message, Kafka dùng ý tưởng không xoá sau khi consume). ↩

  17. Grokking Modern System Design · ch.18 Pub-sub · bài "Design of a Pub-sub System" · mục "First design" (chi phí nhiều queue và sao chép message cho mỗi subscriber). ↩

  18. Grokking Modern System Design · ch.18 Pub-sub · bài "Requirements" · mục "Use cases of pub-sub" (Improved performance: phân phối kiểu push). ↩

  19. Quy tắc ước lượng của khoá học · /hoc/chon-server. ↩