Thư viện Use-Case Cloud

Pipeline xử lý ảnh serverless trên AWS

Người dùng tải ảnh lên S3, Lambda tạo các bản resize, CloudFront phân phối. Kèm phân tích đánh đổi và những cái bẫy có thật.

Bài toán

Người dùng tải ảnh lên. Ứng dụng cần nhiều phiên bản của mỗi ảnh — thumbnail cho danh sách, bản vừa cho trang chi tiết, bản WebP cho trình duyệt hỗ trợ — và phải phục vụ chúng nhanh cho người xem ở nhiều nơi.

Đây là bài toán thuộc dạng "làm một lần, đọc rất nhiều lần". Tải lên thì thưa và không đều; đọc thì dày và nên được cache. Kiến trúc dưới đây tách hẳn hai phần đó.

Luồng hoạt động

  1. Client PUT ảnh gốc vào bucket nhận ảnh (n1).
  2. S3 phát event notification và gọi hàm resize (n2). S3 có thể gọi Lambda trực tiếp, không cần thành phần trung gian nào.
  3. Hàm đọc ảnh gốc, tạo các bản dẫn xuất, rồi PutObject sang bucket lưu ảnh đã xử lý (n3) — một bucket khác. Lý do ở mục dưới.
  4. CloudFront (n4) đứng trước bucket dẫn xuất, cache tại edge và ký request về origin bằng origin access control (OAC). Bucket không hề mở ra Internet.
  5. Nếu hàm thất bại sau khi Lambda đã thử lại hết, bản ghi invocation được đẩy vào hàng đợi SQS (n5) làm on-failure destination.
  6. Log của hàm chảy vào CloudWatch Logs (n6) mà không cần cấu hình gì.

Vì sao phải dùng hai bucket

Đây là cái bẫy quan trọng nhất của kiến trúc này, và AWS ghi rõ trong tài liệu:

If your notification writes to the same bucket that triggers the notification, it could cause an execution loop.

Nếu hàm resize ghi ảnh dẫn xuất trở lại chính bucket đã kích hoạt nó, mỗi lần ghi lại sinh ra một event mới, event đó lại gọi hàm — hàm tự gọi chính nó, vòng lặp không có điểm dừng tự nhiên. Hoá đơn tăng theo cấp số nhân, và vì mỗi lời gọi đều "thành công" nên không có cảnh báo lỗi nào bật lên.

Tài liệu AWS đưa ra hai cách xử lý: dùng hai bucket, hoặc giới hạn trigger vào một prefix riêng cho ảnh đầu vào. Kiến trúc này chọn hai bucket vì ranh giới tách bạch dễ kiểm tra hơn — một prefix cấu hình sai vẫn có thể nhìn "gần đúng", còn bucket thì không.

Đánh đổi

Mục này mới là phần đáng đọc. Kiến trúc trên không phải lựa chọn duy nhất đúng.

Xử lý bất đồng bộ: nhanh cho người tải lên, nhưng "chưa có ngay"

Người dùng nhận phản hồi ngay khi ảnh gốc lên xong, không phải chờ resize. Đổi lại, ảnh dẫn xuất chưa tồn tại trong khoảng vài trăm ms đến vài giây sau đó. Giao diện phải xử lý trạng thái này — placeholder, hoặc fallback về ảnh gốc — chứ không được giả định thumbnail đã có.

Nếu nghiệp vụ bắt buộc phải có thumbnail ngay khi request tải lên trả về, kiến trúc này sai: khi đó cần resize đồng bộ trong request, và chấp nhận người dùng chờ.

Giới hạn 15 phút của Lambda

Lambda có timeout tối đa 900 giây (15 phút) và không thể tăng. Với ảnh thì thừa sức. Nhưng nếu pipeline sau này mở rộng sang video, con số đó trở thành bức tường cứng — và lúc đó phải chẻ công việc thành nhiều bước có orchestration.

Đây chính là chỗ Cloud Run của Google Cloud khác biệt rõ: timeout tới 60 phút (3.600 giây), gấp bốn lần Lambda. Cùng một bài toán transcode, trên Lambda phải chia bước, trên Cloud Run thường vẫn chạy gọn trong một request.

Bộ nhớ Lambda: cấu hình cao hơn không chắc đắt hơn

