Mở đầu — vì sao bài này quan trọng
Ở những bài trước, bạn đã học từng "mảnh ghép" của thế giới serverless: hàm Lambda/Cloud Functions/Azure Functions (Bài 11), API Gateway (Bài 23), message queue (Bài 24), object storage (Bài 12), NoSQL (Bài 16). Từng dịch vụ đứng riêng thì không có nhiều giá trị. Sức mạnh thực sự nằm ở cách bạn ghép chúng lại thành một kiến trúc hoàn chỉnh — đó chính là nội dung của bài này.
Serverless không chỉ là "chạy code không cần server". Nó là một triết lý thiết kế: bạn trả tiền theo từng lần gọi, hệ thống tự co giãn từ 0 đến hàng nghìn request/giây, và bạn không phải vá lỗi hệ điều hành hay lo về capacity planning. Nhưng đổi lại, bạn phải tư duy khác đi. Không còn một tiến trình chạy liên tục giữ trạng thái trong RAM; thay vào đó là những hàm nhỏ, không trạng thái (stateless), được kích hoạt bởi sự kiện.
Vấn đề là: nếu bạn bê nguyên tư duy monolith hay microservice truyền thống vào serverless, bạn sẽ vấp phải hàng loạt cạm bẫy — cold start, timeout, chi phí ẩn, khó debug. Vì vậy cộng đồng đã đúc kết ra một tập hợp các pattern (mẫu kiến trúc) đã được kiểm chứng. Nắm được các pattern này, bạn có thể lắp ráp một hệ thống production-grade nhanh chóng mà tránh được những sai lầm người đi trước đã trả giá. Đây là kỹ năng phân biệt một người "biết dùng Lambda" với một người "thiết kế được hệ thống serverless".
Khái niệm cốt lõi
Các khối xây dựng (building blocks)
Mọi kiến trúc serverless đều lắp từ vài loại khối cơ bản:
- Function (hàm): đơn vị xử lý logic — AWS Lambda, Google Cloud Functions, Azure Functions. Đây là nơi code của bạn chạy.
- API layer (lớp giao tiếp HTTP): nơi request từ ngoài đi vào — API Gateway, Cloud Endpoints, Azure API Management (APIM).
- Storage & Data (lưu trữ): object storage (S3), NoSQL (DynamoDB), quan hệ (Aurora Serverless) — nơi lưu trạng thái, vì bản thân hàm không giữ trạng thái.
- Event bus / Queue (kênh sự kiện): SQS, SNS, EventBridge, Pub/Sub — cầu nối để các hàm nói chuyện với nhau một cách bất đồng bộ.
- Orchestration (điều phối): Step Functions, Cloud Workflows — để nối nhiều hàm thành một quy trình có logic điều kiện, retry, chờ đợi.
Pattern 1 — Simple Web Backend (API đồng bộ)
Đây là pattern nền tảng: API Gateway → Lambda → Database. Client gọi một endpoint HTTP, API Gateway route request đến hàm tương ứng, hàm xử lý và trả về ngay lập tức (đồng bộ). Phù hợp cho REST API, backend cho mobile app, form đăng ký. Điểm mấu chốt: mỗi request phải hoàn tất trong giới hạn timeout (API Gateway thường là 29–30 giây), nên chỉ dùng cho tác vụ nhanh.
Pattern 2 — Asynchronous Processing (xử lý bất đồng bộ)
Khi tác vụ chậm (xử lý ảnh, xuất PDF, gọi API bên thứ ba), bạn không nên bắt client chờ. Pattern là: API nhận request → đẩy vào Queue (SQS/Pub/Sub) → trả về 202 Accepted ngay → một hàm khác lấy từ queue xử lý sau. Queue đóng vai trò "bộ đệm", làm phẳng đỉnh tải (buffering) và cho phép retry an toàn. Đây là ứng dụng trực tiếp của message queue bạn đã học ở Bài 24, nhưng đặt trong ngữ cảnh serverless.
Pattern 3 — Fan-out / Fan-in
Fan-out: một sự kiện kích hoạt nhiều hàm chạy song song (ví dụ một ảnh upload lên S3 kích hoạt đồng thời hàm tạo thumbnail, hàm quét nội dung, hàm cập nhật database). Thường dùng SNS hoặc EventBridge làm điểm phân phối.
Fan-in: gom kết quả của nhiều luồng song song lại thành một. Ví dụ chạy 100 hàm xử lý 100 phần dữ liệu, rồi tổng hợp. Pattern này cần một cơ chế đếm/đồng bộ, thường dùng DynamoDB hoặc Step Functions Map state.
Pattern 4 — Orchestration vs Choreography
Đây là điểm gây tranh cãi nhất khi thiết kế serverless.
- Orchestration (điều phối tập trung): có một "nhạc trưởng" — như AWS Step Functions — điều khiển thứ tự gọi các hàm, xử lý lỗi, retry, rẽ nhánh. Ưu điểm: nhìn được toàn cảnh luồng, dễ debug. Nhược điểm: có một điểm phụ thuộc trung tâm.
- Choreography (biên đạo phân tán): không có nhạc trưởng; mỗi hàm phát ra sự kiện, hàm khác lắng nghe và phản ứng (qua EventBridge/Pub/Sub). Ưu điểm: rất lỏng lẻo (loosely coupled), dễ mở rộng. Nhược điểm: khó hình dung luồng tổng thể khi hệ thống lớn.
Pattern 5 — Strangler Fig (bóp nghẹt dần monolith)
Khi di chuyển một hệ thống cũ sang serverless, bạn không viết lại từ đầu. Bạn đặt một API Gateway phía trước, route dần từng chức năng sang Lambda mới, phần còn lại vẫn về hệ thống cũ. Theo thời gian, monolith bị "bóp nghẹt" và biến mất. Pattern này giảm rủi ro cực lớn so với "big bang rewrite".
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT xử lý ảnh sản phẩm (fan-out)
Một sàn thương mại điện tử tại TP.HCM (giả định tên "ChợViệt", quy mô ~50.000 sản phẩm mới mỗi tháng) gặp vấn đề: mỗi khi người bán upload ảnh sản phẩm, họ cần tạo 4 kích thước thumbnail, quét nội dung không phù hợp, và trích xuất màu chủ đạo để gợi ý lọc. Trước đây họ chạy một server EC2 luôn bật để làm việc này, tốn khoảng 3,2 triệu đồng/tháng dù đêm khuya gần như không có ảnh nào.
Họ chuyển sang pattern fan-out: người bán upload ảnh trực tiếp lên S3 (dùng presigned URL), sự kiện s3:ObjectCreated bắn vào SNS, SNS fan-out ra 3 Lambda chạy song song (resize, moderation, color-extract). Kết quả: chi phí giảm còn khoảng 900 nghìn đồng/tháng (chỉ trả khi có ảnh), và thời gian xử lý một lô ảnh giảm từ 8 giây tuần tự xuống ~2,5 giây nhờ chạy song song.
Bài học: khi các tác vụ độc lập với nhau, đừng chạy tuần tự trong một hàm. Tách ra và fan-out — vừa nhanh hơn, vừa dễ bảo trì (sửa hàm moderation không đụng đến hàm resize).
Ví dụ 2 — Fintech xử lý đăng ký khoản vay (orchestration bằng Step Functions)
Một startup fintech ở Singapore phục vụ thị trường Đông Nam Á cần quy trình duyệt hồ sơ vay tiêu dùng: (1) xác thực CMND/CCCD qua OCR, (2) gọi API chấm điểm tín dụng bên thứ ba, (3) kiểm tra danh sách đen, (4) nếu điểm biên thì chuyển cho nhân viên duyệt tay và chờ tối đa 24 giờ, (5) gửi email kết quả.
Ban đầu họ nhồi tất cả vào một Lambda dài, nhưng gặp ngay hai vấn đề: Lambda có timeout tối đa 15 phút (không thể chờ 24h duyệt tay), và khi API tín dụng lỗi thì cả luồng hỏng, phải làm lại từ đầu.
Họ chuyển sang AWS Step Functions. Mỗi bước là một state riêng, có retry policy độc lập (API tín dụng lỗi thì tự thử lại 3 lần). Bước duyệt tay dùng "callback pattern" — Step Functions tạm dừng, phát token, và chờ đến khi nhân viên bấm duyệt (có thể sau nhiều giờ) mới đi tiếp. Nhờ vậy tỷ lệ hồ sơ xử lý thành công tăng từ 82% lên 97%, và đội ngũ nhìn được đúng bước nào đang tắc trên giao diện trực quan của Step Functions.
Bài học: quy trình nghiệp vụ nhiều bước, có chờ đợi và cần độ tin cậy cao, thì đừng viết một hàm khổng lồ. Hãy dùng orchestration — nó lo giúp bạn trạng thái, retry và cả việc chờ con người.
Ví dụ 3 — Báo điện tử migrate bằng Strangler Fig
Một tòa soạn báo điện tử tại Hà Nội có backend PHP monolith 8 năm tuổi, khó bảo trì. Họ không dám viết lại toàn bộ. Thay vào đó, họ đặt API Gateway phía trước. Chức năng mới — API đề xuất bài liên quan — được viết bằng Lambda + DynamoDB. API Gateway route /api/recommend/* sang Lambda, còn tất cả đường dẫn khác vẫn về server PHP cũ.
Sau 6 tháng, họ lần lượt bóc thêm chức năng đếm lượt xem, gửi thông báo đẩy, và tìm kiếm sang serverless. Monolith co lại dần. Không có "ngày D" đáng sợ nào, mỗi lần chuyển một mảnh nhỏ đều có thể rollback dễ dàng chỉ bằng đổi route.
Bài học: serverless là công cụ migrate tuyệt vời. Đừng "đập đi xây lại", hãy bóp nghẹt dần — rủi ro thấp, giá trị đến sớm.
Hướng dẫn từng bước
Giả sử bạn xây dựng một API xử lý đơn hàng bất đồng bộ (kết hợp Pattern 1 + 2). Đây là các bước tư duy và triển khai:
- Vẽ luồng sự kiện trước, viết code sau. Xác định: request vào đâu, ai lưu, ai xử lý, kết quả trả về client bằng cách nào. Với đơn hàng: client POST → API Gateway → Lambda "nhận đơn" ghi vào DynamoDB với trạng thái
PENDING→ đẩy message vào SQS → trả về 202 kèmorderId.
- Thiết kế hàm stateless. Hàm "nhận đơn" không giữ gì trong RAM giữa các lần gọi. Mọi trạng thái nằm ở DynamoDB. Điều này cho phép chạy song song vô số instance.
- Cấu hình trigger đúng. Gắn SQS làm event source cho Lambda "xử lý đơn". AWS tự động poll queue và gọi hàm theo lô (batch). Đặt
batchSizehợp lý (ví dụ 10) để cân bằng thông lượng và chi phí.
- Đảm bảo idempotency (chạy lại không sinh lỗi kép). Vì message có thể được giao nhiều lần ("at-least-once"), hàm xử lý phải kiểm tra: đơn này đã xử lý chưa? Dùng
orderIdlàm khóa chống trùng trong DynamoDB.
- Thêm Dead Letter Queue (DLQ). Cấu hình để message thất bại sau N lần retry sẽ rơi vào một queue riêng, thay vì mất hoặc lặp vô hạn. Đây là "lưới an toàn" bắt buộc.
- Đo cold start và cấu hình bộ nhớ. Chạy thử, xem CloudWatch. Nếu cold start ảnh hưởng trải nghiệm, cân nhắc Provisioned Concurrency cho các hàm nhạy với độ trễ.
- Đặt IAM quyền tối thiểu (least privilege). Mỗi hàm chỉ được cấp đúng quyền nó cần (ví dụ hàm nhận đơn chỉ được ghi vào một bảng, gửi vào một queue). Đây là nguyên tắc bạn đã học ở Bài 26.
Lỗi thường gặp & mẹo
- Cold start bị bỏ qua đến khi lên production. Hàm khởi tạo lần đầu (hoặc sau khi "ngủ") mất vài trăm ms đến vài giây. Mẹo: giữ package nhỏ, khởi tạo kết nối database ngoài handler, và dùng Provisioned Concurrency cho endpoint quan trọng.
- Coi database quan hệ như "vô hạn kết nối". Lambda co giãn đến hàng nghìn instance, mỗi instance mở một kết nối → làm sập RDS. Mẹo: dùng RDS Proxy để pool kết nối, hoặc chọn DynamoDB/Aurora Serverless vốn hợp với mô hình này hơn.
- Quên tính idempotency. Message queue thường giao "ít nhất một lần", nghĩa là hàm có thể chạy hai lần cho cùng một sự kiện. Nếu không chống trùng, khách hàng bị trừ tiền hai lần. Luôn thiết kế idempotent.
- Lambda gọi Lambda đồng bộ theo chuỗi dài. Hàm A gọi B, B gọi C và chờ nhau → bạn trả tiền cho cả thời gian chờ (gọi là "double billing"), lại dễ vượt timeout. Mẹo: nối bằng sự kiện/queue bất đồng bộ, hoặc dùng Step Functions.
- Nhét quá nhiều việc vào một hàm ("Lambda-lith"). Một hàm 2000 dòng làm đủ thứ thì khó test, khó scale riêng phần. Chia theo trách nhiệm, nhưng cũng đừng chia quá vụn thành hàng trăm hàm siêu nhỏ khó quản lý — hãy cân bằng.
- Không quan sát được hệ thống. Serverless phân tán nên khó debug. Bật distributed tracing (X-Ray — Bài 35) và log có cấu trúc ngay từ đầu, đừng để đến khi có sự cố mới thêm.
- Mẹo chi phí: serverless rẻ khi tải thấp/không đều, nhưng khi tải cao và ổn định liên tục 24/7, một container chạy thường trực có thể rẻ hơn. Hãy tính toán trước, đừng mặc định serverless luôn rẻ nhất.
Bài tập thực hành
- Vẽ kiến trúc: Cho yêu cầu "người dùng upload video, hệ thống nén thành 3 độ phân giải và gửi email thông báo khi xong". Hãy vẽ luồng serverless dùng S3, SNS/SQS, Lambda và xác định đây là pattern nào (gợi ý: fan-out + async).
- Chọn pattern: Với hai tình huống sau, quyết định dùng orchestration hay choreography và giải thích: (a) quy trình hoàn tất đơn hàng gồm trừ kho, tính phí ship, tạo hóa đơn, gửi mail; (b) khi có user mới đăng ký, cần đồng thời gửi mail chào mừng, tạo profile mặc định, ghi analytics.
- Triển khai nhỏ: Dùng AWS SAM hoặc Serverless Framework, tạo một API Gateway + Lambda trả về "Hello" và một Lambda thứ hai được kích hoạt bởi SQS. Ghi lại thời gian cold start lần đầu so với các lần sau.
- Thiết kế idempotency: Viết mã giả (pseudo-code) cho hàm xử lý thanh toán đảm bảo không trừ tiền hai lần khi nhận message trùng. Dùng
transactionIdlàm khóa chống trùng.
Tóm tắt
Serverless không phải là "code không cần server" một cách ngây thơ, mà là nghệ thuật lắp ghép các khối — function, API layer, storage, queue/event bus, và orchestration — theo những pattern đã được kiểm chứng. Bạn đã học năm pattern cốt lõi: simple web backend (API đồng bộ), asynchronous processing (đệm qua queue), fan-out/fan-in (song song hóa), orchestration vs choreography (điều phối tập trung hay biên đạo phân tán), và strangler fig (di chuyển dần khỏi monolith).
Qua ba tình huống thực tế — sàn TMĐT xử lý ảnh, fintech duyệt hồ sơ vay, và tòa soạn báo migrate — bạn thấy rằng chọn đúng pattern quyết định cả chi phí lẫn độ tin cậy. Nguyên tắc xuyên suốt: thiết kế hàm stateless, đảm bảo idempotency, nối các thành phần bằng sự kiện thay vì gọi đồng bộ chuỗi dài, và luôn có DLQ cùng khả năng quan sát.
Ở các bài tiếp theo, bạn sẽ đi sâu vào Event-driven Architecture (Bài 46) — nền tảng tư duy cho choreography — và cách dùng IaC (Bài 47–48) để triển khai những kiến trúc này một cách tự động, lặp lại được. Hãy nhớ: pattern là bản đồ, còn bạn là người lái. Hiểu vì sao mỗi pattern ra đời quan trọng hơn học thuộc tên gọi của chúng.