Harbor

So sánh Message Queue: Kafka, RabbitMQ, SQS, Pub/Sub và các lựa chọn khác

Hướng dẫn chọn message queue: so sánh Kafka, RabbitMQ, SQS và SNS, Google Pub/Sub, Redis, NATS, Pulsar, BullMQ theo mô hình, thứ tự, replay, thông lượng, chi phí vận hành. Có sơ đồ quyết định, gợi ý theo quy mô 10k, 1M, 50M+ người dùng và các lỗi hay gặp.

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

Hai câu hỏi trước khi chọn công cụ

Trong phỏng vấn, đừng bắt đầu bằng tên sản phẩm. Hãy trả lời hai câu hỏi về bản chất công việc.

Câu hỏi 1: message là một lệnh hay một sự kiện? Microservices Patterns §3.3.1 phân biệt hai kiểu kênh. Kênh point-to-point giao message cho đúng một consumer, hợp với command. Kênh publish-subscribe giao mỗi message cho mọi consumer đã gắn vào, hợp với event.1 Một lệnh như "gửi email này" chỉ nên chạy một lần. Một sự kiện như "đơn 123 đã thanh toán" có thể có nhiều bên cần biết (kho, hoá đơn, analytics).

Câu hỏi 2: sau khi đọc xong, message có cần còn lại không? Grokking ch.17 nêu hai cách xoá message. Cách một là không xoá khi consume, consumer tự theo dõi vị trí đã đọc, một job xoá khi hết hạn, và Kafka chủ yếu dùng ý tưởng này. Cách hai là ẩn message trong một khoảng thời gian, consumer xoá bằng API sau khi xử lý xong.2 Cách một cho phép đọc lại (replay) và nhiều nhóm đọc độc lập. Cách hai hợp với hàng đợi việc, mỗi việc xong là biến mất.

Hai câu trả lời này đã loại được một nửa danh sách. Phần còn lại là thông lượng, thứ tự và chi phí vận hành.

Trong phỏng vấn: Nói được "đây là work queue hay event log" ngay từ đầu cho thấy bạn chọn công cụ theo bài toán, không theo thương hiệu.

Các lựa chọn trong một phút

Microservices Patterns §3.3.4 nêu các tiêu chí chọn broker: ngôn ngữ hỗ trợ, chuẩn giao thức, thứ tự message, bảo đảm giao nhận, persistence, durability, scalability, latency và competing consumers.3 Danh sách dưới dùng các tiêu chí đó. Sách nêu ActiveMQ, RabbitMQ, Kafka là broker mã nguồn mở, và AWS Kinesis, SQS là dịch vụ cloud.3

  • Kafka: log phân vùng, consumer kéo theo offset, giữ message theo retention, replay được. Chi tiết ở Kafka.
  • RabbitMQ: broker với exchange và queue, định tuyến linh hoạt, ack từng message, retry và dead-letter. Chi tiết ở RabbitMQ.
  • Amazon SQS và SNS: SQS là hàng đợi managed, message ẩn trong visibility timeout rồi consumer xoá. SNS là pub-sub managed, thường đặt phía trước nhiều hàng đợi SQS để fan-out.4
  • Google Pub/Sub: topic và subscription managed, mỗi subscription có hàng đợi riêng, có thể push hoặc pull.4
  • Redis: list làm hàng đợi, Streams làm log nhẹ có consumer group, pub-sub phát tức thời không lưu.
  • NATS: hệ thống nhắn tin rất nhẹ và nhanh, dạng subject; bản JetStream thêm lưu trữ và replay.4
  • Apache Pulsar: giống Kafka về log, tách tầng lưu trữ khỏi tầng broker, hỗ trợ nhiều tenant.4
  • BullMQ: thư viện job queue cho Node.js chạy trên Redis, có retry, delay, ưu tiên, lịch lặp.4

Với Redis, Redis in Action ch.3 ghi rõ pub-sub của Redis không lưu: nếu client bị ngắt kết nối khi message được gửi thì nó không bao giờ thấy message đó. Sách vì vậy dựng hai cách nhắn tin tin cậy ở ch.6.5 Ch.6 mô tả task queue FIFO bằng list và task có trễ bằng sorted set, và gọi cách này là pull messaging, nơi người nhận tự lấy message từ "hộp thư".6 Sách còn nêu ActiveMQ, RabbitMQ, SQS là các phần mềm task queue chuyên dụng, còn dựng queue trên Redis là cách làm ad hoc.6

