Quay lại bài viết
19 thg 5, 2026
25 min read

Amazon SQS Deep Dive: Hàng đợi tin nhắn phân tán xử lý hàng triệu messages mỗi ngày

Bạn đang vận hành một hệ thống e-commerce. Mọi thứ hoạt động ổn định với vài nghìn requests mỗi phút — cho đến ngày flash sale. Lượng traffic tăng gấp 20 lần trong vòng vài giây. Hàng nghìn đơn hàng đổ về cùng lúc, mỗi đơn đều cần ghi xuống database. Connection pool cạn kiệt, queries xếp hàng chờ, timeout cascades, và rồi database sụp. Khách hàng thấy lỗi 500, team dev thức đêm rollback, business mất doanh thu.

Vấn đề cốt lõi ở đây không phải database yếu — mà là kiến trúc tight coupling. Application ghi thẳng xuống database, nên tốc độ ghi bị giới hạn bởi khả năng xử lý của database. Traffic tăng đột biến → database nghẽn → cả hệ thống đổ theo.

Giải pháp? Đặt một hàng đợi tin nhắn (message queue) ở giữa. Thay vì ghi thẳng xuống database, application chỉ cần “ném” request vào hàng đợi. Phía bên kia, các worker lấy message ra và ghi xuống database với tốc độ mà database chịu được. Hàng đợi đóng vai trò như một bộ giảm xóc — hấp thụ traffic đột biến và giải phóng từ từ, đều đặn.

Đây chính là bài toán mà Amazon SQS (Simple Queue Service) giải quyết. Một hàng đợi tin nhắn fully managed, không cần provisioning, không cần lo capacity planning. Bạn gửi message vào, consumer lấy ra. AWS lo toàn bộ phần availability, durability, và scaling phía dưới.

Bài viết này sẽ đi sâu vào kiến trúc bên trong của SQS — từ cách message được lưu trữ và replicate trên nhiều server phân tán, sự khác biệt giữa Standard Queue và FIFO Queue (và tại sao Standard Queue deliver message không theo thứ tự), visibility timeout, long polling, Dead Letter Queue, cho đến các patterns thực tế như tích hợp Auto Scaling Group và dùng SQS làm buffer cho database writes.


1. SQS là gì?

Amazon SQS (Simple Queue Service) là một dịch vụ message queue fully managed của AWS — cho phép các ứng dụng gửi, lưu trữ, và nhận message giữa các thành phần phần mềm với bất kỳ khối lượng nào, mà không lo mất message hay cần các service khác phải online cùng lúc.

Nói đơn giản, SQS hoạt động như một hộp thư bưu điện. Người gửi (producer) bỏ thư vào hộp, người nhận (consumer) đến lấy thư khi sẵn sàng. Người gửi không cần biết người nhận là ai, đang ở đâu, hay có đang online hay không — thư vẫn nằm an toàn trong hộp cho đến khi được lấy.

Một vài đặc điểm quan trọng:

SQS có hai loại queue: Standard QueueFIFO Queue. Chúng ta sẽ đi sâu vào sự khác biệt ở phần tiếp theo.

Tiêu chíSQSKafkaRabbitMQ
Delivery modelPull — consumer chủ động poll messagePull — consumer chủ động kéo từ partitionPush — broker đẩy message đến consumer
OrderingBest-effort (Standard) / Strict per group (FIFO)Strict per partitionStrict per queue
RetentionTối đa 14 ngàyTuỳ cấu hình (ngày/dung lượng)Cho đến khi consume
ThroughputGần như vô hạn (Standard)Rất cao (~1M+ msg/s)Trung bình (~50K msg/s)
ManagementFully managedSelf-managed hoặc Amazon MSKSelf-managed
Primary use caseTask queue, decoupling, bufferEvent streaming, log aggregationTask queue, request-reply

2. Kiến trúc bên trong — “How Things Appear” vs “How Things Actually Are”

Khi bạn nhìn vào SQS từ bên ngoài, mọi thứ trông rất đơn giản: một hàng đợi, message vào từ đầu này, ra từ đầu kia, theo thứ tự FIFO (First In, First Out). Đây là “How Things Appear” — cái mà bạn thấy.

