Thứ Sáu, 17h chiều, hệ thống checkout bắt đầu chậm dần. Order service gọi sync HTTP tới payment service, payment gọi tiếp inventory service để trừ kho, inventory gọi notification service để gửi email xác nhận. Chuỗi bốn service nối đuôi nhau, mỗi cái thêm 200-500ms latency. Bình thường tổng cộng khoảng 1.2 giây, chấp nhận được. Nhưng chiều thứ Sáu, notification service bị chậm vì SMTP provider rate limit, mỗi request tốn 3 giây thay vì 200ms. Timeout cascade ngược lên: inventory chờ notification timeout, payment chờ inventory timeout, order chờ payment timeout. User nhấn “Thanh toán” rồi xoay loading 15 giây, retry, tạo ra order trùng. Error rate checkout tăng từ 0.1% lên 12% trong 20 phút.
Nguyên nhân gốc không phải bug code hay thiếu resource, mà là coupling thời gian. Order service không cần biết email đã gửi chưa để xác nhận đơn hàng. Nhưng vì mọi thứ nối sync qua HTTP, một service phụ chậm kéo sập toàn bộ flow chính. Giải pháp tự nhiên nhất: tách những bước không cần response đồng bộ ra khỏi chuỗi HTTP, đẩy vào message queue. Order service ghi message “order created” vào queue, notification service đọc ra và gửi email theo tốc độ riêng. Payment chậm hay notification chậm không ảnh hưởng lẫn nhau.
Nhưng “dùng message queue” không phải một quyết định, đó là đầu vào cho hàng loạt quyết định tiếp theo. Kafka hay RabbitMQ? SQS hay self-host? At-least-once có đủ không hay cần exactly-once? Bài này đặt ba hệ thống messaging phổ biến nhất cạnh nhau, so sánh theo đặc tính kỹ thuật và use case thực tế, không phải để chọn “cái tốt nhất” (không có) mà để chọn cái phù hợp nhất với bài toán đang giải.
Vì sao cần async messaging
Trước khi đi vào so sánh công cụ, cần nắm rõ ba giá trị cốt lõi mà messaging mang lại, vì nếu bài toán không cần ba thứ này, thêm queue chỉ thêm phức tạp mà không giải quyết gì.
Decoupling là giá trị rõ ràng nhất. Producer gửi message vào queue rồi quay về làm việc khác, không cần biết consumer là ai, có bao nhiêu consumer, hay consumer đang bận hay rảnh. Khi thêm một consumer mới (ví dụ analytics service cần biết “order created”), producer không đổi một dòng code. So với HTTP sync nơi caller phải biết endpoint của callee, cấu hình retry, xử lý timeout, messaging giảm coupling đáng kể.
Load leveling giải quyết bài toán traffic spike. Flash sale 10x traffic bình thường, nếu mọi request đổ trực tiếp vào database thì database chết. Đặt queue ở giữa: producer ghi message nhanh vào queue (queue được thiết kế để nhận write cực nhanh), consumer đọc ra và xử lý theo tốc độ mà downstream chịu được. Queue đóng vai trò bộ đệm, hấp thụ spike, san phẳng tải cho backend.
Resilience là hệ quả của hai thứ trên. Khi consumer chết, message vẫn nằm trong queue chờ consumer khởi động lại. Không mất data, không cần producer retry. So với HTTP sync nơi một service chết là request fail ngay lập tức, messaging cho phép hệ thống chịu lỗi tạm thời (transient failure) một cách tự nhiên, miễn là queue đủ capacity để buffer message trong thời gian downtime.
Nhưng messaging không miễn phí. Thêm queue là thêm một component cần vận hành, monitor, và hiểu. Debug chuỗi async khó hơn sync vì request không còn là call stack đơn lẻ mà trải qua nhiều hop với latency không xác định. Eventual consistency thay vì strong consistency, user có thể thấy “đơn hàng đang xử lý” thay vì “đơn hàng đã xác nhận” ngay lập tức. Đây là trade-off có ý thức, không phải nhược điểm cần “fix”.
Hai mô hình messaging cơ bản
Mọi hệ thống messaging đều xây trên hai mô hình nền tảng. Hiểu rõ hai mô hình này trước khi nhảy vào Kafka hay RabbitMQ sẽ giúp đánh giá đúng từng công cụ.
Hiểu nôm na: point-to-point giống như phân công việc trong nhóm, task vào pool, ai rảnh thì nhận, mỗi task chỉ một người làm. Pub/sub giống như group chat, một người gửi tin, tất cả thành viên đều nhận, ai cần xử lý thì tự xử lý.
Point-to-point (queue)
Mô hình đơn giản nhất: một message chỉ được xử lý bởi đúng một consumer. Nhiều consumer có thể cùng đọc từ một queue, nhưng mỗi message chỉ đến tay một consumer duy nhất, queue phân phối message theo round-robin hoặc cơ chế tương đương. Đây là mô hình task queue, mỗi message là một công việc cần làm, và chỉ cần làm một lần.
Ví dụ: “gửi email xác nhận đơn hàng #1234”. Ba consumer cùng đọc queue, nhưng chỉ một consumer nhận message này và gửi email. Hai consumer còn lại nhận message khác. Kết quả: email chỉ gửi một lần, workload được chia đều.
Pub/sub (topic)
Mỗi message được phân phối tới tất cả subscriber quan tâm. Producer publish message vào topic, mọi consumer đã subscribe topic đó đều nhận được bản copy. Đây là mô hình event broadcasting, “có chuyện xảy ra, ai quan tâm thì xử lý”.
Ví dụ: event “order.created” publish vào topic. Inventory service subscribe để trừ kho, notification service subscribe để gửi email, analytics service subscribe để ghi metric. Mỗi service nhận cùng event và xử lý theo logic riêng. Thêm service mới chỉ cần subscribe, producer không thay đổi.
Sự khác biệt quan trọng: trong point-to-point, ba consumer cạnh tranh nhau để nhận message, mỗi message chỉ đến một nơi. Trong pub/sub, ba subscriber đều nhận cùng message, mỗi message được nhân bản. Kafka, RabbitMQ, và SQS đều hỗ trợ cả hai mô hình nhưng với cách tiếp cận khác nhau, và đó là nơi sự khác biệt bắt đầu.
Kafka, append-only log cho event streaming
Kafka không tự nhận mình là “message queue”, nó là distributed event streaming platform. Sự khác biệt không chỉ là marketing: kiến trúc của Kafka xây trên một abstraction hoàn toàn khác so với queue truyền thống.
Log, partition, và offset
Khái niệm cốt lõi của Kafka là append-only log. Mỗi topic là một log, message được ghi vào cuối log theo thứ tự, mỗi message nhận một offset (số thứ tự tăng dần). Message không bị xoá sau khi consumer đọc, nó nằm trong log cho đến khi hết retention period (mặc định 7 ngày, có thể set vô hạn).
Đây là khác biệt lớn nhất với RabbitMQ và SQS: message tồn tại độc lập với consumer. Consumer chỉ là pointer (offset) chỉ vào vị trí trong log. Consumer A đọc tới offset 1000, consumer B mới join thì bắt đầu từ offset 0, cùng log, hai vị trí khác nhau, không xung đột. Consumer crash thì restart và đọc tiếp từ offset đã commit cuối cùng, message không mất vì nó vẫn nằm trong log.
Mỗi topic được chia thành nhiều partition, mỗi partition là một log riêng biệt, phân bố trên các broker khác nhau trong cluster. Partition là đơn vị song song hoá: nhiều consumer có thể đọc nhiều partition cùng lúc. Số partition quyết định mức song song tối đa, 12 partition nghĩa là tối đa 12 consumer đọc đồng thời trong cùng consumer group.
Message được gửi vào partition nào phụ thuộc vào partition key. Cùng key luôn vào cùng partition, đảm bảo ordering cho message cùng key. Ví dụ: dùng order_id làm partition key thì tất cả event liên quan đến order #1234 (created, paid, shipped, delivered) luôn vào cùng partition, consumer đọc đúng thứ tự. Không có key thì Kafka round-robin qua các partition, ordering không đảm bảo xuyên partition.
Consumer group
Consumer group là cách Kafka kết hợp pub/sub và point-to-point. Trong cùng một consumer group, mỗi partition được gán cho đúng một consumer, đây là point-to-point ở mức partition. Nhưng nhiều consumer group có thể subscribe cùng topic, mỗi group nhận toàn bộ message, đây là pub/sub ở mức group.
Ví dụ thực tế: topic orders có 6 partition. Consumer group inventory-service có 3 instance, mỗi instance đọc 2 partition. Consumer group analytics-service có 2 instance, mỗi instance đọc 3 partition. Cả hai group đều nhận toàn bộ message, nhưng bên trong mỗi group, message được chia đều giữa các instance. Thêm consumer group notification-service không ảnh hưởng hai group kia, đây là sức mạnh của mô hình log.
Khi consumer trong group crash hoặc join mới, Kafka thực hiện rebalance, phân phối lại partition giữa các consumer còn lại. Rebalance gây delay vài giây đến vài chục giây tuỳ cấu hình, trong thời gian đó partition bị reassign không được consume. Đây là trade-off cần biết khi design, rebalance quá thường xuyên (consumer flapping) gây processing gap.
Offset management và exactly-once
Consumer commit offset sau khi xử lý message thành công. Nếu consumer crash trước khi commit, message sẽ được đọc lại khi consumer restart, đây là at-least-once delivery. Message có thể được xử lý nhiều lần, nên consumer phải idempotent, xử lý cùng message hai lần cho kết quả giống hệt xử lý một lần.
Kafka từ version 0.11 hỗ trợ exactly-once semantics (EOS) cho trường hợp produce-consume-produce trong cùng Kafka cluster, tức là đọc message từ topic A, xử lý, ghi kết quả vào topic B, với đảm bảo mỗi message chỉ được xử lý đúng một lần. Cơ chế dựa trên idempotent producer (producer ID + sequence number) và transactional API (atomic write across multiple partitions).
Nhưng cần hiểu giới hạn: EOS chỉ hoạt động trong phạm vi Kafka, nếu consumer đọc message Kafka rồi ghi vào PostgreSQL, “exactly-once” end-to-end phụ thuộc vào consumer tự implement idempotency ở tầng database. Kafka đảm bảo message không duplicate trong log, nhưng không kiểm soát được side effect bên ngoài Kafka.
Retention và replay
Retention là tính năng mà queue truyền thống không có. Message trong Kafka không biến mất sau khi đọc, nó tồn tại theo retention policy (thời gian hoặc dung lượng). Điều này cho phép replay: consumer mới join có thể đọc lại toàn bộ lịch sử message từ đầu, hoặc consumer hiện tại có thể reset offset về thời điểm cụ thể để reprocess.
Use case thực tế: team analytics deploy model mới, cần chạy lại toàn bộ order event 30 ngày qua để rebuild aggregate. Với queue truyền thống thì phải backup data riêng. Với Kafka, chỉ cần reset offset consumer group về timestamp 30 ngày trước, Kafka replay tự động.
Retention cũng là lý do Kafka tốn disk nhiều hơn RabbitMQ/SQS đáng kể. Giữ 7 ngày message cho topic 10,000 msg/s × 1KB/msg = khoảng 6 TB. Cần tính disk capacity từ đầu, và đặt retention hợp lý, không phải topic nào cũng cần 30 ngày retention.
Khi nào chọn Kafka
Kafka toả sáng ở ba kịch bản. Thứ nhất, event streaming, khi message là event (sự kiện đã xảy ra) và nhiều consumer cần đọc cùng event stream: analytics, search indexing, audit log, CDC (Change Data Capture). Thứ hai, throughput cao, Kafka xử lý hàng triệu message/giây trên cluster vừa phải nhờ sequential disk IO và batching. Thứ ba, replay và reprocessing, khi cần khả năng đọc lại lịch sử message, rebuild state, hoặc audit.
Kafka không phù hợp cho task queue đơn giản nơi message cần route phức tạp theo nội dung (RabbitMQ làm tốt hơn), hoặc khi team nhỏ không muốn vận hành cluster (SQS đơn giản hơn nhiều).
RabbitMQ, smart broker, routing linh hoạt
RabbitMQ theo triết lý ngược với Kafka: broker thông minh, consumer đơn giản. Broker biết message đi đâu, consumer nào đang sẵn sàng, message nào đã được acknowledge. Queue truyền thống nhất, message được gửi đến, consumer lấy ra, acknowledge, message biến mất.
Exchange, binding, và queue
RabbitMQ có một layer abstraction mà Kafka không có: exchange. Producer không gửi message trực tiếp vào queue mà gửi vào exchange. Exchange dựa trên routing key của message và binding rules để quyết định chuyển message vào queue nào.
Bốn loại exchange tạo ra bốn mô hình routing khác nhau.
Direct exchange route message tới queue có binding key khớp chính xác routing key. Producer gửi message với routing key order.created, chỉ queue nào bind với key order.created mới nhận. Dùng cho point-to-point đơn giản, mỗi message type đi vào queue riêng.
Topic exchange cho phép pattern matching với wildcard. Queue bind với order.* nhận cả order.created, order.paid, order.cancelled. Queue bind với *.created nhận cả order.created lẫn user.created. Linh hoạt hơn direct, phù hợp khi consumer muốn nhận subset event theo pattern.
Fanout exchange bỏ qua routing key hoàn toàn, broadcast message tới tất cả queue đã bind. Đây là pub/sub thuần tuý. Dùng khi mọi consumer cần nhận mọi message, ví dụ notification fanout.
Headers exchange route dựa trên message headers thay vì routing key, ít dùng trong thực tế nhưng hữu ích khi routing logic phức tạp hơn string pattern.
Sự linh hoạt routing này là thế mạnh lớn nhất của RabbitMQ so với Kafka và SQS. Nếu bài toán cần message đi tới consumer khác nhau dựa trên nội dung message (routing key, header), RabbitMQ design cho chính xác use case đó.
Acknowledgement, reject, và prefetch
Khi consumer nhận message từ RabbitMQ, nó phải acknowledge (ack) sau khi xử lý xong. Broker chỉ xoá message khỏi queue sau khi nhận ack. Nếu consumer crash trước khi ack, message được đưa lại vào queue và giao cho consumer khác, đây là cơ chế at-least-once delivery.
Consumer cũng có thể nack (negative acknowledge), báo broker rằng xử lý thất bại. Kèm theo nack, consumer chọn requeue (đẩy message lại vào queue để thử lại) hoặc reject (bỏ message, có thể chuyển sang dead letter queue). Nack + requeue cần cẩn thận: nếu message liên tục fail rồi requeue, nó tạo vòng lặp vô hạn, poison message. Phải có cơ chế đếm retry và chuyển sang DLQ sau N lần thất bại.
Prefetch (basic.qos) quyết định broker gửi bao nhiêu message cho consumer cùng lúc mà chưa cần đợi ack. Prefetch = 1 nghĩa là consumer nhận một message, xử lý xong ack rồi mới nhận message tiếp, throughput thấp nhưng đảm bảo fair distribution và không overload consumer chậm. Prefetch = 50 cho phép consumer buffer 50 message, throughput cao hơn nhưng nếu consumer crash thì 50 message cần requeue. Chọn prefetch phụ thuộc vào thời gian xử lý mỗi message và khả năng chịu redelivery.
Clustering và quorum queue
RabbitMQ clustering cho HA nhưng lịch sử phức tạp hơn Kafka. Trước đây, classic mirrored queue replicate message qua các node trong cluster, nhưng cơ chế này có nhiều edge case khó chịu: split brain khi network partition, message loss khi mirror chưa sync kịp.
Từ RabbitMQ 3.8, quorum queue thay thế mirrored queue cho production workload cần HA. Quorum queue dựa trên Raft consensus, message chỉ được confirm sau khi đa số node (quorum) đã ghi, đảm bảo không mất message khi minority node fail. Trade-off: quorum queue tốn resource hơn classic queue (Raft overhead), nhưng data safety tốt hơn đáng kể.
Nếu đang setup RabbitMQ mới cho production, dùng quorum queue mặc định cho queue cần durability. Classic queue vẫn phù hợp cho transient message (cache invalidation, log routing) nơi mất vài message không ảnh hưởng nghiêm trọng.
Khi nào chọn RabbitMQ
RabbitMQ phù hợp nhất cho task queue và routing phức tạp. Ví dụ: hệ thống xử lý ảnh, user upload ảnh, message vào queue, worker pool lấy ra resize/compress/upload CDN. Mỗi message là một task cần làm đúng một lần, nhiều worker cạnh tranh nhau nhận task. RabbitMQ phân phối đều, hỗ trợ priority queue (message quan trọng xử lý trước), delay queue (xử lý sau N giây), và retry logic tốt.
RabbitMQ cũng phù hợp khi cần request-reply pattern, gửi message và chờ response qua correlation ID. Kafka không thiết kế cho pattern này.
Không chọn RabbitMQ khi cần replay message (message biến mất sau ack), throughput cực cao liên tục (Kafka tối ưu IO tốt hơn cho append-only), hoặc khi team không muốn vận hành cluster (SQS đơn giản hơn).
SQS, managed simplicity
Amazon SQS không cạnh tranh với Kafka hay RabbitMQ về tính năng, nó cạnh tranh về operational simplicity. Không có broker cần vận hành, không có cluster cần tune, không có disk cần monitor. Gửi message vào, đọc ra, xoá sau khi xử lý. AWS lo phần còn lại: scaling, durability, availability.
Standard vs FIFO queue
SQS có hai loại queue với đặc tính rất khác nhau.
Standard queue cho throughput gần như không giới hạn, hàng triệu message/giây mà không cần provision capacity. Trade-off: best-effort ordering (message có thể đến không đúng thứ tự) và at-least-once delivery (message có thể được deliver nhiều hơn một lần). Với hầu hết use case async đơn giản (gửi email, resize ảnh, log processing), hai trade-off này chấp nhận được vì consumer idempotent sẽ xử lý đúng.
FIFO queue đảm bảo exactly-once processing và strict ordering trong cùng message group. Mỗi message có MessageGroupId, message cùng group được deliver theo đúng thứ tự gửi. Giống khái niệm partition key trong Kafka, ordering đảm bảo trong group, không đảm bảo xuyên group. Trade-off: throughput giới hạn 300 msg/s per group (batch lên 3000), thấp hơn nhiều so với Standard queue.
Chọn FIFO khi ordering quan trọng (financial transactions, inventory updates) và throughput vừa phải. Chọn Standard khi throughput là ưu tiên và consumer đã idempotent.
Visibility timeout
Đây là cơ chế đặc trưng của SQS mà Kafka và RabbitMQ không có ở cùng level. Khi consumer nhận message từ SQS, message không bị xoá, nó trở nên invisible với các consumer khác trong khoảng thời gian visibility timeout (mặc định 30 giây). Nếu consumer xử lý xong và gọi DeleteMessage trong thời gian đó, message biến mất. Nếu consumer crash hoặc timeout, message tự động visible lại, consumer khác sẽ nhận lại.
Visibility timeout quá ngắn: consumer chưa xử lý xong mà message đã visible lại, consumer khác nhận và xử lý trùng, duplicate processing. Visibility timeout quá dài: consumer crash nhưng message phải chờ hết timeout mới được retry, tăng latency recovery. Cần set phù hợp với thời gian xử lý trung bình, thường gấp 3-5 lần processing time trung bình.
SQS cũng hỗ trợ ChangeMessageVisibility, consumer có thể gia hạn timeout nếu biết mình cần thêm thời gian. Hữu ích cho task xử lý lâu (video transcoding, report generation) mà thời gian không cố định.
Long polling
Mặc định SQS dùng short polling, consumer gọi ReceiveMessage, SQS trả về ngay (có thể trả empty nếu queue trống). Consumer phải poll liên tục, tốn API call và tiền. Long polling (WaitTimeSeconds > 0, tối đa 20 giây) cho phép SQS giữ connection và chỉ trả response khi có message hoặc hết timeout, giảm số API call đáng kể (giảm tới 90% so với short polling), giảm chi phí, và giảm latency (message mới đến được trả ngay thay vì đợi poll cycle tiếp theo).
Luôn bật long polling cho SQS consumer, không có lý do dùng short polling trong production trừ khi có requirement đặc biệt.
Dead letter queue
SQS có native DLQ support, configure maxReceiveCount trên queue chính, khi message nhận quá N lần mà không được delete (consumer liên tục fail), SQS tự động chuyển message sang DLQ. DLQ là một SQS queue thường, có thể monitor message count, inspect nội dung, và replay khi đã fix bug.
Đây là một trong những tính năng mà SQS implement “sẵn” một cách rất sạch. Với RabbitMQ, cần configure dead letter exchange và binding. Với Kafka, phải tự implement DLQ topic và retry logic trong consumer code.
Khi nào chọn SQS
SQS là lựa chọn mặc định khi bài toán là async task đơn giản trên AWS và team không muốn vận hành messaging infrastructure. Gửi email, xử lý webhook, trigger Lambda, decouple microservice, SQS giải quyết gọn gàng với zero ops overhead. Chi phí theo message ($0.40/triệu request cho Standard), không có cost cố định cho cluster.
Không chọn SQS khi cần replay message (message bị xoá sau delete), routing phức tạp (không có exchange concept), throughput cực cao với ordering (FIFO bị giới hạn 300/s per group), hoặc khi hệ thống không nằm trên AWS (vendor lock-in).
So sánh ordering guarantee
Ordering là một trong những khía cạnh dễ nhầm nhất khi chọn messaging system. “Có đảm bảo ordering không?” không phải câu hỏi yes/no, đáp án luôn là “phụ thuộc vào cách configure”.
Kafka đảm bảo ordering trong cùng partition. Message cùng partition key vào cùng partition, consumer đọc đúng thứ tự. Nhưng xuyên partition thì không, hai message gửi vào hai partition khác nhau có thể đến consumer theo thứ tự bất kỳ. Muốn ordering cho entity cụ thể (user, order), dùng entity ID làm partition key. Muốn total ordering cho toàn topic thì chỉ dùng 1 partition, nhưng mất khả năng song song hoá.
RabbitMQ đảm bảo ordering trong cùng một queue, một consumer. Nếu queue có nhiều consumer (competing consumers), ordering giữa các consumer không đảm bảo, consumer A nhận message 1 nhưng xử lý chậm, consumer B nhận message 2 và xử lý xong trước. Fanout exchange broadcast message tới nhiều queue, ordering giữa các queue không đảm bảo. Nếu cần strict ordering, dùng một queue với một consumer, nhưng mất throughput.
SQS Standard không đảm bảo ordering, best-effort chỉ nghĩa là “thường đúng thứ tự, nhưng không cam kết”. SQS FIFO đảm bảo strict ordering trong cùng MessageGroupId. Khác group thì xử lý song song, không ordering giữa group.
Bài học thực tế: nếu bạn chỉ cần ordering cho entity (tất cả event của order #1234 phải đúng thứ tự), partition key (Kafka) hoặc message group (SQS FIFO) đều giải quyết tốt mà không hy sinh throughput. Nếu cần total ordering cho toàn bộ message stream, đó là constraint rất đắt, cân nhắc kỹ trước khi yêu cầu.
At-least-once, at-most-once, exactly-once
Ba thuật ngữ này xuất hiện trong mọi cuộc thảo luận về messaging, nhưng ý nghĩa thực tế và giới hạn thường bị hiểu sai.
At-most-once: message có thể mất nhưng không bao giờ bị xử lý hai lần. Cách đạt được: consumer ack trước khi xử lý. Nếu crash sau ack nhưng trước khi xử lý xong, message đã bị xoá khỏi queue, mất. Dùng khi mất message chấp nhận được (metric sampling, non-critical log).
At-least-once: message không mất nhưng có thể bị xử lý nhiều lần. Cách đạt được: consumer xử lý xong rồi mới ack. Crash trước ack thì message redelivery, xử lý lại. Đây là mặc định của cả ba hệ thống (Kafka, RabbitMQ, SQS Standard) khi configure đúng. Consumer phải idempotent, xử lý message thứ hai cùng nội dung không tạo side effect khác. Idempotency thường implement bằng cách lưu message ID đã xử lý vào database/cache, check trước khi xử lý.
Exactly-once: mỗi message được xử lý đúng một lần, không mất, không trùng. Đây là holy grail nhưng trong distributed system, exactly-once end-to-end cực kỳ khó đạt được. Kafka EOS đảm bảo exactly-once trong phạm vi Kafka-to-Kafka (consume từ topic A, produce vào topic B). SQS FIFO đảm bảo exactly-once delivery (message không duplicate trong queue) nhưng consumer vẫn cần handle redelivery nếu delete call fail.
Thực tế, hầu hết production system dùng at-least-once + idempotent consumer, đây là sweet spot giữa safety và complexity. Thiết kế consumer idempotent từ đầu thay vì phụ thuộc vào exactly-once guarantee của infrastructure. Cách phổ biến: lưu (message_id, status) vào bảng processed_messages trong cùng transaction với business logic, nếu message_id đã tồn tại thì skip.
Dead letter queue, nơi chứa message thất bại
Dead letter queue (DLQ) là queue phụ chứa message mà consumer không thể xử lý thành công sau nhiều lần retry. Nếu không có DLQ, message lỗi sẽ hoặc bị mất (ack rồi bỏ qua) hoặc block queue (retry vô hạn, chặn message sau). DLQ giải quyết cả hai vấn đề: message lỗi được tách ra, queue chính tiếp tục chảy, và team có thể inspect message lỗi sau để debug và replay.
SQS có DLQ native, configure RedrivePolicy với maxReceiveCount. Message nhận quá N lần tự động chuyển sang DLQ queue. Đơn giản, không cần code gì thêm.
RabbitMQ dùng dead letter exchange, khi message bị reject (nack without requeue) hoặc TTL hết, nó được route sang exchange khác (DLX), từ đó vào DLQ queue. Cần configure binding cho DLX, linh hoạt hơn SQS nhưng phải setup.
Kafka không có DLQ native. Consumer phải tự implement: catch exception khi xử lý message, retry N lần, nếu vẫn fail thì produce message vào topic riêng (ví dụ orders.dlq). Kafka Streams và các framework như Spring Kafka có utility cho việc này, nhưng vẫn là application-level logic.
Monitoring DLQ là bắt buộc, DLQ mà không ai monitor thì message lỗi tích tụ im lặng cho đến khi user complain. Đặt alert khi DLQ message count > 0 hoặc tăng đột biến. Mỗi message trong DLQ là một bug hoặc edge case cần điều tra, không phải thứ “để đó rồi tính”.
Replay strategy: sau khi fix bug gây ra message lỗi, cần cơ chế replay message từ DLQ về queue chính. SQS có DLQ redrive (native feature). RabbitMQ cần consume từ DLQ rồi republish. Kafka consume từ DLQ topic rồi produce lại vào topic chính. Quan trọng là test replay trước khi cần nó, đến lúc incident mới tìm cách replay thì muộn.
Backpressure, khi consumer không theo kịp producer
Backpressure xảy ra khi producer gửi message nhanh hơn consumer xử lý. Nếu không xử lý, queue phình lên vô hạn, memory hết (RabbitMQ), disk đầy (Kafka), chi phí tăng (SQS). Mỗi hệ thống có cơ chế backpressure khác nhau.
Kafka: consumer lag là metric chính, khoảng cách giữa offset mới nhất (log end) và offset consumer đã commit. Lag tăng nghĩa là consumer chậm hơn producer. Kafka không chủ động “chặn” producer khi consumer chậm, message vẫn được ghi vào log cho đến khi hết disk. Backpressure xử lý bằng cách scale consumer (thêm instance), hoặc tăng partition để song song hoá. Nếu lag quá lớn và retention hết trước khi consumer đọc xong, message sẽ bị truncate, mất data thực sự. Monitor consumer lag là alert cấp cao nhất cho Kafka consumer.
RabbitMQ: prefetch limit là cơ chế backpressure tự nhiên. Prefetch = 10 nghĩa là broker chỉ gửi tối đa 10 unacked message cho consumer, nếu consumer chậm, broker tự động giảm tốc gửi. Queue memory limit cũng là safety net: khi queue quá lớn, RabbitMQ có thể block publisher (flow control) hoặc page message xuống disk. Nhưng RabbitMQ khi queue quá lớn performance giảm đáng kể, nó không thiết kế để giữ hàng triệu message pending.
SQS: backpressure implicit qua visibility timeout và consumer polling rate. SQS có thể giữ message rất lâu (retention tối đa 14 ngày) nên queue phình không gây crash, nhưng message “cũ” vẫn cần xử lý. Scale consumer bằng cách thêm poller, hoặc dùng Lambda trigger (SQS event source) để auto-scale consumer theo queue depth. SQS không charge theo queue size mà theo API call, nên queue lớn không tăng chi phí storage, nhưng tăng chi phí processing khi consumer phải xử lý backlog.
Operational complexity
Đây là yếu tố quyết định mà nhiều team đánh giá thấp khi chọn messaging system. Tính năng kỹ thuật trên giấy không phản ánh effort vận hành hàng ngày.
Kafka có operational overhead cao nhất. Cluster Kafka cần tối thiểu 3 broker cho production (replication factor 3), cộng thêm ZooKeeper cluster (3-5 node) hoặc KRaft (Kafka 3.3+ bỏ ZooKeeper). Cần monitor broker health, partition replication lag, under-replicated partitions, disk usage, consumer lag. Upgrade Kafka cluster yêu cầu rolling restart cẩn thận, version compatibility giữa broker và client cần kiểm tra. Topic configuration (partition count, retention, replication factor) cần planning từ đầu vì thay đổi sau khó khăn, tăng partition count được nhưng giảm không được, và tăng partition ảnh hưởng key ordering.
Nếu chạy trên cloud, managed Kafka (Confluent Cloud, AWS MSK, Aiven) giảm đáng kể ops overhead nhưng chi phí cao hơn self-host nhiều lần, tính trước TCO.
RabbitMQ trung bình về operational complexity. Cluster thường 3 node cho HA với quorum queue. Monitoring cần chú ý memory usage (RabbitMQ giữ message index trong RAM), queue depth, consumer count, message rate. RabbitMQ có management UI built-in, tiện cho debug nhưng không thay thế monitoring hệ thống (Prometheus exporter có sẵn). Upgrade RabbitMQ đơn giản hơn Kafka nhưng vẫn cần rolling upgrade cẩn thận, đặc biệt khi đổi major version.
Cạm bẫy RabbitMQ hay gặp: queue tích message quá nhiều (consumer chậm hoặc chết) → RabbitMQ page message xuống disk → performance giảm drastically → cascade effect lên các queue khác trên cùng cluster. Cần alert sớm khi queue depth tăng bất thường.
SQS gần như zero ops. Không có server, không có cluster, không cần monitor infrastructure. AWS đảm bảo SLA 99.9% availability. (Nói thật: 70% team chọn SQS không phải vì phân tích kỹ, mà vì Lambda đã có sẵn trong stack và SQS event trigger quá tiện, lý do thực dụng hoàn toàn hợp lệ.) Chỉ cần monitor application-level metric: message age (thời gian message nằm trong queue), approximate message count, DLQ depth. Trade-off: ít control hơn, không tune được internal, không access raw metric của broker, và phụ thuộc hoàn toàn vào AWS.
Anti-pattern hay gặp
Dùng queue như database
Đây là anti-pattern phổ biến nhất. Team lưu trạng thái quan trọng trong message body, để message nằm trong queue lâu (không consume), và dựa vào queue retention như storage layer. Queue không phải database, không có query capability, không có index, không có transaction isolation. Message mất khi retention hết hoặc khi broker gặp sự cố ngoài replication scope. Nếu cần lưu trạng thái, lưu vào database rồi gửi message reference (ID) qua queue.
Kafka retention dài (tuần, tháng) khiến nhiều team nhầm lẫn, “Kafka giữ data nên không cần database riêng”. Kafka topic không phải table, không query by field, không update in-place, không join. Kafka Streams và ksqlDB cho phép query trên stream, nhưng đó là stream processing, không phải database replacement.
Fire-and-forget không monitor
Gửi message vào queue rồi assume “nó sẽ được xử lý”. Không monitor consumer lag, không check DLQ, không alert khi queue depth tăng. Kết quả: message tích tụ vì consumer crash tuần trước mà không ai biết, user không nhận email xác nhận suốt 7 ngày mới có người report.
Mỗi queue trên production cần ít nhất ba alert: message age > threshold (message nằm quá lâu không được xử lý), DLQ count > 0 (có message thất bại), consumer count = 0 (không ai đang consume). Thiếu bất kỳ alert nào là mời incident vào nhà.
Ignore DLQ
Có DLQ nhưng không ai nhìn. Message lỗi tích tụ hàng nghìn, team discover sau 3 tháng khi audit. Mỗi message DLQ có thể là bug chưa fix, edge case chưa handle, hoặc data inconsistency đang lan. DLQ cần review process, ít nhất weekly inspect message mới nhất, categorize lỗi, tạo ticket fix. Automate nếu được: parse error message từ DLQ, group by error type, alert cho owner team.
Message quá lớn
Nhét payload 5MB vào message body, ảnh, PDF, JSON response lớn. Queue không thiết kế cho payload lớn: SQS giới hạn 256KB (Extended Client Library cho S3 offload), Kafka mặc định 1MB (tăng được nhưng ảnh hưởng performance), RabbitMQ chấp nhận nhưng performance giảm với message lớn.
Pattern chuẩn: lưu payload lớn vào object storage (S3, GCS), gửi reference URL qua message. Consumer đọc reference, fetch payload từ storage. Giữ message nhỏ (<100KB) cho throughput và reliability tối ưu.
Không idempotent consumer
At-least-once delivery nghĩa là message có thể đến nhiều lần, mà hầu hết hệ thống dùng at-least-once. Consumer không idempotent sẽ tạo side effect khi nhận duplicate: gửi email hai lần, trừ tiền hai lần, tạo record trùng. Idempotency phải được thiết kế vào consumer từ ngày đầu, không phải “xử lý sau”. Dùng deduplication key (message ID, idempotency key) và check trước khi xử lý, đơn giản nhưng cần kỷ luật.
Chọn theo bài toán
Thay vì so sánh trừu tượng, ánh xạ use case cụ thể vào công cụ phù hợp sẽ thực tế hơn.
Event streaming, CDC, audit log, Kafka. Nhiều consumer cần đọc cùng event stream, cần replay, cần retention dài. Kafka sinh ra cho bài toán này.
Task queue, job processing, RabbitMQ. Worker pool xử lý task (resize ảnh, gửi email, generate report), cần priority, delay, retry logic. RabbitMQ routing và ack model phù hợp hơn Kafka cho pattern này.
Async decoupling đơn giản trên AWS, SQS. Tách service, buffer request, trigger Lambda. Không cần vận hành infrastructure, pay per message. SQS + Lambda là combo mạnh cho event-driven architecture serverless.
Real-time analytics pipeline, Kafka. Ingest event từ nhiều source, process qua Kafka Streams hoặc Flink, output vào data warehouse. Throughput cao, ordering per key, replay khi reprocess.
IoT device telemetry, Kafka hoặc SQS tuỳ scale. Hàng triệu device gửi data, Kafka nếu cần stream processing real-time, SQS nếu chỉ cần buffer rồi batch process.
Request-reply async, RabbitMQ. Gửi request, chờ response qua correlation ID và reply-to queue. Kafka không thiết kế cho pattern này, SQS có thể làm được nhưng clunky.
Nếu phân vân giữa hai lựa chọn, hỏi ba câu: team có experience vận hành cái nào? Hệ thống đang chạy trên platform nào (AWS → SQS có advantage)? Bài toán cần replay/retention không (yes → Kafka, no → RabbitMQ hoặc SQS)? Ba câu này thường cho đáp án rõ ràng hơn so sánh feature matrix.
Công cụ messaging tốt nhất là cái team bạn đã có kinh nghiệm vận hành. Hỏi ba câu: cần replay không? Đang trên AWS không? Cần routing phức tạp không?, đáp án thường hiện ra trước khi đọc hết feature matrix. DLQ là bắt buộc dù dùng tool nào, và consumer idempotent phải thiết kế từ ngày đầu chứ không phải “xử lý sau”.