Bảng so sánh

Bảng dưới là nhận định của khoá học, mọi cột thông lượng là hạng ước lượng cần load test. Thư viện chỉ xác nhận các ý nền (log hay queue, pub-sub không lưu của Redis, point-to-point hay pub-sub).43

Mô hìnhThứ tựReplayThông lượng (hạng ước lượng)Chi phí vận hànhHợp nhất cho
KafkaLog phân vùng, kéo theo offsetTheo partitionCó, trong retentionRất caoCao nếu tự hostEvent streaming, CDC, nhiều consumer đọc cùng dữ liệu
RabbitMQExchange, queue, đẩy tới consumerTheo queue, mất khi nhiều consumerKhông (queue thường)Trung bình đến caoVừaTask queue, định tuyến phức tạp, retry và delay
SQSHàng đợi managed, xoá sau khi xử lýStandard: không; FIFO: theo groupKhôngCao, tự scaleGần như khôngHàng đợi job đơn giản, ít vận hành
SNS + SQSPub-sub fan-out vào nhiều hàng đợiTheo hàng đợiKhôngCao, tự scaleGần như khôngMột sự kiện, nhiều dịch vụ xử lý riêng
Google Pub/SubTopic và subscription managedCó ordering keyCó giới hạnRất cao, tự scaleGần như khôngFan-out và ingestion trên GCP
Redis listHàng đợi trong RAMTheo listKhôngCao (một node)Thấp nếu đã có RedisHàng đợi nhỏ, nhanh
Redis StreamsLog nhẹ, consumer groupTheo streamCó, bị giới hạn bởi RAMCao (một node)Thấp đến vừaLuồng vừa phải, đã dùng Redis
Redis pub-subPhát tức thời, không lưuTheo kênhKhôngCaoThấpRealtime, chấp nhận mất message
NATS (core)Subject, nhẹ, không lưuTheo subjectKhông (JetStream: có)Rất caoThấp đến vừaGiao tiếp giữa service độ trễ thấp
PulsarLog, tách lưu trữ khỏi brokerTheo partitionCóRất caoCaoQuy mô rất lớn, nhiều tenant
BullMQJob queue trên RedisƯu tiên, FIFOKhông (có job đã xong để xem)Thấp đến vừaThấpJob nền trong ứng dụng Node.js

Cách đọc bảng. Nếu bạn cần replay hoặc nhiều nhóm đọc độc lập thì chọn các dòng có "Có": Kafka, Pulsar, Pub/Sub, Streams. Nếu bạn chỉ cần "làm việc này đúng một lần rồi xong" thì chọn SQS, RabbitMQ, BullMQ. Grokking ch.18 chỉ ra vì sao hàng đợi thuần không hợp cho fan-out lớn: với hàng triệu subscriber trên hàng nghìn topic, phải duy trì hàng triệu queue và sao chép cùng một message vào từng queue, vừa đắt vừa tốn chỗ.7 Thiết kế thứ hai của sách dùng broker lưu message một lần và để consumer đọc theo offset, đúng mô hình log.8

Sơ đồ quyết định

Đang vẽ sơ đồ…

Sơ đồ là heuristic của khoá học, không phải quy tắc cứng.4 Hai ngoại lệ thường gặp. Thứ nhất, nếu hạ tầng đã nằm sẵn trên một cloud thì dịch vụ managed của cloud đó thường thắng về công vận hành. Thứ hai, nếu cần độ trễ dưới vài mili giây giữa các service và chấp nhận mất message khi lỗi, NATS core hoặc Redis pub-sub phù hợp hơn một broker bền.

Chọn theo quy mô

Các mốc dưới đây là gợi ý của khoá học. Quy tắc tải đỉnh: trung bình × 3 tới 10, sự kiện flash ×50 tới ×100.9 Thư viện không có số liệu sản phẩm, nên chỉ lấy quyết định làm nội dung chính.4

Khoảng 10.000 DAU