Nhưng bên dưới lớp vỏ đơn giản đó là một hệ thống phân tán phức tạp. Đây là “How Things Actually Are”.

2.1. Kiến trúc 3 tầng

SQS được xây dựng trên ba tầng chính:

Frontend Layer — tầng tiếp nhận request. Load balancer phân phối request đến các frontend server, nơi thực hiện authentication (xác thực qua IAM), validation (kiểm tra kích thước message, queue tồn tại hay không), và routing request xuống tầng storage.

Backend Storage Layer — trái tim của SQS. Đây là một distributed key-value store — hệ thống lưu trữ phân tán dạng key-value. Message được lưu dưới dạng cặp key-value, trong đó key được tạo từ queue ID và message metadata, value là message body (tối đa 256KB) cùng các attributes. Điểm quan trọng: message được shard (phân mảnh) trên nhiều storage node dựa theo queue ID để cân bằng tải.

Coordination Layer — tầng điều phối, sử dụng cơ chế distributed consensus (tương tự Paxos hoặc Raft) để đảm bảo tính nhất quán giữa các node. Với Standard Queue, tầng này hoạt động ở mức tối thiểu để ưu tiên throughput. Với FIFO Queue, tầng này hoạt động chặt chẽ hơn để đảm bảo thứ tự và exactly-once delivery.

2.2. Message được lưu trữ như thế nào?

Khi producer gửi một message vào SQS, điều xảy ra phía sau là:

  1. Frontend server nhận request, xác thực, validate.
  2. Message được replicate đồng bộ (synchronous replication) lên nhiều storage node nằm ở nhiều Availability Zone (AZ) khác nhau.
  3. SQS sử dụng cơ chế quorum-based writes — ghi được coi là thành công khi M trong N replicas xác nhận đã lưu. Điều này đảm bảo message tồn tại ngay cả khi một AZ gặp sự cố.
  4. Chỉ sau khi đủ quorum, SQS mới trả về response thành công cho producer.

Nhờ cơ chế này, SQS đạt độ bền dữ liệu cực cao — AWS công bố 11 nines durability (99.999999999%). Message của bạn gần như không thể mất.

2.3. Tại sao Standard Queue deliver message không theo thứ tự?

Đây là câu hỏi mà nhiều người thắc mắc: “SQS là queue, queue thì phải FIFO chứ? Tại sao message lại đến không theo thứ tự?”

Câu trả lời nằm ở kiến trúc phân tán bên dưới.

Khi bạn gửi message A, rồi B, rồi C vào một Standard Queue — chúng không nằm trên cùng một server. SQS shard message trên nhiều storage node khác nhau để cân bằng tải. Message A có thể nằm ở node 1, B ở node 3, C ở node 2.

Khi consumer gọi ReceiveMessage, SQS không scan toàn bộ các node. Thay vào đó, nó kết nối ngẫu nhiên đến một vài node và trả về bất kỳ message nào tìm thấy ở đó. Nếu nó hit vào node 3 trước → bạn nhận B trước A.

Đây không phải bug — đây là trade-off có chủ đích, đây cũng là lý do AWS gọi nó là best-effort ordering. Bằng cách không enforce ordering, SQS có thể phân tán message trên nhiều node hơn, phục vụ request từ bất kỳ node nào, và đạt được throughput gần như vô hạn. Nếu phải enforce ordering, SQS sẽ cần coordinate giữa các node — tốn thời gian, giảm throughput, giới hạn scale.

2.4. FIFO Queue — đánh đổi throughput lấy ordering

Ở phần trước, ta thấy Standard Queue mất ordering vì message bị phân tán ngẫu nhiên trên nhiều node. FIFO Queue giải quyết vấn đề này bằng cách đảm bảo tất cả message cùng một Message Group ID luôn được route về cùng một partition (cùng một storage node hoặc nhóm node).

Khi producer gửi message với MessageGroupId = "order-123", SQS sử dụng giá trị này để hash và xác định partition đích — tương tự cách Kafka dùng partition key. Mọi message với cùng group ID đều đến cùng một partition, nơi SQS gán cho chúng sequence number tăng dần. Nhờ tất cả nằm trên cùng partition, việc duy trì thứ tự trở thành bài toán local — không cần distributed coordination phức tạp giữa nhiều node.

