Thực hành: dựng pipeline fanout SNS–SQS–Lambda nhẹ trong tài khoản AWS của khách
Một topic, ba hàng đợi, một Lambda: bài tập vừa Free Tier này dạy đúng những lỗi trùng lặp và mất message mà bạn sẽ gặp khi chạy thật ở site khách.
- 1Hệ thống đặt hàng publishPublish một lần lên topic SNS order-placed, không cần biết ai đang nghe
- 2SNS fanoutMỗi hàng đợi đã đăng ký nhận một bản giống hệt nhau của message
- 3SQS giữ messageinventory-queue và analytics-queue giữ message, mặc định 4 ngày
- 4Lambda kéo theo batchNhánh kho: event source mapping kéo tối đa 10 message; handler phải idempotent
- 5Lỗi quay lại hoặc vào DLQNhánh kho: chỉ message lỗi quay lại; vượt maxReceiveCount thì sang inventory-dlq
Hàng đợi đứng giữa SNS và Lambda để message không mất, còn DLQ giữ lại những message hỏng thay vì để chúng quay vòng mãi.
Đồ hoạ: FDE Times
Tóm tắt nhanh
- Hàng đợi SQS tiêu chuẩn giao message ít nhất một lần, Lambda cũng có thể xử lý trùng, nên code phải idempotent.
- Bật ReportBatchItemFailures để chỉ message lỗi quay lại hàng đợi, và đặt maxReceiveCount để đẩy message hỏng sang DLQ.
- Lambda và hàng đợi phải cùng Region nhưng có thể khác tài khoản, ràng buộc cần chốt sớm khi làm trong AWS của khách.
Hàng đợi tiêu chuẩn của Amazon SQS chỉ hứa giao message “ít nhất một lần”.
Khách không muốn thêm server phải trông. Bộ ba SNS, SQS và Lambda là lời đáp gọn: serverless tính tiền theo thời gian thực thi, bắt đầu khi code chạy và dừng khi code dừng, nên pipeline lúc rảnh gần như không tốn gì.
Bài này dựng lại ví dụ fanout chính thức của AWS (đơn hàng được đặt, tách ra hàng đợi kho và hàng đợi analytics), rồi thêm Lambda và dead-letter queue. Cuối bài, bạn sẽ biết pipeline này hỏng ở đâu và cách chặn từ đầu.
Bạn cần gì trước khi bắt đầu?
Một tài khoản AWS sandbox của riêng bạn, đừng tập trên tài khoản khách. Free Tier đã bao gồm 1.000.000 lượt publish SNS và 1.000.000 request SQS, quá dư cho bài tập.
Chọn một Region và giữ nguyên nó suốt bài. Lambda và hàng đợi SQS bắt buộc phải cùng Region, dù được phép nằm ở hai tài khoản khác nhau. Ghi tên Region vào một file ghi chú ngay bây giờ.
Bước 1: topic SNS là cái loa phát đơn hàng
SNS là dịch vụ pub/sub được quản lý hoàn toàn. Pub/sub là mô hình giao tiếp bất đồng bộ: bên publish không cần biết ai đang nghe, bên subscribe không cần biết ai đang nói. Hệ thống đặt hàng của khách chỉ việc publish một lần.
Trong console SNS, tạo một topic tên order-placed. Kiểm tra: topic hiện trong danh sách, và bạn đã chép ARN của nó vào file ghi chú.
Bước 2: tạo ba hàng đợi chứ không phải hai
Tạo ba hàng đợi loại Standard: inventory-queue, analytics-queue và inventory-dlq. Hàng đợi thứ ba là dead-letter queue (DLQ), tính năng SQS có sẵn. Rất dễ bỏ qua nó khi mọi thứ đang chạy êm, nên đừng bỏ.
Thời gian lưu message mặc định là 4 ngày, chỉnh được từ 60 giây tới 1.209.600 giây (14 ngày). Lời khuyên thực tế: để DLQ ở mức 14 ngày. Message hỏng thường được phát hiện sau một cuối tuần, và bạn muốn nó vẫn còn đó khi có người xem.
Trên inventory-queue, đặt RedrivePolicy trỏ tới inventory-dlq với maxReceiveCount. Đoạn dưới đây là phác thảo để bạn nắm ý, không theo cú pháp của công cụ nào:
# Phác thảo, đã giản lược
inventory-queue:
type: standard
RedrivePolicy:
dead_letter_queue: inventory-dlq
maxReceiveCount: 3
inventory-dlq:
retention_seconds: 1209600
Bài này chỉ gắn Lambda cho nhánh kho, nên chỉ nhánh kho có DLQ. analytics-queue ở đây chưa có consumer. Khi bạn gắn consumer cho nó, hãy tạo thêm analytics-dlq với cùng cấu hình, đừng để một nhánh được bảo vệ còn nhánh kia thì không.
Kiểm tra: trong cấu hình của inventory-queue, mục dead-letter queue hiện tên inventory-dlq.
Bước 3: đăng ký hai hàng đợi vào topic
Ở trang của từng hàng đợi, đăng ký nó vào topic order-placed. Khi nhiều hàng đợi cùng đăng ký một topic, mỗi hàng đợi nhận một bản giống hệt nhau mỗi lần có message được đẩy lên. Kho và analytics vì thế chạy song song, không chờ nhau.
Kiểm tra: từ console SNS, publish một message thử như {"order_id": "A-001", "sku": "SKU-42", "qty": 2}. Sau đó poll cả hai hàng đợi từ console SQS. Nếu chỉ một hàng đợi thấy message, đăng ký của hàng đợi kia chưa thành công.
Vì sao không cho SNS gọi thẳng Lambda? AWS khuyên ghép SNS với SQS để không mất message khi subscriber đang offline. Hàng đợi là lớp đệm: Lambda lỗi hay bị giới hạn thì message vẫn nằm đó chờ.
Bước 4: gắn Lambda bằng event source mapping
Tạo một Lambda inventory-worker cùng Region, rồi thêm trigger SQS trỏ tới inventory-queue. Trigger này là một event source mapping: Lambda tự poll hàng đợi và gọi hàm theo từng batch. Mặc định mỗi batch tối đa 10 message, có thể thêm batch window tối đa 5 phút để gom cho đầy.
Trong cấu hình mapping, bật partial batch response bằng cách đặt ReportBatchItemFailures cho FunctionResponseTypes:
# Phác thảo, đã giản lược
event_source_mapping:
function: inventory-worker
queue: inventory-queue
batch_size: 10
FunctionResponseTypes: [ReportBatchItemFailures]
Kiểm tra: trigger hiện trạng thái đang bật, và log của hàm ghi nhận message thử từ bước 3.
Một batch hỏng một message thì sao?
Hãy đếm thử. Lambda kéo về batch 10 message, message thứ 7 lỗi vì SKU không tồn tại. Một message đang được xử lý vẫn nằm trong hàng đợi, chỉ bị ẩn trong khoảng visibility timeout. Nếu không bật partial batch response, cả batch bị coi là thất bại và quay lại sau visibility timeout.
Lần chạy sau, 9 message tốt bị xử lý lại. Nếu handler cứ nhận message là trừ kho, đơn A-001 bị trừ 2 sản phẩm hai lần. Không có lỗi nào trong log, chỉ có số tồn kho sai.
Bật ReportBatchItemFailures thì chỉ message số 7 quay lại. Với maxReceiveCount: 3, nó thử thêm vài lần rồi bị chuyển sang inventory-dlq. Không có DLQ, các message hỏng cứ quay vòng mãi, tới lúc số message không xử lý được đông hơn cả message hợp lệ trong hàng đợi.
Bước 5: viết handler chịu được trùng lặp
Partial batch response giảm trùng lặp nhưng không xoá được nó. Event source mapping xử lý mỗi event ít nhất một lần và vẫn có thể xử lý trùng. Code của bạn phải idempotent.
Phác thảo Python dưới đây đã giản lược: already_processed, mark_processed và build_partial_response là hàm bạn tự viết, và định dạng trả về cho partial batch response cần đối chiếu với tài liệu Lambda trước khi chạy thật.
# Phác thảo, đã giản lược
def handler(event, context):
failed_ids = []
for record in event["Records"]:
msg_id = record["messageId"]
try:
order = parse_order(record) # tự viết
key = order["order_id"]
if already_processed(key): # tra bảng khoá idempotency
continue
apply_inventory_change(order) # logic nghiệp vụ
mark_processed(key)
except Exception:
failed_ids.append(msg_id)
return build_partial_response(failed_ids) # chỉ báo message lỗi
Chi tiết đáng chú ý là khoá idempotency dùng order_id của nghiệp vụ, không dùng ID của message. Thử hình dung hệ thống đặt hàng của khách tự retry và publish lại đơn A-001 lên topic: hàng đợi sẽ nhận thêm một message mới cho cùng một đơn. Khoá theo order_id vẫn nhận ra đơn đã xử lý, còn khoá theo ID message thì có thể để lọt.
Kiểm tra: publish lại đúng message A-001 hai lần. Số tồn kho chỉ được trừ một lần.
Những lỗi hay gặp nhất
Lỗi thứ nhất là tạo Lambda ở Region khác hàng đợi rồi mất nửa giờ không hiểu sao trigger không hiện. Lỗi thứ hai là quên DLQ, thường chỉ lộ ra khi hàng đợi phình to.
Lỗi thứ ba là tin rằng hàng đợi Standard giao đúng một lần. Nếu nghiệp vụ thật sự cần xử lý đúng một lần, FIFO queue mới cung cấp điều đó, và bạn nên nêu lựa chọn này với khách thay vì tự quyết.
Lỗi cuối cùng nằm ngoài code: không dọn dẹp. Hướng dẫn chính thức của AWS dặn xoá tài nguyên sau khi tập. Trong tài khoản khách, một topic hay hàng đợi bị bỏ quên là thứ đội vận hành của họ sẽ phải hỏi bạn sau này.
Ở site khách, nên hỏi gì trước?
Nếu khách cấp cho đội bạn một tài khoản riêng, tách khỏi tài khoản chạy hệ thống đặt hàng, hãy hỏi về Region ngay trong buổi kickoff. Hàng đợi và Lambda có thể khác tài khoản, nhưng không thể khác Region.
Câu thứ hai nên hỏi là: “nếu một đơn bị xử lý hai lần thì hỏng gì?”. Câu trả lời cho bạn biết cần idempotency chặt tới đâu, hay phải bàn chuyện FIFO.
Khi đọc một JD FDE có nhắc “event-driven” hay “integration”, hãy hỏi lại nhà tuyển dụng xem việc thật có gồm những pipeline kiểu này không; nếu có, bài tập này giúp bạn chuẩn bị đúng hướng.
Trên CV, đừng ghi “biết SQS”. Hãy ghi kết quả đo được, chẳng hạn bạn đã dựng fanout SNS–SQS kèm DLQ và partial batch response, rồi chứng minh bằng test publish trùng rằng tồn kho không bị trừ hai lần. Cách ghi đó cho thấy bạn hiểu chỗ pipeline hay vỡ, chứ không chỉ thuộc tên dịch vụ.
Pipeline này nhẹ đến mức dựng xong trong một buổi chiều. Thứ khách nhớ lâu hơn là message số 7: nó đã nằm yên trong DLQ chờ người xem, thay vì âm thầm trừ kho hai lần.
8 nguồn
- What is Amazon Simple Queue Service?
- Push Notification Service - Amazon Simple Notification Service (SNS) - AWS
- What is Pub/Sub Messaging? - Pub/Sub Messaging Explained - AWS
- Send Fanout Event Notifications
- Using Lambda with Amazon SQS
- Handling errors for an SQS event source in Lambda
- Best practices for implementing partial batch responses
- What Is Serverless Computing? | IBM · 2024-06-10