Giả định mỗi người dùng tạo 20 sự kiện cần xử lý nền mỗi ngày (giả định của ví dụ): 10.000 × 20 = 200.000 sự kiện/ngày, chia 86.400 giây được ~2,3 msg/s, đỉnh ×10 khoảng 23 msg/s. Con số này nhỏ tới mức bất kỳ công cụ nào cũng dư sức. Tiêu chí quyết định là công vận hành.

  • Đã có Redis và dùng Node.js: BullMQ. Thêm một thư viện, không thêm hạ tầng. Lưu ý Redis phải bật persistence nếu job quan trọng, vì Redis in Action nhắc dữ liệu có thể mất theo cách cấu hình snapshot hoặc append-only file.10
  • Chạy trên cloud: SQS hoặc Pub/Sub. Không máy chủ, trả theo lượng dùng.
  • Đừng dựng Kafka cho quy mô này. Microservices Patterns cũng nhắc broker là thêm một thành phần phải cài, cấu hình và vận hành.3

Khoảng 1 triệu DAU

Với 1.000.000 × 20 = 20 triệu sự kiện/ngày ≈ 230 msg/s trung bình, đỉnh ×10 khoảng 2.300 msg/s (giả định của ví dụ). Vẫn nằm trong tầm một node RabbitMQ hoặc một dịch vụ managed.

  • Hàng đợi việc (email, ảnh, webhook): SQS hoặc RabbitMQ cụm 3 node. RabbitMQ thắng khi cần routing, delay và retry theo từng message, xem RabbitMQ.
  • Event dùng chung giữa nhiều service (đơn hàng, thanh toán): bắt đầu cân nhắc Kafka managed, hoặc SNS + SQS nếu chỉ cần fan-out đơn giản không replay.
  • Thêm dead-letter queue và cảnh báo độ dài queue ngay từ giai đoạn này.11

50 triệu DAU trở lên

50.000.000 × 20 = 1 tỷ sự kiện/ngày ≈ 11.600 msg/s trung bình, đỉnh ×10 khoảng 116.000 msg/s (giả định của ví dụ). Đây là vùng của log phân vùng.

  • Kafka hoặc Pulsar cho luồng sự kiện chính. Microservices Patterns §3.3.5 nêu giải pháp mở rộng consumer mà vẫn giữ thứ tự: kênh chia shard theo shard key, mỗi shard giao cho một consumer của nhóm.12 Đây là xương sống của scale ghi, xem Scale ghi.
  • Dùng schema (Avro hoặc Protobuf) và nhiều cụm theo mục đích. Phần này là kinh nghiệm vận hành, chưa có sách.4
  • Giữ RabbitMQ hoặc SQS cho các hàng đợi tác vụ riêng, vì Kafka không hợp với retry và delay từng message. Xem thêm Tác vụ dài.

Những bài toán dùng queue

Cả ba bài dưới đây dùng queue để tách việc chậm khỏi request và chịu đỉnh tải; lệnh đi qua kênh point-to-point, sự kiện đi qua kênh publish-subscribe.1 Cột lựa chọn là gợi ý của khoá học.4

BàiVai trò của queueLựa chọn hợp lý
Thông báoTách việc gửi push, email, SMS khỏi request; mỗi kênh một queue, retry riêngHàng đợi việc (SQS hoặc RabbitMQ), Kafka nếu sự kiện đến từ nhiều nguồn
Flash saleHấp thụ đỉnh tải, đặt hàng vào queue rồi xử lý theo tốc độ của khoKafka hoặc SQS làm bộ đệm, kèm kiểm tra tồn kho idempotent
Đặt véHàng đợi chờ (waiting room), xử lý thanh toán và hết hạn giữ chỗQueue có delay, RabbitMQ hoặc SQS

Các bài khác cùng họ: quy trình nhiều bước bằng saga dùng queue làm kênh điều phối, xem Quy trình nhiều bước; việc chạy lâu cần retry và tiến độ, xem Tác vụ dài.

Sai lầm hay gặp

Sai lầm hay gặp: Nói "exactly-once" như một thuộc tính của broker. Microservices Patterns §3.3.6 giải thích exactly-once thường quá tốn kém nên broker chỉ hứa at-least-once, và consumer phải tự chống trùng, ví dụ bằng bảng message đã xử lý trong cùng transaction với thay đổi nghiệp vụ.13

