Lambda (Java 21, SnapStart, cold start, concurrency), API Gateway, Step Functions

5 câu hỏi~45 phútTừ khóa: Lambda, RequestHandler, cold start, SnapStart, reserved/provisioned concurrency, event source mapping, API Gateway REST vs HTTP API, Lambda proxy integration, Step Functions, SAA: Domain 1 Performance

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 đề

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ĩaTrên shop-app
FunctionĐơn vị deploy: code + runtime + cấu hình (memory, timeout, env, role)shop-image-resize, shop-order-notifier
HandlerMethod được gọi khi có event. Java: class implement RequestHandler<I, O>com.shop.lambda.ImageResizeHandler::handleRequest
Execution roleIAM role mà code chạy với quyền của nó (default credentials chain lấy từ đây)shop-lambda-role
Trigger / event sourceThứ gọi function: S3, SQS, API Gateway, EventBridge, SNS, DynamoDB Streams, Kinesis…S3 event (ảnh), SQS (đơn hàng), HTTP API
Event source mappingLambda tự poll nguồn (SQS, Kinesis, DynamoDB Streams) thay bạn, gom batch rồi gọi handlerLambda poll shop-order-events
Synchronous vs asynchronous invokeSync: 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 startLần gọi đầu trên một execution environment mới: tải code, khởi tạo JVM, chạy static init / constructorJava: 1–6 giây nếu không tối ưu
SnapStartLambda 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 JavaGiả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 concurrencyGiữ sẵn N environment đã init, không cold start. Tốn tiền theo giờ dù không gọiBật cho API nhận webhook giờ sale
Lambda layerGói thư viện/dependency tách khỏi code, dùng chung giữa nhiều functionThường không cần với Java fat jar
Lambda@Edge / CloudFront FunctionsChạ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:

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 requestCao nhấtKhoảng 1/3 RESTTheo giờ ALB + LCU, hợp khi đã có ALB
Latency thêmCao hơnThấp hơnThấp
AuthIAM, Cognito, Lambda authorizer, API keyIAM, JWT (OIDC/Cognito), Lambda authorizerCognito/OIDC trên listener rule
Usage plan, API key, throttling per clientCóKhôngKhông
Cache responseCóKhôngKhô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
WAFCóKhông trực tiếp (qua CloudFront)Có
Payload tối đa10 MB10 MB1 MB
Timeout backend29 s (có thể xin tăng)30 sTheo Lambda (≤ 15 phút) nhưng idle ALB 60 s

Reserved vs provisioned concurrency:

Reserved concurrencyProvisioned concurrency
Mục đíchGiới hạn trần / bảo đảm có phần trong pool accountLoại bỏ cold start
Chi phíMiễn phíTốn theo GB-giờ dù không gọi
Đặt = 0Tắt function (hữu ích khi sự cố)Không áp dụng
Áp lênFunction (mọi version)Một version hoặc alias cụ thể
Kết hợp Auto ScalingKhôngCó (Application Auto Scaling theo lịch hoặc theo utilization)

Lambda vs Step Functions vs tự viết orchestration:

Một Lambda toStep Functions StandardStep Functions Express
Hợp vớiMột bước, ≤ 15 phútWorkflow dài (tới 1 năm), cần audit từng bước, exactly-onceWorkflow ngắn (≤ 5 phút), tần suất cực cao, at-least-once
Tính tiềnGB-giâyTheo state transitionTheo số lần chạy + thời gian
Retry / catchTự codeKhai báo trong ASL (JSON)Khai báo
Chờ người/hệ khácKhô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ứcViệc làmHiệu quả thường thấyChi phí
1Giả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 MB4,8 s → ~1,5 s0
2Tă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
3Khở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=1Giảm thêm 20–40%0
4SnapStart (bật trên published version, gọi qua alias)~0,9 s → ~150–300 msJava: không tốn thêm; chỉ cần publish version
5Provisioned concurrency N environment luôn ấm, có thể scale theo lịch (giờ sale)Cold start ≈ 0 trong N environmentTốn tiền theo giờ
6GraalVM native image (custom runtime provided.al2023) hoặc Spring Boot 3 AOT< 100 msBuild 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:

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

Chi phí và thời gian

Chuẩn bị

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

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 + RDSRDS 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ỏ.