Khi consumer gọi ReceiveMessage, SQS trả về message theo đúng thứ tự sequence number trong mỗi Message Group. Và quan trọng: chỉ một consumer có thể xử lý message từ một Message Group tại một thời điểm. Nếu message #1 của group “order-123” đang in-flight (chưa được delete), SQS sẽ không deliver message #2 của cùng group đó cho bất kỳ consumer nào — đảm bảo không có hai consumer xử lý song song cùng một group.

Đây chính là lý do FIFO Queue có throughput thấp hơn nhiều so với Standard Queue. Mỗi Message Group ID thực chất là một bottleneck có chủ đích — message bị buộc vào một partition duy nhất thay vì được phân tán tự do. Càng ít Message Group ID, càng ít partition được sử dụng, throughput càng thấp. Ngược lại, nếu bạn dùng nhiều Message Group ID khác nhau (ví dụ: mỗi order một group), SQS có thể xử lý song song nhiều group trên nhiều partition — đây là cách để scale FIFO Queue hiệu quả.

Đánh đổi là rõ ràng:

Tiêu chíStandard QueueFIFO Queue
ThroughputGần như vô hạn300 msg/s (3,000 với high throughput, 30,000 với batching)
OrderingBest-effortStrict per Message Group ID
DeliveryAt-least-once (có thể duplicate)Exactly-once processing
DeduplicationKhông có5-phút rolling window
Tên queueTự doPhải kết thúc bằng .fifo

FIFO Queue còn có cơ chế deduplication tích hợp: mỗi message có thể kèm theo MessageDeduplicationId. Trong vòng 5 phút (non-configurable), nếu SQS nhận được message có cùng dedup ID, nó sẽ bỏ qua message trùng nhưng vẫn trả về response thành công. Bạn có thể tự cung cấp dedup ID, hoặc để SQS tính hash SHA-256 từ message body — nhưng best practice là tự cung cấp để kiểm soát rõ ràng logic dedup.


3. Message Lifecycle

Hiểu vòng đời của một message trong SQS là nền tảng để hiểu các concept phía sau như visibility timeout hay Dead Letter Queue.

Một message đi qua các giai đoạn sau:

Bước 1 — SendMessage: Producer gửi message. SQS replicate đồng bộ lên nhiều node, trả về MessageId khi đủ quorum.

Bước 2 — Stored: Message nằm trong queue ở trạng thái visible — sẵn sàng để bất kỳ consumer nào poll.

Bước 3 — ReceiveMessage: Consumer poll message. SQS trả về message kèm một receipt handle — đây là “vé” mà consumer cần giữ để thao tác tiếp (xoá hoặc gia hạn timeout).

Bước 4 — In-flight: Message chuyển sang trạng thái invisible — các consumer khác không nhìn thấy message này nữa. Đây chính là visibility timeout đang hoạt động.

Bước 5 — Kết thúc: Có hai kịch bản:

Mỗi lần message được receive mà không được delete, SQS tăng receive count (số lần nhận) của message lên 1. Con số này rất quan trọng — nó quyết định khi nào message bị đẩy vào Dead Letter Queue, điều mà chúng ta sẽ nói ở phần 6.


4. Visibility Timeout

Visibility timeout là khoảng thời gian mà một message trở nên “vô hình” đối với các consumer khác sau khi được receive. Mục đích là ngăn nhiều consumer cùng xử lý một message — tránh duplicate processing.

Hãy hình dung như thế này: bạn đi thư viện, lấy một cuốn sách khỏi kệ để đọc. Trong lúc bạn đọc (visibility timeout), không ai khác thấy cuốn sách đó trên kệ. Nếu bạn đọc xong và trả lại (delete message), cuốn sách biến mất khỏi hệ thống. Nhưng nếu bạn giữ quá lâu mà không trả — thư viện sẽ đặt lại cuốn sách lên kệ (message visible trở lại) để người khác có thể lấy.

Cấu hình

Khi nào visibility timeout quá ngắn?

Consumer chưa xử lý xong, nhưng timeout đã hết → message visible trở lại → consumer khác poll và xử lý → duplicate processing. Đây là lý do phổ biến nhất gây ra duplicate trong SQS.

