Step Functions: Điều Phối Quy Trình Nhiều Bước và Mẫu Saga
Việc đếm click ở các bài trước là một bước đơn: nhận sự kiện, cộng số, xong. Nhưng có những quy trình nhiều bước, có thứ tự, có rẽ nhánh và phải xử lý lỗi ở từng bước. Kiểm duyệt một link trước khi cho nó hoạt động là một ví dụ: quét xem URL có an toàn không, rồi tùy kết quả mà kích hoạt hoặc từ chối, và nếu bước quét lỗi tạm thời thì thử lại. Nhồi cả chuỗi đó vào một hàm Lambda thì code rối và khó thấy luồng. Step Functions sinh ra để tách phần điều phối đó ra.
Mục tiêu
Dựng một state machine kiểm duyệt link bằng Step Functions: một bước quét an toàn (Lambda), một bước rẽ nhánh theo kết quả, và hai bước cập nhật trạng thái gọi thẳng DynamoDB không qua Lambda. Thêm Retry và Catch để xử lý lỗi, chạy thật để xem luồng đi qua các state, rồi bàn tới mẫu saga cho việc hoàn tác. Step Functions Standard tính tiền theo số lần chuyển state, một workflow vài bước gần như miễn phí ở mức test.
Step Functions điều phối, không xử lý
Một state machine là một sơ đồ các bước (state) và cách chuyển giữa chúng, viết bằng Amazon States Language (ASL) dạng JSON. Mỗi state làm một việc: gọi một Lambda, gọi thẳng một dịch vụ AWS, rẽ nhánh theo điều kiện, chờ, hay kết thúc. Step Functions giữ trạng thái giữa các bước, tự thử lại theo luật bạn khai, và ghi lại lịch sử từng lần chạy. Việc xử lý nặng vẫn nằm trong Lambda hay dịch vụ; Step Functions lo phần điều phối chúng theo đúng thứ tự và xử lý lỗi.
Standard hay Express
Step Functions có hai loại workflow, và chọn sai thì hoặc tốn tiền hoặc thiếu đảm bảo. Tài liệu phân biệt rõ: Standard Workflows "support long-running executions (up to one year) with exactly-once execution semantics, making them suitable for non-idempotent actions like payment processing, and are billed per state transition." Express Workflows "are designed for high-volume, short-duration workloads (up to five minutes) with at-least-once execution semantics... billed based on execution count, duration, and memory."
Nói gọn: Standard cho quy trình chạy lâu, cần đảm bảo đúng một lần và cần lịch sử để soi (như phê duyệt, thanh toán); Express cho luồng tần suất rất cao, ngắn, và idempotent. Kiểm duyệt link không cần tốc độ chục nghìn lần một giây, nhưng cần nhìn lại được từng lần chạy và đảm bảo không kích hoạt nhầm hai lần, nên ta chọn Standard.
Workflow kiểm duyệt
Quy trình gồm bốn state. CheckSafety gọi một Lambda quét URL và trả về safe: true/false. IsSafe là một Choice rẽ nhánh theo giá trị đó. Activate và Reject cập nhật trạng thái link trong DynamoDB. Điểm đáng chú ý: Activate và Reject không qua Lambda mà gọi thẳng dynamodb:updateItem như một service integration, vì việc chỉ là một lệnh ghi, không cần code.
┌─────────────┐
│ CheckSafety │ Task -> Lambda quet URL (Retry 3 lan, Catch -> MarkError)
└──────┬──────┘
▼
┌─────────┐ safe == true
│ IsSafe │───────────────▶ ┌──────────┐ dynamodb:updateItem
│(Choice) │ │ Activate │ status = "active"
└────┬────┘ └──────────┘
│ default (khong an toan)
▼
┌──────────┐ dynamodb:updateItem
│ Reject │ status = "rejected"
└──────────┘
Bước CheckSafety khai Retry và Catch để xử lý lỗi:
"CheckSafety": {
"Type": "Task",
"Resource": "${ModerateCheckArn}",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException", "States.TaskFailed"],
"IntervalSeconds": 1, "MaxAttempts": 3, "BackoffRate": 2
}
],
"Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "MarkError" } ],
"Next": "IsSafe"
}
Retry tự thử lại tối đa ba lần với khoảng cách tăng dần (backoff) khi gặp lỗi tạm thời. Nếu sau khi hết lần thử mà vẫn lỗi, Catch bắt mọi lỗi còn lại và chuyển sang state MarkError thay vì để cả workflow chết lặng. Phần xử lý lỗi này nếu viết trong một Lambda đơn lẻ sẽ là một mớ try/catch và vòng lặp; ở đây nó là khai báo.
Bước rẽ nhánh là một Choice đọc trường safe trong dữ liệu:
"IsSafe": {
"Type": "Choice",
"Choices": [ { "Variable": "$.safe", "BooleanEquals": true, "Next": "Activate" } ],
"Default": "Reject"
}
State machine khai trong SAM bằng AWS::Serverless::StateMachine, với các giá trị ARN và tên bảng thay vào ASL qua DefinitionSubstitutions:
LinkModerationStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
Type: STANDARD
DefinitionUri: statemachine/moderation.asl.json
DefinitionSubstitutions:
ModerateCheckArn: !GetAtt ModerateCheckFunction.Arn
TableName: !Ref Table
Policies:
- LambdaInvokePolicy: { FunctionName: !Ref ModerateCheckFunction }
- DynamoDBCrudPolicy: { TableName: !Ref Table }
Chạy thật
Tạo hai link trạng thái pending, một trỏ tới URL bình thường, một chứa từ trong danh sách cấm. Chạy workflow cho từng cái rồi đọc trạng thái. Cả hai execution đều SUCCEEDED, và trạng thái link phản ánh nhánh đã đi:
=== execution link an toan === SUCCEEDED
=== execution link co 'malware' === SUCCEEDED
$ status trong DynamoDB sau kiem duyet:
safe001: active
bad001 : rejected
Cùng một workflow, hai kết quả khác nhau tùy dữ liệu vào. Để thấy luồng thực sự đi qua những state nào, lấy lịch sử của một lần chạy link không an toàn:
$ aws stepfunctions get-execution-history --execution-arn "$ARN" \
--query "events[?stateEnteredEventDetails!=null].stateEnteredEventDetails.name" --output text
CheckSafety IsSafe Reject
Lịch sử cho thấy execution vào CheckSafety, qua IsSafe, rồi rẽ sang Reject, đúng như sơ đồ. Mỗi lần chạy đều để lại dấu vết như vậy, nên khi một quy trình thật trục trặc, bạn nhìn được nó dừng ở bước nào và với dữ liệu gì, điều rất khó có nếu cả chuỗi nằm trong một hàm.
Saga: hoàn tác khi giữa chừng thất bại
Workflow trên chỉ đọc và ghi một item, nên không cần hoàn tác. Nhưng nhiều quy trình thật gồm chuỗi bước thay đổi nhiều nơi, và nếu một bước giữa chừng thất bại thì các bước trước đó cần được hoàn tác. Đó là mẫu saga: mỗi bước có một bước bù trừ (compensation) để undo, và khi có lỗi, workflow chạy các bước bù trừ theo chiều ngược lại.
Lấy ví dụ tính năng tên miền tùy chỉnh mà series sẽ không dựng nhưng đáng hình dung: giữ chỗ tên miền, tính phí, rồi xác nhận. Nếu bước tính phí thất bại sau khi đã giữ chỗ, ta phải nhả tên miền đã giữ, nếu không nó bị khóa vĩnh viễn. Trong ASL, mỗi bước có thể Catch lỗi và chuyển sang một state bù trừ:
"ChargeCard": {
"Type": "Task",
"Resource": "${ChargeArn}",
"Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "ReleaseDomain" } ],
"Next": "ConfirmDomain"
},
"ReleaseDomain": {
"Type": "Task",
"Resource": "${ReleaseArn}",
"Next": "FailSaga"
}
Step Functions hợp với saga vì việc giữ trạng thái và bắt lỗi theo từng bước vốn là thứ nó làm sẵn. Bạn không phải tự viết logic theo dõi "đã làm tới đâu để mà undo"; nó nằm trong cấu trúc state machine. Standard workflow với exactly-once là lựa chọn đúng cho loại quy trình này, vì hoàn tác mà chạy nhầm hai lần lại sinh lỗi mới.
🧹 Dọn dẹp
aws dynamodb delete-item --table-name url-shortener --key '{"PK":{"S":"LINK#safe001"},"SK":{"S":"META"}}'
aws dynamodb delete-item --table-name url-shortener --key '{"PK":{"S":"LINK#bad001"},"SK":{"S":"META"}}'
Giữ stack cho phần sau.
Tổng kết
Step Functions tách phần điều phối một quy trình nhiều bước ra khỏi code xử lý. Workflow kiểm duyệt của ta gồm một bước quét bằng Lambda, một bước rẽ nhánh Choice, và hai bước ghi DynamoDB gọi thẳng không qua Lambda, kèm Retry và Catch khai báo thay vì viết tay. Mỗi lần chạy để lại lịch sử state đọc được. Standard hợp khi cần đảm bảo đúng một lần và soi lại được, còn Express hợp với luồng tần suất rất cao. Và mẫu saga dùng chính cơ chế Catch theo từng bước để hoàn tác khi một bước giữa chừng thất bại.
Phần event-driven tới đây đã đủ: API, dữ liệu, auth, sự kiện, realtime, điều phối. Sản phẩm chạy được đầu cuối, nhưng vận hành nó như production thì cần nhìn được vào bên trong khi có sự cố. Phần V mở đầu bằng quan sát: bài sau gắn Lambda Powertools để có log có cấu trúc và tracing, rồi đọc một service map của X-Ray để thấy một request đi qua những đâu và chậm ở khâu nào.