Danh sách lỗi thường gặp:

  1. Dual write. Ghi database rồi publish message bằng hai bước riêng. Một trong hai bước lỗi là mất đồng bộ. Cách sửa là transactional outbox hoặc CDC.14 Xem Change Data Capture.
  2. Retry vô hạn, không có dead-letter. Một message độc làm worker chết rồi quay lại queue và làm worker khác chết. Building Microservices kể đúng sự cố này và kết luận phải giới hạn số lần retry, có dead letter queue và công cụ xem lại.11
  3. Bỏ qua thứ tự. Nhiều consumer đọc cùng một queue có thể xử lý Order Cancelled trước Order Created. Cần shard key hoặc thiết kế consumer chịu được sự đảo thứ tự.12
  4. Dùng pub-sub không lưu cho việc quan trọng. Redis pub-sub mất message khi client rớt kết nối, như Redis in Action đã cảnh báo.5 Việc quan trọng cần queue bền.
  5. Chọn Kafka cho work queue nhỏ. Chi phí vận hành lớn hơn lợi ích, và retry từng message khó. Xem Kafka.
  6. Nhồi payload lớn vào message. Gửi tham chiếu (ID, URL blob) thay vì cả file. Đây là khuyến nghị của khoá học.4
  7. Không theo dõi độ trễ của consumer. Queue dài dần là dấu hiệu sớm nhất của sự cố. Building Microservices nhắc cần monitoring tốt trước khi dùng kiến trúc bất đồng bộ.11
  8. Bỏ qua kiểu giao nhận. Node.js Design Patterns nêu ba mức: at most once (có thể mất), at least once (có thể trùng), exactly once (chậm và tốn dữ liệu hơn).15 Biết mình cần mức nào trước khi chọn công cụ.

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

  • Mở bằng hai câu hỏi: lệnh hay sự kiện, cần giữ lại để đọc lại hay không.12
  • Chọn theo quy mô và đội ngũ: ở quy mô nhỏ ưu tiên ít vận hành (BullMQ, SQS, managed), quy mô lớn mới tới Kafka hoặc Pulsar.
  • Nêu rõ ngữ nghĩa giao nhận và cách chống trùng.13
  • Nhắc dead-letter queue, giới hạn retry, và giám sát độ dài queue.11
  • Nêu ví dụ cụ thể: Thông báo dùng queue theo kênh, Flash sale dùng queue làm bộ đệm đỉnh tải, Đặt vé dùng queue có delay để hết hạn giữ chỗ.
  • Liên hệ các bài chi tiết: Kafka, RabbitMQ, Tác vụ dài, Quy trình nhiều bước.

Mẹo: Khi bị hỏi "sao không dùng Kafka cho mọi thứ?", trả lời bằng chi phí: Kafka mạnh ở replay và thông lượng, nhưng thêm việc vận hành và không có retry, delay từng message sẵn có. Chọn công cụ nhỏ nhất đủ giải bài toán.

Nguồn trích dẫn

  1. 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). ↩ ↩2 ↩3

  2. 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). ↩ ↩2

  3. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.4 Using a message broker (các broker phổ biến, tiêu chí chọn broker, lợi ích và nhược điểm). ↩ ↩2 ↩3 ↩4

  4. Chưa có nguồn trong thư viện: thư viện không có sách về SQS, SNS, Google Pub/Sub, NATS, Pulsar, BullMQ hay Kinesis; phần mô tả sản phẩm, bảng so sánh, thông lượng, sơ đồ quyết định và gợi ý theo quy mô là nhận định của khoá học, không có số liệu giới hạn hay giá, mọi con số là giả định cần load test. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11

  5. Redis in Action · ch.3 · §3.6 Publish/subscribe (hai lý do không dùng PUBLISH/SUBSCRIBE khi cần giao tin cậy: bộ đệm đầu ra và mất message khi ngắt kết nối). ↩ ↩2

  6. Redis in Action · ch.6 · §6.4 Task queues (FIFO queue, delayed tasks) và §6.5 Pull messaging. ↩ ↩2

  7. 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). ↩

  8. Grokking Modern System Design · ch.18 Pub-sub · bài "Design of a Pub-sub System" · mục "Second design" (broker lưu message, cluster manager, consumer đọc theo offset). ↩

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

  10. Redis in Action · ch.4 · §4.1 Persistence options (snapshot và append-only file, nguy cơ mất dữ liệu theo cấu hình). ↩

  11. 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, dead letter queue, monitoring, tr. 57). ↩ ↩2 ↩3 ↩4

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

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

  14. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.7 Transactional messaging (Transactional outbox, Polling publisher, Transaction log tailing). ↩

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