Lambda (Java 21, SnapStart, cold start, concurrency), API Gateway, Step Functions
Tiếp đi: Lambda. Ở bài trước, event OrderPlaced đã nằm trong queue shop-order-events, nhưng mình vẫn phải nuôi một EC2 chỉ để chạy consumer gửi email, và một EC2 khác chỉ để resize ảnh sản phẩm khi upload lên S3. Hai máy đó rảnh gần như cả ngày. Có cách nào không phải trả tiền server cho mấy việc "chạy khi có việc" như vậy không?
Đây đúng là bài toán mà AWS Lambda sinh ra để giải. Ở bài 22.11 ta đã có hàng đợi và sự kiện; bài này đưa hai việc "chạy khi có việc" của shop-app lên Lambda, rồi mở một cửa HTTP bằng API Gateway, và ghé qua Step Functions để biết khi nào cần nó. Phần so sánh tổng quát EC2 / ECS / EKS / Lambda đã có ở bài 18.5, ở đây ta đi sâu vào riêng Lambda với Java.
1. Bài toán: hai server "ngồi chơi" của shop-app
Hiện tại trên shop-app:
S3 shop-app-assets ──(cron 1 phút)──► EC2 t3.small "image-worker"
quét bucket, resize ảnh mới
CPU trung bình: 2%
SQS shop-order-events ──(poll)──────► EC2 t3.small "email-worker"
đọc OrderPlaced, gửi email
CPU trung bình: 1%
2 × t3.small chạy 24/7 ≈ vài chục USD/tháng + patch OS + alarm + ASG
Ngày sale: ảnh upload tăng 50 lần ──► worker nghẽn, phải scale tay
Công việc thật của hai máy là vài giây mỗi lần có sự kiện. Phần còn lại là chờ.
2. Vấn đề
- Trả tiền cho thời gian chờ. EC2 tính tiền theo giờ chạy, không theo việc làm.
- Vận hành không đáng: patch AMI, SSM, log agent, ASG, alarm cho một process 200 dòng code.
- Scale thủ công: ngày sale phải đoán trước số máy; đoán sai thì nghẽn hoặc lãng phí.
- Polling S3 bằng cron vừa chậm (tối đa 1 phút) vừa tốn request
ListObjects; S3 đã có cơ chế event push (bài 22.6) nhưng không có ai nhận. - Muốn thêm một API nhỏ kiểu "webhook nhận trạng thái thanh toán" cũng phải mở thêm port, thêm ALB rule, thêm instance.
3. Giải pháp: Lambda + API Gateway (+ Step Functions khi cần điều phối)
AWS Lambda là compute serverless: bạn upload code (jar/zip hoặc container image), AWS chạy nó khi có event, tính tiền theo số lần gọi và theo GB-giây (memory × thời gian chạy, làm tròn 1 ms). Không có server để bạn quản lý, scale tự động theo số event đồng thời.
Thuật ngữ đề thi cần nắm:
| Thuật ngữ | Nghĩa | Trên shop-app |
|---|---|---|
| Function | Đơn vị deploy: code + runtime + cấu hình (memory, timeout, env, role) | shop-image-resize, shop-order-notifier |
| Handler | Method được gọi khi có event. Java: class implement RequestHandler<I, O> | com.shop.lambda.ImageResizeHandler::handleRequest |
| Execution role | IAM role mà code chạy với quyền của nó (default credentials chain lấy từ đây) | shop-lambda-role |
| Trigger / event source | Thứ gọi function: S3, SQS, API Gateway, EventBridge, SNS, DynamoDB Streams, Kinesis… | S3 event (ảnh), SQS (đơn hàng), HTTP API |
| Event source mapping | Lambda tự poll nguồn (SQS, Kinesis, DynamoDB Streams) thay bạn, gom batch rồi gọi handler | Lambda poll shop-order-events |
| Synchronous vs asynchronous invoke | Sync: caller chờ kết quả (API Gateway, ALB). Async: Lambda nhận event vào queue nội bộ, retry 2 lần, có thể gửi DLQ / destination (S3, SNS, EventBridge) | S3 event → async; HTTP → sync |
| Cold start | Lần gọi đầu trên một execution environment mới: tải code, khởi tạo JVM, chạy static init / constructor | Java: 1–6 giây nếu không tối ưu |
| SnapStart | Lambda chạy init một lần khi publish version, chụp snapshot (Firecracker microVM), lần gọi sau khôi phục từ snapshot thay vì init lại. Java 11/17/21, miễn phí thêm cho Java | Giảm cold start ~10 lần cho Spring/Java |
| Reserved concurrency | Đặt trần số instance đồng thời cho một function (và dành riêng lượng đó khỏi pool chung của account). Có thể đặt 0 để tắt function. Miễn phí | Giới hạn shop-order-notifier ≤ 50 để không đè RDS |
| Provisioned concurrency | Giữ sẵn N environment đã init, không cold start. Tốn tiền theo giờ dù không gọi | Bật cho API nhận webhook giờ sale |
| Lambda layer | Gói thư viện/dependency tách khỏi code, dùng chung giữa nhiều function | Thường không cần với Java fat jar |
| Lambda@Edge / CloudFront Functions | Chạy code tại edge của CloudFront (xem bài 22.10) | Rewrite URL ảnh |
API Gateway là cửa HTTP được quản lý, đứng trước Lambda (hoặc bất kỳ HTTP backend / AWS service nào). Hai loại hay gặp:
- REST API (v1): đầy đủ tính năng: API key, usage plan, request validation, caching, WAF, private endpoint, canary. Đắt hơn, chậm hơn một chút.
- HTTP API (v2): nhẹ, rẻ hơn nhiều, latency thấp hơn, có JWT authorizer sẵn, CORS sẵn; thiếu usage plan, cache, request validation.
- (WebSocket API cho realtime hai chiều.)
Step Functions là dịch vụ workflow (state machine) nối nhiều Lambda / service theo bước, có retry, branch, parallel, chờ callback, và lưu trạng thái từng bước. Dùng khi logic là "chuỗi bước có nhánh" chứ không phải một function.
4. Code và cấu hình từng bước
Bước 1: project Maven cho shop-image-resize (Java 21)
Lambda Java không dùng Spring Boot để giữ cold start nhỏ: chỉ cần aws-lambda-java-core, aws-lambda-java-events và SDK v2 S3. Đóng gói bằng maven-shade-plugin thành một fat jar.
<!-- pom.xml -->
<properties>
<maven.compiler.release>21</maven.compiler.release>
<awssdk.version>2.29.0</awssdk.version>
</properties>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
<exclusions>
<!-- Bỏ Netty, Apache client: dùng url-connection-client cho nhẹ -->
<exclusion><groupId>software.amazon.awssdk</groupId><artifactId>netty-nio-client</artifactId></exclusion>
<exclusion><groupId>software.amazon.awssdk</groupId><artifactId>apache-client</artifactId></exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
<version>${awssdk.version}</version>
</dependency>
<!-- Để dùng hook SnapStart (beforeCheckpoint / afterRestore) -->
<dependency>
<groupId>io.github.crac</groupId>
<artifactId>org-crac</artifactId>
<version>0.1.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<finalName>shop-image-resize</finalName>
<transformers>
<!-- gộp META-INF/services, bắt buộc cho SDK v2 http client -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Bước 2: handler resize ảnh (S3 event)
package com.shop.lambda;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import org.crac.Core;
import org.crac.Resource;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class ImageResizeHandler implements RequestHandler<S3Event, String>, Resource {
// Khởi tạo NGOÀI handleRequest: chạy một lần mỗi execution environment,
// được SnapStart chụp vào snapshot.
private static final S3Client s3 = S3Client.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();
private static final int WIDTH = Integer.parseInt(
System.getenv().getOrDefault("THUMB_WIDTH", "300"));
public ImageResizeHandler() {
Core.getGlobalContext().register(this); // đăng ký hook CRaC
// Warm-up: gọi một request nhẹ để class loading, TLS handshake xảy ra trước checkpoint
}
@Override
public String handleRequest(S3Event event, Context ctx) {
for (var rec : event.getRecords()) {
String bucket = rec.getS3().getBucket().getName();
String key = URLDecoder.decode(rec.getS3().getObject().getKey(), StandardCharsets.UTF_8);
if (key.startsWith("thumbs/")) continue; // tránh vòng lặp vô hạn!
ctx.getLogger().log("resize " + bucket + "/" + key);
try (InputStream in = s3.getObject(GetObjectRequest.builder().bucket(bucket).key(key).build())) {
BufferedImage src = ImageIO.read(in);
int h = src.getHeight() * WIDTH / src.getWidth();
BufferedImage dst = new BufferedImage(WIDTH, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = dst.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(src, 0, 0, WIDTH, h, null);
g.dispose();
var out = new ByteArrayOutputStream();
ImageIO.write(dst, "jpg", out);
s3.putObject(PutObjectRequest.builder()
.bucket(bucket).key("thumbs/" + key).contentType("image/jpeg").build(),
RequestBody.fromBytes(out.toByteArray()));
} catch (Exception e) {
throw new RuntimeException("resize failed: " + key, e); // ném ra để Lambda retry / DLQ
}
}
return "ok " + event.getRecords().size();
}
@Override public void beforeCheckpoint(org.crac.Context<? extends Resource> c) {
// Đóng kết nối, xóa cache có thời hạn, bỏ random seed… trước khi chụp snapshot
}
@Override public void afterRestore(org.crac.Context<? extends Resource> c) {
// Mở lại kết nối nếu cần. Credential: SDK tự refresh, không cần làm gì.
}
}
Ba điểm cần nhớ với Java Lambda: (1) khởi tạo nặng ở field static / constructor, không trong handleRequest, để tái dùng giữa các lần gọi "warm"; (2) không ghi output vào cùng prefix đang trigger (vòng lặp vô hạn tốn tiền); (3) ném exception để Lambda biết thất bại, đừng nuốt lỗi.
Bước 3: handler nhận HTTP qua API Gateway (shop-order-notifier)
Với HTTP API (v2), payload là APIGatewayV2HTTPEvent; với REST API là APIGatewayProxyRequestEvent. Function này nhận webhook "payment confirmed" rồi đẩy sang SQS shop-order-events; nó cũng có thể được nối thẳng vào SQS làm consumer.
public class OrderNotifierHandler
implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {
private static final SqsClient sqs = SqsClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder()).build();
private static final String QUEUE_URL = System.getenv("ORDER_QUEUE_URL");
private static final ObjectMapper om = new ObjectMapper();
@Override
public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent req, Context ctx) {
try {
var body = om.readTree(req.getBody());
String orderId = body.path("orderId").asText();
if (orderId.isBlank()) return json(400, "{\"error\":\"orderId required\"}");
sqs.sendMessage(b -> b.queueUrl(QUEUE_URL)
.messageBody(om.writeValueAsString(Map.of(
"type", "PaymentConfirmed", "orderId", orderId,
"requestId", ctx.getAwsRequestId()))));
return json(202, "{\"status\":\"queued\",\"orderId\":\"" + orderId + "\"}");
} catch (Exception e) {
ctx.getLogger().log("ERROR " + e);
return json(500, "{\"error\":\"internal\"}");
}
}
private static APIGatewayV2HTTPResponse json(int status, String body) {
return APIGatewayV2HTTPResponse.builder()
.withStatusCode(status)
.withHeaders(Map.of("Content-Type", "application/json"))
.withBody(body).build();
}
}
Nếu muốn nối cùng function này vào SQS làm consumer, implement thêm RequestHandler<SQSEvent, SQSBatchResponse> và trả về danh sách batchItemFailures để chỉ message lỗi mới quay lại queue (partial batch response, bật bằng --function-response-types ReportBatchItemFailures).
Bước 4: IAM role cho Lambda
# trust policy: ai được assume role này? dịch vụ Lambda
cat > lambda-trust.json <<'EOF'
{ "Version": "2012-10-17",
"Statement": [{ "Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole" }] }
EOF
aws iam create-role --role-name shop-lambda-role \
--assume-role-policy-document file://lambda-trust.json \
--tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
# Quyền ghi log CloudWatch (managed policy)
aws iam attach-role-policy --role-name shop-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole --profile shop-lab
# Quyền S3 chỉ đúng bucket + SQS chỉ đúng queue (least privilege)
cat > lambda-inline.json <<EOF
{ "Version": "2012-10-17", "Statement": [
{ "Effect": "Allow", "Action": ["s3:GetObject","s3:PutObject"],
"Resource": "arn:aws:s3:::shop-app-assets-$ACCOUNT_ID/*" },
{ "Effect": "Allow",
"Action": ["sqs:SendMessage","sqs:ReceiveMessage","sqs:DeleteMessage","sqs:GetQueueAttributes"],
"Resource": "arn:aws:sqs:$AWS_REGION:$ACCOUNT_ID:shop-order-events" } ] }
EOF
aws iam put-role-policy --role-name shop-lambda-role --policy-name shop-lambda-access \
--policy-document file://lambda-inline.json --profile shop-lab
Bước 5: deploy bằng CLI
mvn -q package
aws lambda create-function --function-name shop-image-resize \
--runtime java21 --architectures arm64 \
--handler com.shop.lambda.ImageResizeHandler::handleRequest \
--zip-file fileb://target/shop-image-resize.jar \
--role arn:aws:iam::$ACCOUNT_ID:role/shop-lambda-role \
--memory-size 1024 --timeout 30 \
--environment "Variables={THUMB_WIDTH=300}" \
--snap-start ApplyOn=PublishedVersions \
--tags Project=shop-app,Env=lab --profile shop-lab
arm64 (Graviton) rẻ hơn x86 khoảng 20% và Java chạy tốt. memory-size vừa là RAM vừa quyết định CPU được cấp (1769 MB ≈ 1 vCPU): Java init nhanh hơn rõ khi tăng memory lên 1–2 GB, nên "tăng memory để rẻ hơn" là chuyện thật, vì thời gian chạy giảm nhiều hơn mức tăng giá.
Bước 6: API Gateway HTTP API → Lambda
API_ID=$(aws apigatewayv2 create-api --name shop-api --protocol-type HTTP \
--target arn:aws:lambda:$AWS_REGION:$ACCOUNT_ID:function:shop-order-notifier \
--tags Project=shop-app,Env=lab --query ApiId --output text --profile shop-lab)
# Cho phép API Gateway gọi function (resource-based policy trên Lambda)
aws lambda add-permission --function-name shop-order-notifier \
--statement-id apigw --action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:$AWS_REGION:$ACCOUNT_ID:$API_ID/*" --profile shop-lab
--target tạo nhanh "quick create": route $default + integration Lambda proxy + stage $default auto-deploy. Trong lab ta sẽ tạo route riêng POST /webhooks/payment.
Bước 7: Spring Boot gọi Lambda? Thường không cần
Trong shop-app, service chính không gọi Lambda trực tiếp; nó chỉ upload ảnh lên S3 và gửi message lên SQS, Lambda tự chạy theo sự kiện. Đó là điểm hay: ghép lỏng qua event. Nếu thật sự cần gọi đồng bộ, dùng LambdaClient.invoke() của SDK v2, nhưng hãy tự hỏi vì sao không gọi HTTP qua API Gateway hoặc đẩy event.
5. Cách hoạt động
Luồng resize ảnh (async invoke):
shop-app ──PutObject──► S3 shop-app-assets ──event──► Lambda internal queue
(prefix products/) │
▼
┌─────────────────────────────┐
│ Execution environment │
│ [INIT] JVM + static init │ ◄── cold start (lần đầu)
│ [INVOKE] handleRequest() │ ◄── warm: chỉ bước này
│ (freeze giữa các lần gọi) │
└─────────────────────────────┘
│ PutObject thumbs/…
▼
S3 thumbs/products/…
Lỗi: retry 2 lần (async) ──► on-failure destination / DLQ (SQS)
Luồng webhook (sync invoke):
PSP ──POST /webhooks/payment──► API Gateway HTTP API ──proxy──► Lambda shop-order-notifier
(TLS, throttle, JWT authorizer) │ SendMessage
◄──── 202 {"status":"queued"} ◄────────────────────────── ▼
SQS shop-order-events
│ event source mapping (Lambda poll)
▼
Lambda consumer / shop-app worker
Cold start với SnapStart:
Không SnapStart: [tải jar][start JVM][static init 2-5s][handleRequest]
SnapStart: publish version ──► chạy init một lần ──► snapshot (đĩa, mã hóa)
mỗi environment mới: [restore snapshot ~200ms][handleRequest]
Concurrency:
10 event đến cùng lúc ──► 10 environment chạy song song (mỗi environment xử lý 1 event tại một thời điểm)
Account mặc định ~1000 concurrent/region ──► reserved concurrency chia phần, provisioned giữ ấm
6. So sánh
API Gateway REST API vs HTTP API vs ALB (đề thi rất hay hỏi):
| REST API (v1) | HTTP API (v2) | ALB → Lambda | |
|---|---|---|---|
| Giá / 1 triệu request | Cao nhất | Khoảng 1/3 REST | Theo giờ ALB + LCU, hợp khi đã có ALB |
| Latency thêm | Cao hơn | Thấp hơn | Thấp |
| Auth | IAM, Cognito, Lambda authorizer, API key | IAM, JWT (OIDC/Cognito), Lambda authorizer | Cognito/OIDC trên listener rule |
| Usage plan, API key, throttling per client | Có | Không | Không |
| Cache response | Có | Không | Không |
| Request validation, mapping template (VTL) | Có | Không (chỉ parameter mapping) | Không |
| Private API (trong VPC) | Có | Không (nhưng có VPC link ra private ALB/NLB) | Internal ALB |
| WAF | Có | Không trực tiếp (qua CloudFront) | Có |
| Payload tối đa | 10 MB | 10 MB | 1 MB |
| Timeout backend | 29 s (có thể xin tăng) | 30 s | Theo Lambda (≤ 15 phút) nhưng idle ALB 60 s |
Reserved vs provisioned concurrency:
| Reserved concurrency | Provisioned concurrency | |
|---|---|---|
| Mục đích | Giới hạn trần / bảo đảm có phần trong pool account | Loại bỏ cold start |
| Chi phí | Miễn phí | Tốn theo GB-giờ dù không gọi |
| Đặt = 0 | Tắt function (hữu ích khi sự cố) | Không áp dụng |
| Áp lên | Function (mọi version) | Một version hoặc alias cụ thể |
| Kết hợp Auto Scaling | Không | Có (Application Auto Scaling theo lịch hoặc theo utilization) |
Lambda vs Step Functions vs tự viết orchestration:
| Một Lambda to | Step Functions Standard | Step Functions Express | |
|---|---|---|---|
| Hợp với | Một bước, ≤ 15 phút | Workflow dài (tới 1 năm), cần audit từng bước, exactly-once | Workflow ngắn (≤ 5 phút), tần suất cực cao, at-least-once |
| Tính tiền | GB-giây | Theo state transition | Theo số lần chạy + thời gian |
| Retry / catch | Tự code | Khai báo trong ASL (JSON) | Khai báo |
| Chờ người/hệ khác | Không (tốn tiền chờ) | waitForTaskToken, Wait tới 1 năm, không tốn tiền khi chờ | Hạn chế |
7. Khi nào dùng / không dùng
Dùng Lambda khi: xử lý theo sự kiện (S3, SQS, EventBridge, DynamoDB Streams), traffic thưa hoặc rất lồi lõm, chạy ngắn (< 15 phút), team muốn bỏ hẳn việc quản server, glue code giữa các dịch vụ AWS, cron (EventBridge Scheduler → Lambda) thay cho máy chạy crontab.
Không dùng Lambda khi: request đều đặn cao suốt ngày (Fargate/EC2 rẻ hơn khi tải ổn định, xem bài 22.13), cần chạy > 15 phút, cần kết nối DB giữ lâu với pool lớn (mỗi environment một kết nối → RDS quá tải; cần RDS Proxy, xem bài 22.7), latency p99 phải ổn định mà không muốn trả provisioned concurrency, app Spring Boot monolith to muốn "nhét" vào Lambda (được, qua Spring Cloud Function hoặc container image, nhưng cold start và kích thước là vấn đề).
Dùng API Gateway khi: cần cửa HTTP cho Lambda, cần auth/throttle/API key tập trung, public API cho đối tác. Dùng ALB khi đã có ALB cho shop-app và chỉ muốn thêm vài path vào Lambda.
Dùng Step Functions khi: saga đặt hàng nhiều bước (reserve kho → charge → gửi mail, lỗi thì hoàn), xử lý cần chờ phê duyệt, pipeline xử lý file nhiều giai đoạn; nếu đã quen Kafka/outbox ở chương Messaging thì đây là bản "managed" của ý tưởng đó.
Mình đo lần gọi đầu của shop-order-notifier mất 4,8 giây, các lần sau chỉ 40 ms. Cổng thanh toán timeout ở 3 giây nên webhook đầu tiên mỗi sáng đều fail. Ngoài SnapStart thì còn cách nào, và SnapStart có nhược điểm gì không?
Bạn đã đo đúng chỗ. Số 4,8 giây đó gần hết là Init Duration (cột riêng trong dòng REPORT của CloudWatch Logs). Có một "thang" tối ưu, đi từ rẻ đến tốn:
| Mức | Việc làm | Hiệu quả thường thấy | Chi phí |
|---|---|---|---|
| 1 | Giảm dependency: bỏ Spring Boot khỏi function nhỏ, dùng url-connection-client thay Netty/Apache, loại Jackson module không dùng, jar < 10 MB | 4,8 s → ~1,5 s | 0 |
| 2 | Tăng memory lên 1–2 GB (được nhiều CPU hơn cho JIT và class loading); dùng arm64 | ~1,5 s → ~0,9 s; tiền thường không tăng vì chạy nhanh hơn | ≈ 0 |
| 3 | Khởi tạo client, đọc secret, warm Jackson ở static init (được chụp vào snapshot) + Tiered compilation JAVA_TOOL_OPTIONS=-XX:+TieredCompilation -XX:TieredStopAtLevel=1 | Giảm thêm 20–40% | 0 |
| 4 | SnapStart (bật trên published version, gọi qua alias) | ~0,9 s → ~150–300 ms | Java: không tốn thêm; chỉ cần publish version |
| 5 | Provisioned concurrency N environment luôn ấm, có thể scale theo lịch (giờ sale) | Cold start ≈ 0 trong N environment | Tốn tiền theo giờ |
| 6 | GraalVM native image (custom runtime provided.al2023) hoặc Spring Boot 3 AOT | < 100 ms | Build phức tạp, debug khó |
Nhược điểm và bẫy của SnapStart, hay bị hỏi khi phỏng vấn:
- Chỉ áp dụng cho published version, không cho
$LATEST. Nên trigger phải trỏ vào alias (ví dụlive), và mỗi lần deploy phảipublish-versionrồiupdate-alias. Lab bên dưới làm đúng quy trình này. - Uniqueness: mọi thứ sinh ra ở init được "nhân bản" cho tất cả environment. Random seed, UUID tạo sẵn, ID tạm, token có hạn, kết nối TCP: phải tạo lại trong
afterRestorehoặc trong handler.java.util.RandomvàSecureRandomcủa Corretto trên Lambda đã được xử lý để re-seed, nhưng cache của bạn thì không. - Kết nối mạng trong snapshot sẽ chết sau restore (server phía kia không biết bạn). SDK client tự reconnect; connection pool JDBC thì nên tạo lazy hoặc validate.
- Không dùng được với provisioned concurrency trên cùng function, không dùng với EFS, và ephemeral storage chỉ 512 MB mặc định.
- Lần publish version chậm hơn (phải chạy init + chụp snapshot), và snapshot ít dùng có thể bị dọn, lần gọi sau khôi phục chậm hơn một chút.
Với webhook 3 giây của bạn: mức 1 + 2 + 4 là đủ và không tốn thêm tiền. Nếu PSP gửi dồn 200 webhook trong 1 giây lúc 0h ngày sale (200 environment mới cùng lúc), cân nhắc bật provisioned concurrency 50 theo lịch chỉ trong 2 giờ đó.
Một cách khác không liên quan Lambda: trả lời 202 ngay, đẩy xử lý nặng về sau. Handler webhook ở trên đã làm vậy, nó chỉ SendMessage rồi trả về. Việc nặng (đọc DB, gửi email) để consumer của shop-order-events lo; consumer đó có cold start 2 giây cũng không ai biết.
Nối Lambda vào SQS shop-order-events làm consumer thì khi tải cao Lambda scale lên hàng trăm instance, mỗi instance mở một kết nối tới RDS shop-db. Mình sợ đè sập database. Xử lý thế nào?
Lo đúng rồi. Đây là bài học đau nhất khi ghép "thứ scale vô hạn" (Lambda) với "thứ scale hữu hạn" (RDS db.t3.micro có khoảng ~80 kết nối). Có bốn cái phanh, dùng kết hợp:
1. Phanh ở phía Lambda: reserved concurrency. Đặt --reserved-concurrent-executions 20 cho consumer. Lambda sẽ không bao giờ chạy quá 20 environment, nghĩa là tối đa 20 kết nối DB. Message dư nằm yên trong queue, không mất. Nhưng chú ý điều sau.
2. Phanh ở event source mapping: maximum-concurrency. Với SQS, Lambda poller tự scale số environment theo độ dài queue (tăng ~300/phút). Nếu bạn chỉ dùng reserved concurrency, poller vẫn lấy message rồi bị throttle, message quay lại queue sau visibility timeout, ReceiveCount tăng, có thể rơi vào DLQ oan. Từ 2023 có ScalingConfig.MaximumConcurrency ngay trên mapping (tối thiểu 2): poller chỉ lấy đúng số đó. Dùng cả hai, hoặc chỉ cái này.
aws lambda create-event-source-mapping \
--function-name shop-order-notifier:live \
--event-source-arn arn:aws:sqs:$AWS_REGION:$ACCOUNT_ID:shop-order-events \
--batch-size 10 --maximum-batching-window-in-seconds 5 \
--scaling-config MaximumConcurrency=20 \
--function-response-types ReportBatchItemFailures --profile shop-lab
3. Phanh ở giữa: RDS Proxy. Lambda kết nối tới proxy (IAM auth, không cần password trong env), proxy giữ pool kết nối thật tới RDS và ghép (multiplex) hàng trăm kết nối Lambda vào vài chục kết nối DB. Tốn tiền theo vCPU của RDS, nhưng là câu trả lời "chuẩn đề thi" cho "Lambda + RDS hết kết nối". Xem bài 22.7.
4. Phanh ở code: mỗi environment chỉ mở một kết nối (static, lazy), không dùng HikariCP pool 10 như trong Spring Boot; đặt connectTimeout, socketTimeout ngắn; visibility timeout của queue ≥ 6 × timeout của function (AWS khuyến nghị) để message không bị xử lý song song hai lần.
Không phanh:
queue 10 000 msg ──► poller scale ──► 300 environment ──► 300 kết nối ──► RDS từ chối, mọi thứ lỗi
Có phanh:
queue 10 000 msg ──► poller tối đa 20 ──► 20 environment ──► RDS Proxy ──► 10 kết nối thật
(msg còn lại chờ trong queue, backlog alarm nếu quá lâu)
Và nếu consumer này cần ghi vào DynamoDB shop-carts thay vì RDS thì gần như không cần phanh: DynamoDB on-demand scale cùng Lambda. Đó là lý do nhiều kiến trúc serverless trên đề thi SAA đi kèm DynamoDB chứ không phải RDS.
Cho mình bài lab để tự tay làm phần này nhé.
Lab 22.12: Resize ảnh bằng Lambda Java 21 + webhook qua API Gateway
Mục tiêu
- Build jar Java 21 bằng Maven shade, deploy function
shop-image-resizebằng CLI, bật SnapStart, trigger từ S3 event trên bucketshop-app-assets-$ACCOUNT_ID; upload ảnh thì có filethumbs/…xuất hiện. - Tạo HTTP API
shop-apivới routePOST /webhooks/payment→shop-order-notifier; curl trả 202 và message xuất hiện trongshop-order-events. - Đọc dòng
REPORTtrong CloudWatch Logs, so sánhInit Durationtrước và sau SnapStart. - Đặt reserved concurrency, quan sát throttle; bật provisioned concurrency rồi tắt.
Chi phí và thời gian
- Lambda: Free Tier 1 triệu request + 400 000 GB-giây/tháng; lab này dùng vài trăm request, gần như 0 USD.
- API Gateway HTTP API: Free Tier 1 triệu request 12 tháng đầu; sau đó khoảng 1 USD/1 triệu request (kiểm tra pricing page).
- Provisioned concurrency không nằm trong Free Tier: tính theo GB-giờ ngay cả khi không gọi, khoảng vài cent/giờ cho 2 × 1 GB. Bật trong 10 phút rồi xóa ngay.
- S3, SQS, CloudWatch Logs: trong Free Tier với lượng này.
- Thời gian: 60–90 phút.
Chuẩn bị
- Profile
shop-labđã assumeShopAdminRole(bài 22.1); JDK 21, Maven, curl,jq. - Bucket
shop-app-assets-$ACCOUNT_ID(bài 22.6) và queueshop-order-events(bài 22.11). Nếu đã dọn, tạo lại nhanh bằngaws s3 mbvàaws sqs create-queue. - Source hai handler ở mục 4 trong module
shop-lambda.
export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --profile shop-lab)
export BUCKET=shop-app-assets-$ACCOUNT_ID
export QUEUE_URL=$(aws sqs get-queue-url --queue-name shop-order-events --query QueueUrl --output text --profile shop-lab)
Các bước
Bước 1: IAM role shop-lambda-role. Chạy đúng các lệnh ở mục 4, Bước 4. Kiểm tra:
aws iam get-role --role-name shop-lambda-role --query Role.Arn --output text --profile shop-lab
# chờ ~10 giây để role lan truyền trước khi tạo function
Console: IAM → Roles → Create role → Trusted entity: AWS service → Lambda → chọn policy AWSLambdaBasicExecutionRole.
Bước 2: build và deploy shop-image-resize
cd shop-lambda && mvn -q clean package
ls -lh target/shop-image-resize.jar # kỳ vọng khoảng 8–15 MB
aws lambda create-function --function-name shop-image-resize \
--runtime java21 --architectures arm64 \
--handler com.shop.lambda.ImageResizeHandler::handleRequest \
--zip-file fileb://target/shop-image-resize.jar \
--role arn:aws:iam::$ACCOUNT_ID:role/shop-lambda-role \
--memory-size 1024 --timeout 30 \
--environment "Variables={THUMB_WIDTH=300}" \
--tags Project=shop-app,Env=lab --profile shop-lab
aws lambda wait function-active-v2 --function-name shop-image-resize --profile shop-lab
Console: Lambda → Functions → Create function → Author from scratch → Runtime Java 21, Architecture arm64 → Upload from .zip or .jar → Runtime settings → Edit handler.
Bước 3: thử gọi tay với event mẫu, đọc Init Duration
aws s3 cp sample.jpg s3://$BUCKET/products/sample.jpg --profile shop-lab
cat > s3-event.json <<EOF
{"Records":[{"eventSource":"aws:s3","awsRegion":"$AWS_REGION","eventName":"ObjectCreated:Put",
"s3":{"bucket":{"name":"$BUCKET"},"object":{"key":"products/sample.jpg"}}}]}
EOF
aws lambda invoke --function-name shop-image-resize \
--cli-binary-format raw-in-base64-out --payload file://s3-event.json \
--log-type Tail out.json --query LogResult --output text --profile shop-lab | base64 -d | grep REPORT
Kỳ vọng dòng kiểu: REPORT RequestId: … Duration: 1830 ms Billed Duration: 1831 ms Memory Size: 1024 MB Max Memory Used: 180 MB Init Duration: 1420 ms. Gọi lần hai ngay sau đó: không còn Init Duration, Duration vài chục ms. Ghi lại hai số này.
Bước 4: bật SnapStart, publish version, tạo alias live
aws lambda update-function-configuration --function-name shop-image-resize \
--snap-start ApplyOn=PublishedVersions --profile shop-lab
aws lambda wait function-updated-v2 --function-name shop-image-resize --profile shop-lab
VER=$(aws lambda publish-version --function-name shop-image-resize \
--query Version --output text --profile shop-lab) # bước này chậm 1–2 phút: chạy init + snapshot
aws lambda create-alias --function-name shop-image-resize --name live \
--function-version $VER --profile shop-lab
# Kiểm tra snapshot sẵn sàng
aws lambda get-function --function-name shop-image-resize:$VER \
--query 'Configuration.SnapStart' --profile shop-lab
# {"ApplyOn": "PublishedVersions", "OptimizationStatus": "On"}
# Gọi qua alias → đọc REPORT lần nữa
aws lambda invoke --function-name shop-image-resize:live \
--cli-binary-format raw-in-base64-out --payload file://s3-event.json \
--log-type Tail out.json --query LogResult --output text --profile shop-lab | base64 -d | grep REPORT
Kỳ vọng: dòng REPORT có thêm Restore Duration: ~200 ms, và Init Duration biến mất. So với 1420 ms ở Bước 3.
Console: Lambda → function → Configuration → General configuration → Edit → SnapStart: PublishedVersions; Versions → Publish new version; Aliases → Create alias.
Bước 5: trigger S3 event vào alias live
# Cho S3 quyền gọi function
aws lambda add-permission --function-name shop-image-resize:live \
--statement-id s3-assets --action lambda:InvokeFunction \
--principal s3.amazonaws.com --source-arn arn:aws:s3:::$BUCKET \
--source-account $ACCOUNT_ID --profile shop-lab
cat > notif.json <<EOF
{"LambdaFunctionConfigurations":[{
"Id":"resize-on-upload",
"LambdaFunctionArn":"arn:aws:lambda:$AWS_REGION:$ACCOUNT_ID:function:shop-image-resize:live",
"Events":["s3:ObjectCreated:*"],
"Filter":{"Key":{"FilterRules":[{"Name":"prefix","Value":"products/"}]}}}]}
EOF
aws s3api put-bucket-notification-configuration --bucket $BUCKET \
--notification-configuration file://notif.json --profile shop-lab
Filter prefix products/ cộng với if (key.startsWith("thumbs/")) continue; trong code là hai lớp bảo vệ chống vòng lặp. Nếu bạn ghi output vào cùng prefix, mỗi thumbnail lại sinh event mới: hóa đơn tăng cho tới khi bạn tắt. (Nếu bucket đã có notification sang SQS/EventBridge từ bài 22.6, giữ nguyên phần đó trong JSON.)
Console: S3 → bucket → Properties → Event notifications → Create → Prefix products/ → All object create events → Destination: Lambda function.
Bước 6: deploy shop-order-notifier và HTTP API
aws lambda create-function --function-name shop-order-notifier \
--runtime java21 --architectures arm64 \
--handler com.shop.lambda.OrderNotifierHandler::handleRequest \
--zip-file fileb://target/shop-image-resize.jar \
--role arn:aws:iam::$ACCOUNT_ID:role/shop-lambda-role \
--memory-size 1024 --timeout 10 \
--environment "Variables={ORDER_QUEUE_URL=$QUEUE_URL}" \
--snap-start ApplyOn=PublishedVersions \
--tags Project=shop-app,Env=lab --profile shop-lab
aws lambda wait function-active-v2 --function-name shop-order-notifier --profile shop-lab
VER2=$(aws lambda publish-version --function-name shop-order-notifier --query Version --output text --profile shop-lab)
aws lambda create-alias --function-name shop-order-notifier --name live --function-version $VER2 --profile shop-lab
FN_ARN=arn:aws:lambda:$AWS_REGION:$ACCOUNT_ID:function:shop-order-notifier:live
# HTTP API, route, integration, stage
API_ID=$(aws apigatewayv2 create-api --name shop-api --protocol-type HTTP \
--tags Project=shop-app,Env=lab --query ApiId --output text --profile shop-lab)
INT_ID=$(aws apigatewayv2 create-integration --api-id $API_ID \
--integration-type AWS_PROXY --integration-uri $FN_ARN \
--payload-format-version 2.0 --query IntegrationId --output text --profile shop-lab)
aws apigatewayv2 create-route --api-id $API_ID \
--route-key "POST /webhooks/payment" --target integrations/$INT_ID --profile shop-lab
aws apigatewayv2 create-stage --api-id $API_ID --stage-name '$default' --auto-deploy \
--default-route-settings ThrottlingBurstLimit=20,ThrottlingRateLimit=10 --profile shop-lab
aws lambda add-permission --function-name shop-order-notifier:live \
--statement-id apigw --action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:$AWS_REGION:$ACCOUNT_ID:$API_ID/*/*/webhooks/payment" --profile shop-lab
API_URL=$(aws apigatewayv2 get-api --api-id $API_ID --query ApiEndpoint --output text --profile shop-lab)
echo $API_URL
Console: API Gateway → Create API → HTTP API → Build → Add integration: Lambda → Configure routes: POST /webhooks/payment → Stages: $default auto-deploy.
Kiểm tra kết quả
# 1. Upload ảnh thật → thumbnail xuất hiện
aws s3 cp sample.jpg s3://$BUCKET/products/p-100.jpg --profile shop-lab
sleep 5
aws s3 ls s3://$BUCKET/thumbs/products/ --profile shop-lab
# 2026-10-21 10:12:03 12345 p-100.jpg
# 2. Webhook qua API Gateway
curl -s -X POST $API_URL/webhooks/payment -H 'Content-Type: application/json' \
-d '{"orderId":"ORD-1001","status":"PAID"}'
# {"status":"queued","orderId":"ORD-1001"}
curl -s -o /dev/null -w '%{http_code}\n' -X POST $API_URL/webhooks/payment -d '{}'
# 400
# 3. Message có trong queue
aws sqs receive-message --queue-url $QUEUE_URL --max-number-of-messages 1 \
--query 'Messages[0].Body' --output text --profile shop-lab
# {"type":"PaymentConfirmed","orderId":"ORD-1001","requestId":"…"}
# 4. Logs Insights: so sánh cold start
aws logs tail /aws/lambda/shop-image-resize --since 30m --filter-pattern REPORT --profile shop-lab
Để nhìn thống kê, chạy query Logs Insights (Console: CloudWatch → Logs Insights → chọn log group /aws/lambda/shop-image-resize):
filter @type = "REPORT"
| stats count(*) as invocations,
avg(@duration) as avg_ms, pct(@duration, 99) as p99_ms,
avg(@initDuration) as avg_init_ms, max(@initDuration) as max_init_ms,
sum(@billedDuration) * @memorySize / 1024 / 1000 as gb_seconds
by bin(5m)
Thử thêm
1. Reserved concurrency = 1 và gọi dồn.
aws lambda put-function-concurrency --function-name shop-order-notifier \
--reserved-concurrent-executions 1 --profile shop-lab
for i in $(seq 1 20); do
curl -s -o /dev/null -w '%{http_code} ' -X POST $API_URL/webhooks/payment \
-d "{\"orderId\":\"ORD-$i\"}" &
done; wait; echo
# Vài request trả 500/429 (Lambda throttled → API Gateway trả lỗi). Xem metric Throttles:
aws cloudwatch get-metric-statistics --namespace AWS/Lambda --metric-name Throttles \
--dimensions Name=FunctionName,Value=shop-order-notifier \
--start-time $(date -u -d '-10 min' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
--period 60 --statistics Sum --profile shop-lab
aws lambda delete-function-concurrency --function-name shop-order-notifier --profile shop-lab
Bài học: sync invoke bị throttle là lỗi trả thẳng về client; async invoke (S3) thì Lambda tự retry tới 6 giờ.
2. Provisioned concurrency trên alias (tắt SnapStart trên function trước, hai thứ này không đi chung; hoặc làm trên function khác):
aws lambda put-provisioned-concurrency-config --function-name shop-order-notifier \
--qualifier live --provisioned-concurrent-executions 2 --profile shop-lab
aws lambda get-provisioned-concurrency-config --function-name shop-order-notifier --qualifier live --profile shop-lab
# Status: IN_PROGRESS → READY sau ~1 phút. Gọi webhook: không còn Init/Restore Duration.
aws lambda delete-provisioned-concurrency-config --function-name shop-order-notifier --qualifier live --profile shop-lab # XÓA NGAY, tốn tiền theo giờ
3. Phá vòng lặp giả: tạm xóa dòng if (key.startsWith("thumbs/")) và filter prefix trong notification, upload một ảnh, nhìn metric Invocations tăng liên tục, rồi đặt reserved concurrency = 0 để dừng khẩn cấp. (Nhớ hoàn lại.)
4. Đổi memory 512 → 1024 → 2048 bằng update-function-configuration --memory-size, publish version mới, so Duration và Billed Duration × memory. Thường 1024 là điểm rẻ nhất cho Java nhỏ.
Dọn tài nguyên
# 1. Provisioned concurrency (nếu còn) — thứ duy nhất tốn tiền theo giờ ở lab này
aws lambda delete-provisioned-concurrency-config --function-name shop-order-notifier --qualifier live --profile shop-lab 2>/dev/null
# 2. API Gateway
aws apigatewayv2 delete-api --api-id $API_ID --profile shop-lab
# 3. S3 notification (để trống) và thumbnail
aws s3api put-bucket-notification-configuration --bucket $BUCKET --notification-configuration '{}' --profile shop-lab
aws s3 rm s3://$BUCKET/thumbs/ --recursive --profile shop-lab
# 4. Functions (xóa function là xóa luôn version, alias, concurrency config)
aws lambda delete-function --function-name shop-image-resize --profile shop-lab
aws lambda delete-function --function-name shop-order-notifier --profile shop-lab
# 5. Log group (không tự xóa theo function; CloudWatch Logs tính tiền lưu trữ sau Free Tier)
aws logs delete-log-group --log-group-name /aws/lambda/shop-image-resize --profile shop-lab
aws logs delete-log-group --log-group-name /aws/lambda/shop-order-notifier --profile shop-lab
# 6. Role (giữ lại nếu bài 22.13–22.15 còn dùng; xóa thì gỡ policy trước)
aws iam delete-role-policy --role-name shop-lambda-role --policy-name shop-lambda-access --profile shop-lab
aws iam detach-role-policy --role-name shop-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole --profile shop-lab
aws iam delete-role --role-name shop-lambda-role --profile shop-lab
# Kiểm tra còn gì gắn tag lab:
aws resourcegroupstaggingapi get-resources --tag-filters Key=Env,Values=lab \
--resource-type-filters lambda apigateway --query 'ResourceTagMappingList[].ResourceARN' --profile shop-lab
Phỏng vấn và đề thi SAA hỏi về Lambda, API Gateway, Step Functions thì sao?
Q1. Cold start của Lambda Java là gì, bạn đã xử lý thế nào?
🇻🇳 Tiếng Việt:
Cold start là lần gọi đầu trên một execution environment mới: Lambda tải code, khởi động JVM, chạy static init. Với Java điều này mất 1–5 giây nếu không tối ưu. Em xử lý theo thứ tự: giảm dependency và không dùng Spring Boot cho function nhỏ, dùng url-connection-client thay Netty; tăng memory để có nhiều CPU hơn; khởi tạo client ở static init; bật SnapStart trên published version để Lambda khôi phục từ snapshot thay vì init lại, cold start xuống còn vài trăm ms; chỉ khi cần p99 ổn định tuyệt đối mới dùng provisioned concurrency vì nó tốn tiền theo giờ. Ngoài ra em thiết kế để đường nóng trả lời nhanh và đẩy việc nặng sang SQS.
🇬🇧 English:
A cold start is the first invocation on a new execution environment: Lambda downloads the code, starts the JVM and runs static initialization. For Java that is 1 to 5 seconds without tuning. I work through a ladder: trim dependencies and avoid Spring Boot for small functions, use the url-connection HTTP client instead of Netty; raise memory to get more CPU; initialize clients in static init; enable SnapStart on a published version so Lambda restores from a snapshot instead of re-initializing, which brings cold starts down to a few hundred milliseconds; and only use provisioned concurrency when I need a rock-solid p99, because it is billed per hour. I also design hot paths to respond quickly and push heavy work to SQS.
Q2. Reserved concurrency và provisioned concurrency khác nhau thế nào?
🇻🇳 Tiếng Việt:
Reserved concurrency là trần số environment chạy đồng thời của một function, đồng thời dành riêng phần đó khỏi hạn mức chung của account; nó miễn phí và đặt 0 thì tắt function, em dùng để bảo vệ downstream như RDS. Provisioned concurrency là số environment được init sẵn và giữ ấm để không có cold start; nó tốn tiền theo giờ, gắn vào version hoặc alias, và có thể auto scale theo lịch. Một cái là phanh, một cái là động cơ nóng sẵn.
🇬🇧 English:
Reserved concurrency is a cap on how many environments of one function can run at the same time, and it also carves that amount out of the account-wide limit; it is free, setting it to zero disables the function, and I use it to protect downstream systems such as RDS. Provisioned concurrency is a number of pre-initialized, kept-warm environments so there is no cold start; it is billed hourly, attached to a version or alias, and can auto scale on a schedule. One is a brake, the other is a pre-warmed engine.
Q3. API Gateway REST API và HTTP API chọn cái nào? Khi nào dùng ALB thay API Gateway?
🇻🇳 Tiếng Việt:
Mặc định em chọn HTTP API: rẻ hơn khoảng 70%, latency thấp hơn, có JWT authorizer và CORS sẵn. Em chuyển sang REST API khi cần API key và usage plan cho đối tác, cache response, request validation, private API trong VPC hay WAF gắn trực tiếp. Nếu hệ thống đã có ALB cho các container và chỉ muốn thêm vài path vào Lambda, dùng ALB target type Lambda để không thêm một lớp nữa; ALB tính tiền theo giờ nên hợp với tải đều, API Gateway tính theo request nên hợp với tải thưa.
🇬🇧 English:
My default is HTTP API: roughly 70 percent cheaper, lower latency, with built-in JWT authorizer and CORS. I move to REST API when I need API keys and usage plans for partners, response caching, request validation, private APIs inside a VPC, or WAF attached directly. If the system already has an ALB for containers and I only want a few paths on Lambda, I use the ALB Lambda target type to avoid another layer; the ALB is billed hourly, so it suits steady traffic, while API Gateway is billed per request and suits spiky or low traffic.
Q4. Khi nào dùng Step Functions thay vì một Lambda hay một chuỗi SQS?
🇻🇳 Tiếng Việt:
Khi logic là workflow nhiều bước có nhánh, retry khác nhau từng bước, cần chờ con người hay hệ ngoài, hoặc cần nhìn thấy trạng thái từng lần chạy. Ví dụ saga đặt hàng: giữ kho, thu tiền, gửi mail, lỗi thì bù trừ. Step Functions Standard chạy tới một năm, exactly-once, tính tiền theo state transition; Express cho workflow ngắn tần suất cao. Nếu chỉ là một bước hoặc fan-out đơn giản, SQS/SNS và Lambda đủ và rẻ hơn.
🇬🇧 English:
When the logic is a multi-step workflow with branches, per-step retry policies, waits for humans or external systems, or when I need visibility into the state of each run. For example an order saga: reserve stock, charge, send email, and compensate on failure. Standard workflows run up to a year with exactly-once semantics and are billed per state transition; Express workflows are for short, high-volume runs. If it is a single step or a simple fan-out, SQS/SNS plus Lambda is enough and cheaper.
Góc thi SAA-C03
Câu 1: Một công ty chạy hàm Lambda viết bằng Java để xử lý đơn hàng qua API Gateway. Người dùng phàn nàn request đầu tiên sau vài phút không hoạt động mất 3–4 giây. Công ty muốn giảm độ trễ này với chi phí thêm THẤP NHẤT. Giải pháp nào?
A. Bật provisioned concurrency 100 cho function.
B. Bật SnapStart trên function và trỏ API Gateway vào một published version/alias.
C. Tăng timeout của function lên 15 phút.
D. Chuyển function sang chạy trên ECS Fargate.
Đáp án: B. SnapStart xử lý đúng cold start của Java, không tốn thêm tiền (với Java), chỉ cần publish version. A cũng giảm cold start nhưng tốn tiền theo giờ, không phải "thấp nhất". C không liên quan: timeout là giới hạn thời gian chạy, không làm init nhanh hơn. D đổi cả kiến trúc, tốn vận hành và tiền chạy liên tục.
Câu 2: Hàm Lambda đọc message từ SQS và ghi vào RDS PostgreSQL. Khi tải tăng, RDS báo "too many connections" và nhiều message rơi vào DLQ. Cần giải pháp có ÍT THAY ĐỔI CODE NHẤT và vẫn xử lý hết message. Chọn HAI.
A. Thêm RDS Proxy giữa Lambda và RDS.
B. Chuyển sang DynamoDB.
C. Đặt maximum concurrency trên event source mapping của SQS.
D. Tăng batch size lên 10 000.
E. Tăng memory của Lambda.
Đáp án: A và C. RDS Proxy ghép hàng trăm kết nối Lambda vào pool nhỏ tới RDS, code chỉ đổi endpoint. Maximum concurrency trên mapping giới hạn số environment poll queue, message thừa chờ trong queue chứ không bị throttle rồi rơi DLQ. B là viết lại ứng dụng. D: batch size SQS tối đa 10 000 nhưng chỉ với tổng payload ≤ 6 MB và không giải quyết số kết nối. E không liên quan tới số kết nối.
Câu 3: Một startup xây API public cho đối tác, cần API key cho từng đối tác với giới hạn 1 000 request/ngày mỗi key, và cache response 60 giây để giảm số lần gọi backend Lambda. Dịch vụ nào đáp ứng?
A. API Gateway HTTP API với JWT authorizer.
B. Application Load Balancer với target Lambda.
C. API Gateway REST API với usage plan và caching.
D. CloudFront trước Lambda Function URL.
Đáp án: C. Usage plan + API key + quota theo ngày + response cache là tính năng riêng của REST API. HTTP API (A) không có usage plan và cache. ALB (B) không có API key/quota. D: CloudFront cache được nhưng không có API key với quota per key; Function URL cũng không có throttle theo client.
Cách nhớ ngắn gọn
- Lambda = chạy khi có event, trả tiền theo GB-giây; Java cần: ít dependency, memory ≥ 1 GB, init ở static, SnapStart trên published version.
- Reserved = phanh (miễn phí, đặt 0 để tắt); Provisioned = giữ ấm (tốn tiền).
- Lambda + RDS hết connection → RDS Proxy + maximum concurrency trên SQS mapping.
- HTTP API rẻ, nhanh, JWT; REST API có usage plan, cache, validation, private, WAF.
- Nhiều bước, nhánh, chờ lâu → Step Functions (Standard dài/exactly-once, Express ngắn/nhiều).
| Từ khóa trong đề | Nghĩ tới |
|---|---|
| "serverless", "no servers to manage", "pay only when running" | Lambda |
| "Java cold start", "initialization latency", "lowest additional cost" | SnapStart |
| "consistent low latency", "predictable", "willing to pay" | Provisioned concurrency |
| "protect downstream", "limit concurrent executions", "disable function" | Reserved concurrency |
| "too many connections" + Lambda + RDS | RDS Proxy |
| "API keys", "usage plan", "cache", "request validation" | API Gateway REST API |
| "lowest cost API", "JWT", "simple proxy to Lambda" | API Gateway HTTP API |
| "orchestrate", "workflow", "wait for approval", "retry each step" | Step Functions |
| "runs longer than 15 minutes" | Không phải Lambda: Fargate/Batch/Step Functions chia nhỏ |
Tự kiểm tra
Vì sao nên tạo S3Client ở field static thay vì trong handleRequest?
Field static chạy một lần mỗi execution environment (giai đoạn init) và được tái dùng cho mọi lần gọi "warm"; SnapStart cũng chụp nó vào snapshot. Tạo trong handler thì mỗi request tốn lại thời gian khởi tạo client và TLS.
SnapStart áp dụng cho $LATEST không? Hệ quả với quy trình deploy?
Không, chỉ cho published version. Phải publish version sau mỗi lần deploy và để trigger (S3, API Gateway) trỏ vào alias, rồi cập nhật alias sang version mới.
Function resize ảnh ghi kết quả vào cùng bucket đang trigger. Rủi ro gì, chặn thế nào?
Vòng lặp vô hạn: thumbnail sinh event mới. Chặn bằng filter prefix/suffix trong notification, kiểm tra key trong code, ghi sang prefix hoặc bucket khác; dừng khẩn cấp bằng reserved concurrency = 0.
Sync invoke và async invoke khác nhau gì khi function lỗi hoặc bị throttle?
Sync (API Gateway, ALB, invoke trực tiếp): lỗi trả thẳng về caller, không tự retry. Async (S3, SNS, EventBridge): Lambda xếp vào queue nội bộ, retry 2 lần, có thể gửi tới on-failure destination hoặc DLQ, và tự retry khi bị throttle tới 6 giờ.
Vì sao tăng memory của Lambda Java có thể làm hóa đơn giảm?
Memory quyết định CPU được cấp. Java init và JIT ăn CPU; nhiều CPU hơn thì Duration giảm nhiều, GB-giây = memory × thời gian có thể giảm dù memory tăng. Thường 1–2 GB là điểm tối ưu cho function Java nhỏ.