Khi nào visibility timeout quá dài?

Consumer crash giữa chừng → message bị “kẹt” ở trạng thái invisible → phải chờ hết timeout mới retry được → tăng latency cho message đó.

ChangeMessageVisibility

Nếu consumer nhận ra mình cần thêm thời gian để xử lý, nó có thể gọi API ChangeMessageVisibility để gia hạn timeout mà không cần release message. Điều này rất hữu ích cho các task có thời gian xử lý không đồng đều — ví dụ process video, resize ảnh, hoặc gọi external API chậm.

Best practice: đặt visibility timeout bằng khoảng 6 lần thời gian xử lý trung bình. Ví dụ: consumer mất trung bình 10 giây để xử lý → set timeout 60 giây. Điều này cho phép đủ buffer cho trường hợp chậm bất thường, nhưng không quá lâu nếu consumer thật sự crash.


5. Long Polling vs Short Polling

Khi consumer gọi ReceiveMessage, có hai cách SQS trả về kết quả: short pollinglong polling.

Short polling — SQS trả về kết quả ngay lập tức, kể cả khi không có message nào. Nếu queue đang trống, bạn nhận response rỗng. Consumer phải poll liên tục theo interval để kiểm tra message mới.

Long polling — SQS chờ trong khoảng thời gian WaitTimeSeconds (tối đa 20 giây) cho đến khi có message, rồi mới trả về. Nếu hết thời gian chờ mà vẫn không có message, nó mới trả về response rỗng.

Nếu bạn nghĩ long polling là vô ích — “chỉ cần poll liên tục thì trước sau cũng nhận được message thôi” — bạn đã sai. Long polling mang lại ba lợi ích quan trọng:

5.1. Giảm chi phí API

SQS tính phí theo request, không phải theo số lượng hay dung lượng message. Mỗi lần gọi ReceiveMessage đều tốn tiền — kể cả khi response rỗng. Với short polling, nếu queue trống mà consumer poll mỗi 1 giây → 86,400 requests/ngày chỉ để nhận… không gì cả. Với long polling (WaitTimeSeconds=20), con số giảm xuống còn 4,320 requests/ngày — giảm 95% chi phí cho empty responses.

5.2. Giảm latency

Với short polling, nếu message đến ngay sau khi consumer vừa poll xong → consumer phải chờ đến lần poll tiếp theo mới nhận được. Khoảng delay này phụ thuộc vào polling interval.

Với long polling, consumer đang “chờ sẵn” ở phía SQS. Khi message đến, nó được delivery thẳng đến consumer ngay lập tức — không cần chờ lần poll tiếp theo.

5.3. Tránh tín hiệu giả “queue empty”

Đây là lợi ích ít người biết nhưng cực kỳ quan trọng, liên quan trực tiếp đến kiến trúc phân tán mà chúng ta đã nói ở phần 2.

Nhớ lại: SQS lưu message trên nhiều storage node phân tán. Với short polling, SQS chỉ query một vài node ngẫu nhiên và trả về kết quả. Nếu message nằm trên node mà short polling không hit tới → bạn nhận được response “queue empty” — nhưng thực tế message vẫn đang nằm đó, chỉ là ở node khác.

Với long polling, SQS query toàn bộ các node trước khi trả về kết quả. Điều này loại bỏ hoàn toàn tín hiệu giả “queue empty”.

Tiêu chíShort PollingLong Polling
WaitTimeSeconds0 (mặc định)1–20 giây
Empty responsesRất nhiềuRất ít
Chi phíCao (nhiều requests)Thấp hơn đáng kể
LatencyCao (phụ thuộc poll interval)Thấp (delivery ngay khi có message)
Coverage các SQS nodesMột vài node ngẫu nhiênToàn bộ nodes

Best practice: gần như luôn luôn nên dùng long polling. Set ReceiveMessageWaitTimeSeconds=20 ở queue level để enable globally cho toàn bộ consumer.


6. Dead Letter Queue (DLQ)

Có những message “hư hỏng” — dữ liệu sai format, consumer có bug, external dependency down. Message được receive, xử lý thất bại, quay lại queue, được receive lại, thất bại lại… lặp đi lặp lại mãi mãi, tiêu tốn processing resources mà không bao giờ thành công. Đây gọi là poison message.