Lambda tính tiền theo GB-giây, và CPU được cấp tỉ lệ thuận với bộ nhớ — ở 1.769 MB hàm có sức tính tương đương một vCPU. Nghĩa là tăng bộ nhớ làm đơn giá mỗi millisecond đắt lên, nhưng thường làm thời gian chạy ngắn lại.

Chi phí vì thế không đơn điệu theo bộ nhớ. Cấu hình 1.024 MB có thể rẻ hơn 512 MB cho cùng một tác vụ resize. Đây là thứ phải đo, không phải đoán.

Hàng đợi thất bại: 4 dòng cấu hình đổi lấy khả năng phục hồi

Không có on-failure destination, một ảnh resize thất bại sẽ biến mất — chỉ còn lại một dòng log rồi bị trôi. Có SQS, ảnh thất bại trở thành message có thể kiểm tra và xử lý lại.

Lưu ý kỹ thuật: chỉ standard queue dùng được, FIFO queue không được hỗ trợ làm on-failure destination. Chi phí thực tế gần như bằng không — 1 triệu request đầu mỗi tháng là miễn phí, và một pipeline khoẻ mạnh gần như không đẩy gì vào đây.

CloudWatch Logs: khoản chi phí âm thầm

Log group mặc định không bao giờ hết hạn. Trên một pipeline chạy nhiều năm, đây là khoản tiền tăng đều mà không ai để ý, vì nó không nằm ở dòng nào nổi bật trong hoá đơn. Đặt retention policy ngay từ đầu.

OAC thay cho bucket công khai

Cho bucket dẫn xuất public thì đơn giản hơn, nhưng mất hai thứ: không kiểm soát được ai đọc gì, và mọi request đều tính phí egress của S3 thay vì được cache ở CDN. Dùng OAC, bucket đóng hoàn toàn, CloudFront là đường vào duy nhất.

Ba điều cần biết khi dùng OAC với S3:

  • Object Ownership của bucket phải đặt Bucket owner enforced.
  • HTTPS giữa CloudFront và S3 chỉ được đảm bảo khi signing behavior là always sign requests.
  • Bucket cấu hình dạng website endpoint không dùng được OAC (cũng không dùng được OAI) — phải khai báo như custom origin.

OAI là cơ chế cũ và AWS khuyến nghị không dùng nữa: nó không hỗ trợ SSE-KMS, không hỗ trợ request động (PUT/POST/DELETE), và không hỗ trợ các Region ra mắt sau tháng 1/2023.

Event notification: giao ít nhất một lần, không bảo đảm thứ tự

S3 event notification được thiết kế để giao ít nhất một lần, và không bảo đảm đúng thứ tự phát sinh. Trong trường hợp hiếm, cơ chế retry của S3 có thể tạo event trùng cho cùng một object.

Hệ quả thực tế: hàm resize phải idempotent. Xử lý cùng một ảnh hai lần phải cho kết quả như xử lý một lần. Ghi đè theo key xác định (thay vì append hay đặt tên theo timestamp) là cách đơn giản nhất để đạt điều đó.

Chi phí

Với giả định 10.000 ảnh/tháng, mỗi ảnh 2 MB, sinh 3 bản dẫn xuất, Lambda 1.024 MB chạy ~2 giây, và 50 GB egress qua CDN ở tỉ lệ cache hit 90%, tổng chi phí vào khoảng 6,2 USD/tháng.

Con số này được tính tay từ đơn giá công bố trên các trang pricing của AWS, không phải lấy từ một estimate đã lưu trong AWS Pricing Calculator, và chưa trừ Free Tier. Hãy đọc nó như một con số cỡ độ lớn.

Điều đáng chú ý hơn con số: egress chiếm phần lớn. Lambda và S3 gần như không đáng kể ở quy mô này. Nếu tỉ lệ cache hit tụt từ 90% xuống 60%, tổng chi phí nhích lên nhiều hơn bất kỳ thay đổi nào khác — kể cả tăng gấp đôi số ảnh.

Khi nào không nên dùng kiến trúc này

  • Cần ảnh dẫn xuất ngay lập tức khi request tải lên trả về → resize đồng bộ.
  • Tác vụ vượt 15 phút (video dài) → Cloud Run, ECS, hoặc chẻ bước.
  • Số lượng biến thể rất lớn và khó đoán trước → cân nhắc resize on-the-fly tại edge thay vì sinh trước toàn bộ, để không lưu những biến thể chẳng ai đọc.

Trong trang này