Lambda Vòng Lặp Vô Hạn với S3: Ngăn Chặn Recursive Trigger Khi Ghi File Về Cùng Bucket
Bạn vừa deploy một Lambda function xử lý file upload lên S3 rồi ghi kết quả về cùng bucket đó — và ngay lập tức nhận được hóa đơn AWS tăng vọt kèm hàng nghìn invocation trong vài phút. Đây là một trong những lỗi vận hành phổ biến nhất với S3 Event Notifications, và nó không có cảnh báo nào trước khi xảy ra.
TL;DR: Lambda Recursive Trigger với S3
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
| Lambda chạy vô hạn | Output file trigger lại event S3 | Tách bucket input/output hoặc dùng prefix/suffix filter |
| Chi phí tăng đột biến | Mỗi invocation tạo ra invocation mới | Đặt reserved concurrency = 0 để dừng khẩn cấp |
| File bị ghi đè liên tục | Không có điều kiện dừng trong code | Kiểm tra metadata hoặc prefix trước khi xử lý |
| Khó phát hiện sớm | CloudWatch Metrics có độ trễ | Đặt alarm trên ConcurrentExecutions ngay từ đầu |
Cơ Chế Hoạt Động: Tại Sao Lambda Recursive Trigger Xảy Ra
S3 Event Notifications kích hoạt Lambda dựa trên các sự kiện như s3:ObjectCreated:*. Khi Lambda đọc file từ bucket, xử lý, rồi ghi output về cùng bucket đó, hành động PutObject của Lambda tự tạo ra một event mới — event này lại trigger Lambda lần nữa. Không có cơ chế built-in nào của S3 hay Lambda tự động phát hiện vòng lặp này.
- Upload ban đầu: User upload
input/photo.jpglên S3 bucket. - Event kích hoạt Lambda: S3 gửi
s3:ObjectCreatedevent, Lambda được invoke. - Lambda xử lý và ghi output: Lambda ghi
processed/photo.jpgvề cùng bucket. - Vòng lặp bắt đầu: Hành động PutObject tạo event mới, Lambda bị invoke lại.
- Exponential growth: Nếu Lambda chạy đủ nhanh, số invocation tăng theo cấp số nhân cho đến khi chạm concurrency limit.
Giống như đặt một chiếc micro trước loa — tín hiệu đầu ra quay ngược lại thành đầu vào, và tiếng hú ngày càng to hơn cho đến khi bạn rút phích cắm.
AWS đã nhận ra rủi ro này và vào năm 2023 giới thiệu tính năng Recursive Loop Detection cho Lambda. Khi phát hiện một function đang được invoke bởi chính output của nó qua S3 (hoặc SQS, SNS), Lambda sẽ tự động dừng sau 16 lần invoke liên tiếp và gửi thông báo. Tuy nhiên, tính năng này chỉ hoạt động khi Lambda dùng AWS SDK để ghi về bucket — không phải mọi trường hợp đều được bảo vệ.
Dừng Khẩn Cấp: Làm Ngay Khi Đang Xảy Ra
Nếu vòng lặp đang chạy, ưu tiên số một là dừng Lambda ngay lập tức. Đừng mất thời gian debug trong khi invocation đang tích lũy chi phí.
Bước 1 — Đặt reserved concurrency về 0 để throttle toàn bộ invocation:
aws lambda put-function-concurrency \
--function-name my-s3-processor \
--reserved-concurrent-executions 0 \
--region us-east-1
Reserved concurrency = 0 không xóa function, không thay đổi trigger — nó chỉ từ chối mọi invocation mới ngay lập tức. Đây là công tắc khẩn cấp nhanh nhất. Các event từ S3 sẽ bị throttle và tùy cấu hình có thể vào Dead Letter Queue hoặc bị drop sau retry window.
Bước 2 — Xác nhận không còn invocation đang chạy:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name ConcurrentExecutions \
--dimensions Name=FunctionName,Value=my-s3-processor \
--start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 \
--statistics Maximum \
--region us-east-1
Bước 3 — Kiểm tra số lượng invocation trong 15 phút qua để ước tính mức độ thiệt hại:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=my-s3-processor \
--start-time $(date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 \
--statistics Sum \
--region us-east-1
Giải Pháp Triệt Để: Ngăn Lambda Recursive Trigger Từ Gốc
Sau khi dừng vòng lặp, bạn cần chọn một trong các giải pháp dưới đây. Mỗi cách có trade-off khác nhau tùy theo kiến trúc hiện tại.
input và output"] Start -->|Có| Q2["Kiểm soát được tất cả
code path ghi bucket?"] Q2 -->|Có| Sol2["Giải Pháp 2: Prefix/Suffix
filter trên S3 Notification"] Q2 -->|Không| Sol1 Sol2 --> Q3["High concurrency?"] Q3 -->|Không| Sol3["Giải Pháp 3: Object Metadata
check trong Lambda code"] Q3 -->|Có| Sol1 Sol1 --> Best(["Khuyến nghị: An toàn nhất"]) Sol2 --> Medium(["Chấp nhận được nếu kiểm soát tốt"]) Sol3 --> Risky(["Rủi ro race condition"])
Giải Pháp 1: Tách Bucket Input và Output (Khuyến Nghị)
Đây là cách đơn giản nhất và loại bỏ hoàn toàn khả năng xảy ra vòng lặp. Lambda chỉ lắng nghe event từ input-bucket, ghi kết quả về output-bucket — hai bucket này hoàn toàn độc lập.
# Tạo bucket output riêng biệt
aws s3api create-bucket \
--bucket my-processed-output \
--region us-east-1 \
--create-bucket-configuration LocationConstraint=us-east-1
# Xóa S3 trigger cũ (nếu cần cập nhật)
aws lambda remove-permission \
--function-name my-s3-processor \
--statement-id s3-trigger-old \
--region us-east-1
Cập nhật code Lambda để ghi về bucket mới:
import boto3
s3 = boto3.client('s3')
def lambda_handler(event, context):
source_bucket = event['Records'][0]['s3']['bucket']['name']
source_key = event['Records'][0]['s3']['object']['key']
# Đọc file từ input bucket
response = s3.get_object(Bucket=source_bucket, Key=source_key)
content = response['Body'].read()
# Xử lý...
processed_content = process(content)
# Ghi về OUTPUT bucket khác — không phải source_bucket
output_bucket = 'my-processed-output'
output_key = f'processed/{source_key}'
s3.put_object(
Bucket=output_bucket,
Key=output_key,
Body=processed_content
)
return {'statusCode': 200}
Giải Pháp 2: Dùng Prefix/Suffix Filter trên S3 Event Notification
Nếu bắt buộc phải dùng cùng một bucket, cấu hình S3 Event Notification chỉ trigger Lambda khi file có prefix hoặc suffix cụ thể — và đảm bảo output file không match filter đó.
Ví dụ: Lambda chỉ trigger khi file có prefix uploads/, và ghi output về prefix processed/.
🔽 Click để xem cấu hình S3 Notification với filter
# notification-config.json
{
"LambdaFunctionConfigurations": [
{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-s3-processor",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"FilterRules": [
{
"Name": "prefix",
"Value": "uploads/"
}
]
}
}
}
]
}
# Áp dụng cấu hình
aws s3api put-bucket-notification-configuration \
--bucket my-single-bucket \
--notification-configuration file://notification-config.json \
--region us-east-1
Trong code Lambda, ghi output về prefix khác:
def lambda_handler(event, context):
source_key = event['Records'][0]['s3']['object']['key']
# Kiểm tra phòng thủ: chỉ xử lý file từ uploads/
if not source_key.startswith('uploads/'):
print(f'Skipping non-input file: {source_key}')
return {'statusCode': 200}
# Xử lý...
# Ghi về prefix khác — không match filter 'uploads/'
output_key = source_key.replace('uploads/', 'processed/', 1)
s3.put_object(Bucket=source_bucket, Key=output_key, Body=processed_content)
Lớp kiểm tra startswith trong code là tuyến phòng thủ thứ hai — nếu filter S3 bị vô tình thay đổi, Lambda vẫn tự bảo vệ được mình.
Giải Pháp 3: Kiểm Tra Object Metadata Trước Khi Xử Lý
Thêm custom metadata vào object đã xử lý, rồi kiểm tra metadata đó ngay đầu Lambda. Nếu file đã có tag processed=true, bỏ qua ngay lập tức.
def lambda_handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# Kiểm tra metadata trước khi làm bất cứ điều gì
head = s3.head_object(Bucket=bucket, Key=key)
if head.get('Metadata', {}).get('processed') == 'true':
print(f'Already processed: {key}, skipping.')
return {'statusCode': 200}
# Xử lý...
# Ghi lại với metadata đánh dấu đã xử lý
s3.put_object(
Bucket=bucket,
Key=key,
Body=processed_content,
Metadata={'processed': 'true'}
)
Giải pháp này có một điểm yếu: giữa lúc Lambda đọc metadata và ghi lại, nếu có race condition (hai invocation cùng chạy), cả hai đều có thể vượt qua kiểm tra. Đây không phải giải pháp hoàn hảo cho môi trường high-concurrency.
Bẫy Thực Tế: Khi Prefix Filter Không Đủ
Đây là pattern thường gặp trong production: team cấu hình prefix filter uploads/ đúng cách, test thành công, deploy lên production. Vài tuần sau, một developer khác thêm một bước xử lý mới ghi file về uploads/temp/ — và vòng lặp quay lại.
Triệu chứng lần này khác: Lambda không chạy vô hạn ngay lập tức mà chỉ tăng gấp đôi số invocation. CloudWatch Metrics cho thấy Invocations tăng đều đặn nhưng không đột biến, nên không ai để ý. Sau 2 giờ, hóa đơn Lambda tháng đó đã vượt budget.
Chẩn đoán ban đầu sai hướng: team kiểm tra Lambda errors, timeout, memory — tất cả đều bình thường. Không có exception nào. Mãi đến khi xem S3 Server Access Logs mới thấy pattern: mỗi PUT uploads/temp/ kéo theo một GET uploads/temp/ từ Lambda execution role.
Bài học: prefix filter chỉ an toàn khi bạn kiểm soát được tất cả các code path ghi về bucket đó. Nếu bucket được chia sẻ giữa nhiều team hoặc nhiều function, tách bucket là lựa chọn duy nhất thực sự an toàn.
Thiết Lập CloudWatch Alarm Phát Hiện Sớm
Recursive trigger thường xảy ra sau khi deploy, không phải trong lúc test. Đặt alarm trước khi vấn đề xảy ra — không phải sau.
aws cloudwatch put-metric-alarm \
--alarm-name lambda-recursive-trigger-detector \
--alarm-description 'Phát hien Lambda co the dang trong vong lap voi S3' \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=my-s3-processor \
--statistic Sum \
--period 60 \
--evaluation-periods 3 \
--threshold 100 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--region us-east-1
Ngưỡng 100 invocations/phút chỉ là ví dụ — điều chỉnh dựa trên traffic thực tế của bạn. Quan trọng hơn là tỷ lệ tăng đột ngột, không phải con số tuyệt đối.
IAM Permissions Tối Thiểu Cho Lambda S3 Processor
Áp dụng least privilege giúp giới hạn thiệt hại nếu vòng lặp xảy ra — Lambda chỉ có thể đọc từ input bucket và ghi vào output bucket, không thể làm gì khác.
🔽 Click để xem IAM Policy tối thiểu
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadFromInputBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-input-bucket/*"
},
{
"Sid": "WriteToOutputBucket",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-processed-output/*"
},
{
"Sid": "CloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/my-s3-processor:*"
}
]
}
Nếu Lambda execution role không có s3:PutObject trên input bucket, vòng lặp không thể hình thành — ngay cả khi code có bug. IAM là lớp bảo vệ cuối cùng và thường bị bỏ qua.
Khôi Phục Sau Khi Dừng Vòng Lặp
Sau khi đã fix code và cấu hình, khôi phục Lambda về trạng thái bình thường:
# Xóa reserved concurrency throttle để Lambda hoạt động trở lại
aws lambda delete-function-concurrency \
--function-name my-s3-processor \
--region us-east-1
Xác nhận trigger đã được cấu hình đúng:
aws s3api get-bucket-notification-configuration \
--bucket my-input-bucket \
--region us-east-1
Test với một file nhỏ trước khi mở traffic đầy đủ — và theo dõi CloudWatch Metrics trong 5 phút đầu sau khi khôi phục.
Wrap-Up: Ngăn Lambda Recursive Trigger với S3
Vòng lặp vô hạn giữa Lambda và S3 không phải lỗi khó fix — nó chỉ khó phát hiện kịp thời. Ba lớp bảo vệ nên có đồng thời: tách bucket input/output ở kiến trúc, kiểm tra prefix/metadata trong code, và CloudWatch alarm để phát hiện sớm. Recursive Loop Detection của AWS là safety net hữu ích nhưng không thay thế được thiết kế đúng từ đầu.
Tham khảo thêm:
- AWS Lambda với Amazon S3 — Official Documentation
- Lambda Recursive Loop Detection
- S3 Event Notification Filtering
Glossary: Thuật Ngữ Chính
| Thuật ngữ | Giải thích |
|---|---|
| S3 Event Notification | Cơ chế S3 gửi thông báo đến Lambda, SQS, SNS khi có sự kiện như ObjectCreated, ObjectRemoved |
| Reserved Concurrency | Giới hạn số lượng concurrent execution tối đa của một Lambda function; đặt về 0 để throttle hoàn toàn |
| Recursive Trigger | Vòng lặp xảy ra khi output của Lambda kích hoạt lại chính Lambda đó |
| Prefix Filter | Điều kiện lọc trong S3 Event Notification, chỉ trigger khi object key bắt đầu bằng chuỗi chỉ định |
| Recursive Loop Detection | Tính năng của Lambda tự động phát hiện và dừng vòng lặp đệ quy sau 16 lần invoke liên tiếp |
Nhận xét
Đăng nhận xét