Dead Letter Queue (DLQ) là giải pháp: một SQS queue riêng biệt, nơi các poison message bị “đày” đến sau khi thất bại quá nhiều lần.

Cách hoạt động

Khi cấu hình DLQ, bạn set maxReceiveCount — số lần tối đa một message được receive trước khi bị chuyển sang DLQ. Ví dụ maxReceiveCount=3:

DLQ bản chất cũng chỉ là một SQS queue bình thường — bạn tạo một queue, rồi cấu hình nó là DLQ target cho queue gốc.

Xử lý message trong DLQ

Message nằm trong DLQ thường được dùng để:

Lưu ý quan trọng về retention

Thời gian retention của message trong DLQ được tính từ lúc message lần đầu tiên được gửi vào queue gốc, không phải từ lúc nó vào DLQ. Ví dụ: retention là 14 ngày, message nằm trong queue gốc 13 ngày mới bị đẩy sang DLQ → nó chỉ còn 1 ngày trong DLQ trước khi bị xoá tự động.

Best practice: luôn set retention của DLQ bằng hoặc dài hơn retention của queue gốc để có đủ thời gian debug và reprocess.


7. Redrive Allow Policy

Mặc định, bất kỳ queue nào cũng có thể cấu hình sử dụng bất kỳ queue khác làm DLQ. Điều này tạo ra vấn đề governance — bạn không muốn queue của team khác tự ý đẩy failed messages vào queue của bạn.

Redrive Allow Policy kiểm soát queue nào được phép sử dụng queue này làm DLQ. Có ba chế độ:

allowAll (mặc định) — bất kỳ queue nào trong cùng account và region đều có thể dùng queue này làm DLQ.

denyAll — không queue nào được phép dùng queue này làm DLQ.

byQueue — chỉ các queue được chỉ định rõ mới được phép.

{ "redrivePermission": "byQueue", "sourceQueueArns": [ "arn:aws:sqs:ap-southeast-1:123456789:order-processing-queue", "arn:aws:sqs:ap-southeast-1:123456789:payment-queue" ] }

Best practice: trong production, luôn dùng byQueue để kiểm soát rõ ràng queue nào được redirect failed messages đến DLQ của bạn.


8. SQS với Auto Scaling Group (ASG)

Một trong những patterns mạnh mẽ nhất của SQS là kết hợp với Auto Scaling Group (ASG) — tự động scale số lượng EC2 consumer instances dựa trên độ sâu của queue (queue depth).

Kiến trúc

Luồng hoạt động:

  1. Message liên tục được gửi vào SQS queue.
  2. CloudWatch monitor metric ApproximateNumberOfMessagesVisible — số message đang chờ trong queue.
  3. Khi metric vượt ngưỡng (ví dụ > 1,000 messages), CloudWatch Alarm chuyển sang trạng thái ALARM.
  4. Alarm trigger ASG scaling policy → ASG launch thêm EC2 instances.
  5. Các instance mới bắt đầu poll SQS, xử lý message → queue depth giảm.
  6. Khi queue depth xuống dưới ngưỡng → alarm về OK → ASG scale in, terminate bớt instances.

Tại sao dùng queue depth thay vì CPU?

Với hầu hết các workload queue-based, CPU không phản ánh đúng workload thực tế. Consumer có thể đang I/O bound (chờ database, chờ external API) — CPU thấp nhưng hàng ngàn message đang xếp hàng chờ. Scale theo CPU sẽ không trigger, dù hệ thống đang quá tải.

Queue depth phản ánh trực tiếp backlog thực tế — bao nhiêu công việc đang chờ xử lý. Đây là metric chính xác hơn cho quyết định scale.

Custom metric: backlog per instance

Thay vì dùng raw queue depth, bạn có thể tạo custom metric chính xác hơn:

backlog_per_instance = ApproximateNumberOfMessages / current_instance_count

Metric này cho biết mỗi instance đang “gánh” bao nhiêu message. Ví dụ: 10,000 messages với 5 instances → mỗi instance gánh 2,000 messages. Scale khi backlog_per_instance vượt ngưỡng cho phép sẽ đưa ra quyết định tỷ lệ thuận hơn so với raw queue depth.


9. SQS làm buffer cho database writes

Đây là pattern mà chúng ta đã nhắc đến ở phần mở đầu — dùng SQS như một bộ giảm xóc giữa application tier và database, giải quyết bài toán database bị nghẽn khi traffic tăng đột biến.

Kiến trúc

Luồng hoạt động:

  1. Request từ client đến EC2 instances (nhóm Enqueue). Các instance này validate request, tạo message, và gọi SendMessage đẩy vào SQS.
  2. SQS queue hấp thụ toàn bộ traffic — bất kể đột biến hay đều đặn. SQS scale gần như vô hạn, nên không bao giờ bị nghẽn ở tầng này.
  3. EC2 instances (nhóm Dequeue) poll message từ SQS bằng ReceiveMessages, rồi insert vào database với tốc độ mà database chịu được.
  4. Cả hai nhóm EC2 đều có Auto Scaling độc lập — nhóm Enqueue scale theo request traffic, nhóm Dequeue scale theo queue depth (pattern ở phần 8).

Tại sao pattern này hiệu quả?

SQS đóng vai trò decoupling — tách biệt tầng nhận request và tầng ghi database. Hai tầng scale độc lập với nhau:

Trade-off

Writes trở thành eventually consistent — có độ trễ giữa lúc request đến và lúc dữ liệu thực sự được ghi vào database. Điều này chấp nhận được cho nhiều use case: analytics, logging, notifications, order processing. Nhưng không phù hợp cho các trường hợp cần xác nhận đồng bộ rằng dữ liệu đã được ghi — ví dụ kiểm tra số dư tài khoản trước khi cho phép giao dịch.


10. SQS với Lambda

Ngoài EC2, AWS Lambda là consumer phổ biến nhất cho SQS. Khi cấu hình Lambda trigger từ SQS, AWS tự động quản lý việc polling và scaling.

Cách Lambda poll SQS

Khi bạn tạo SQS event source mapping cho Lambda, AWS tạo một thread pool (mặc định 5 threads). Mỗi thread độc lập long-poll SQS và lấy message theo batch (tối đa 10 messages/batch). Hệ thống tự động tăng giảm số threads dựa trên lượng message trong queue và concurrency limit.

Failure scenario cần lưu ý

Nếu Lambda reserved concurrency bị cạn kiệt (ví dụ set reserved concurrency = 10, nhưng đã có 10 Lambda instances đang chạy), các threads còn lại sẽ đẩy batch message trở lại queue. Hành động này tăng receive count của mỗi message lên 1.

Hệ quả: nếu maxReceiveCount của DLQ thấp (ví dụ 3), message có thể bị đẩy vào DLQ sớm hơn dự kiến — không phải vì xử lý thất bại, mà vì concurrency không đủ. Đây là một gotcha phổ biến khi dùng Lambda với SQS.

Best practice: khi dùng Lambda với SQS, set maxReceiveCount đủ cao để account cho cả trường hợp throttling, không chỉ processing failure. Và monitor metric NumberOfMessagesReceived so với NumberOfMessagesDeleted — nếu chênh lệch lớn, có khả năng Lambda đang bị throttle.


11. SNS + SQS: Fan-Out Pattern

Các pattern trước (ASG, buffer, Lambda) đều xử lý một luồng duy nhất: producer gửi message → một nhóm consumer xử lý. Nhưng trong thực tế, một sự kiện thường cần được xử lý bởi nhiều service độc lập cùng lúc.

Ví dụ: khi một đơn hàng được tạo, hệ thống cần đồng thời kiểm tra gian lận (Fraud Service), xử lý vận chuyển (Shipping Service), gửi email xác nhận (Notification Service), và cập nhật analytics. Nếu Buying Service gửi message trực tiếp đến từng service, bạn tạo ra tight coupling — mỗi khi thêm một service mới, phải sửa code ở Buying Service.

Fan-out pattern giải quyết vấn đề này bằng cách kết hợp SNS (Simple Notification Service) — một dịch vụ pub/sub (publish-subscribe) cho phép gửi một message đến nhiều subscriber cùng lúc — với SQS để đảm bảo mỗi subscriber nhận và xử lý message một cách đáng tin cậy.

Kiến trúc

Luồng hoạt động:

  1. Buying Service gửi một message duy nhất đến SNS Topic — chỉ cần publish một lần.
  2. SNS tự động fan-out — sao chép message và đẩy đến tất cả SQS queues đã subscribe vào topic đó.
  3. Mỗi SQS queue hoạt động hoàn toàn độc lập — Fraud Service poll từ queue riêng, Shipping Service poll từ queue riêng. Một service chậm hoặc lỗi không ảnh hưởng đến các service khác.

Tại sao kết hợp SNS + SQS thay vì chỉ dùng SNS?

SNS đơn thuần là push-based — khi SNS gửi message đến subscriber, nếu subscriber đang down hoặc không thể xử lý tại thời điểm đó, message sẽ bị mất. SNS không lưu trữ message.

Kết hợp SQS ở phía sau mỗi subscriber giải quyết hoàn toàn vấn đề này:

Mở rộng subscriber

Đây là lợi ích mạnh nhất của fan-out: thêm subscriber mới mà không cần thay đổi producer. Khi cần thêm Analytics Service để theo dõi đơn hàng, bạn chỉ cần tạo một SQS queue mới, subscribe vào cùng SNS topic — xong. Buying Service không biết và không cần biết có bao nhiêu subscriber đang lắng nghe.

Đây chính là Open-Closed Principle áp dụng ở tầng infrastructure — hệ thống mở cho mở rộng (thêm subscriber) nhưng đóng cho thay đổi (không sửa producer).

Cấu hình quan trọng: SQS Access Policy

Để SNS có thể gửi message vào SQS queue, queue cần có resource-based access policy cho phép SNS service principal thực hiện action sqs:SendMessage. Nếu thiếu policy này, SNS sẽ không thể ghi vào queue và message sẽ bị drop âm thầm.

Best practice: luôn restrict SQS access policy bằng condition aws:SourceArn trỏ đến SNS topic cụ thể — tránh cho phép mọi SNS topic ghi vào queue. Điều này đặc biệt quan trọng trong môi trường multi-team, nơi nhiều SNS topic tồn tại trong cùng một AWS account.

Cross-Region Delivery

SNS hỗ trợ cross-region delivery — một SNS topic ở Region A có thể fan-out message đến SQS queues ở Region B hoặc Region C. Điều này hữu ích cho các kiến trúc multi-region, nơi bạn muốn replicate events sang các region khác để xử lý locally, giảm latency cho end user.

Ứng dụng thực tế: S3 Events Fan-Out

Một use case phổ biến của fan-out pattern là xử lý S3 event notifications. S3 có một giới hạn quan trọng: với cùng một tổ hợp event type (ví dụ s3:ObjectCreated:*) và prefix (ví dụ images/), bạn chỉ được tạo một S3 Event notification rule duy nhất.

Nghĩa là nếu bạn muốn ba service khác nhau cùng react khi một file được upload lên images/ — bạn không thể tạo ba S3 event rules riêng biệt.

Giải pháp: cấu hình S3 gửi event notification đến một SNS Topic duy nhất. SNS sau đó fan-out đến nhiều SQS queues và/hoặc Lambda functions. Mỗi downstream service nhận bản sao riêng của event và xử lý độc lập — một service tạo thumbnail, service khác extract metadata, service thứ ba index vào search engine.


Tổng kết

SQS trông đơn giản từ bên ngoài — gửi message, nhận message, xoá message. Nhưng bên dưới là một hệ thống phân tán được thiết kế cẩn thận với nhiều trade-off có chủ đích:

SQS là lựa chọn đúng khi bạn cần một task queue đơn giản, fully managed, scale vô hạn để decouple các thành phần trong hệ thống. Nếu bạn cần event streaming với replay và retention dài hạn → Kafka. Nếu cần fan-out một event đến nhiều subscriber → kết hợp SNS + SQS như đã thảo luận ở phần 11. Nếu cần routing phức tạp dựa trên event content → EventBridge. Mỗi công cụ có vị trí riêng — SQS giỏi nhất ở sự đơn giản và đáng tin cậy.

Liên quan