Lambda 지속성 함수 모범 사례 - AWS Lambda

Lambda 지속성 함수 모범 사례

지속성 함수는 기존 Lambda 함수와 다른 패턴이 필요한 재생 기반 실행 모델을 사용합니다. 다음 모범 사례에 따라 안정적이면서 비용 효율적인 워크플로를 빌드합니다.

결정론적 코드 작성

재생 도중에 함수는 처음부터 실행되며 원래 실행과 동일한 실행 경로를 따라야 합니다. 지속성 작업 외부의 코드는 결정론적이어야 하며, 동일한 입력이 주어지면 동일한 결과를 생성해야 합니다.

비결정론적 작업을 단계로 래핑합니다.

  • 난수 생성 및 UUID

  • 현재 시간 또는 타임스탬프

  • 외부 API 직접 호출 및 데이터베이스 쿼리

  • 파일 시스템 작업

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; import { randomUUID } from 'crypto'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate transaction ID inside a step const transactionId = await context.step('generate-transaction-id', async () => { return randomUUID(); }); // Use the same ID throughout execution, even during replay const payment = await context.step('process-payment', async () => { return processPayment(event.amount, transactionId); }); return { statusCode: 200, transactionId, payment }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate transaction ID inside a step transaction_id = context.step( lambda _: str(uuid.uuid4()), name='generate-transaction-id' ) # Use the same ID throughout execution, even during replay payment = context.step( lambda _: process_payment(event['amount'], transaction_id), name='process-payment' ) return {'statusCode': 200, 'transactionId': transaction_id, 'payment': payment}
중요

전역 변수 또는 클로저를 사용하여 단계 간에 상태를 공유하지 말고, 반환 값을 통해 데이터를 전달해야 합니다. 단계가 캐시된 결과를 반환하지만 전역 변수가 재설정되므로 재생 도중 전역 상태가 중단됩니다.

클로저 변형 지양: 클로저에 캡처된 변수는 재생 도중 변형(mutation)이 손실될 수 있습니다. 단계는 캐시된 결과를 반환하지만 단계 외부의 변수 업데이트는 재생되지 않습니다.

TypeScript
// ❌ WRONG: Mutations lost on replay export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { await context.step(async () => { total += item.price; // ⚠️ Mutation lost on replay! return saveItem(item); }); } return { total }; // Inconsistent value! }); // ✅ CORRECT: Accumulate with return values export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { total = await context.step(async () => { const newTotal = total + item.price; await saveItem(item); return newTotal; // Return updated value }); } return { total }; // Consistent! }); // ✅ EVEN BETTER: Use map for parallel processing export const handler = withDurableExecution(async (event, context) => { const results = await context.map( items, async (ctx, item) => { await ctx.step(async () => saveItem(item)); return item.price; } ); const total = results.getResults().reduce((sum, price) => sum + price, 0); return { total }; });
Python
# ❌ WRONG: Mutations lost on replay @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: context.step( lambda _: save_item_and_mutate(item, total), # ⚠️ Mutation lost on replay! name=f'save-item-{item["id"]}' ) return {'total': total} # Inconsistent value! # ✅ CORRECT: Accumulate with return values @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: total = context.step( lambda _: save_item_and_return_total(item, total), name=f'save-item-{item["id"]}' ) return {'total': total} # Consistent! # ✅ EVEN BETTER: Use map for parallel processing @durable_execution def handler(event, context: DurableContext): def process_item(ctx, item): ctx.step(lambda _: save_item(item)) return item['price'] results = context.map(items, process_item) total = sum(results.get_results()) return {'total': total}

멱등성을 고려한 설계

재시도 또는 재생으로 인해 작업이 여러 번 실행될 수 있습니다. 비멱등성 작업으로 인해 고객에게 두 번 요금을 부과하거나 여러 번 이메일을 보내는 등의 중복과 관련된 부작용이 발생합니다.

멱등성 토큰 사용: 단계 내에 토큰을 생성하고 외부 API 직접 호출에 포함하여 중복 작업을 방지합니다.

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate idempotency token once const idempotencyToken = await context.step('generate-idempotency-token', async () => { return crypto.randomUUID(); }); // Use token to prevent duplicate charges const charge = await context.step('charge-payment', async () => { return paymentService.charge({ amount: event.amount, cardToken: event.cardToken, idempotencyKey: idempotencyToken }); }); return { statusCode: 200, charge }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate idempotency token once idempotency_token = context.step( lambda _: str(uuid.uuid4()), name='generate-idempotency-token' ) # Use token to prevent duplicate charges def charge_payment(_): return payment_service.charge( amount=event['amount'], card_token=event['cardToken'], idempotency_key=idempotency_token ) charge = context.step(charge_payment, name='charge-payment') return {'statusCode': 200, 'charge': charge}

최대 1회 시맨틱 사용: 복제해서는 안 되는 중요 작업(재무 거래, 재고 차감)의 경우 최대 1회 실행 모드를 구성합니다.

TypeScript
// Critical operation that must not duplicate await context.step('deduct-inventory', async () => { return inventoryService.deduct(event.productId, event.quantity); }, { executionMode: 'AT_MOST_ONCE_PER_RETRY' });
Python
# Critical operation that must not duplicate context.step( lambda _: inventory_service.deduct(event['productId'], event['quantity']), name='deduct-inventory', config=StepConfig(execution_mode='AT_MOST_ONCE_PER_RETRY') )

데이터베이스 멱등성: check-before-write 패턴, 조건부 업데이트 또는 업서트 작업을 사용하여 레코드 중복을 방지합니다.

효율적으로 상태 관리

모든 체크포인트는 영구 스토리지에 상태를 저장합니다. 상태 객체의 크기가 크면 비용이 증가하고 체크포인트 속도가 느려지며 성능에 영향을 미칩니다. 필수 워크플로 조정 데이터만 저장해야 합니다.

상태를 최소한으로 유지합니다.

  • 전체 객체가 아닌 ID와 참조 저장

  • 필요에 따라 단계 내에서 세부 데이터 가져오기

  • 대용량 데이터에 Amazon S3 또는 DynamoDB 사용, 상태의 참조 전달

  • 단계 사이에 큰 페이로드 전달 방지

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Store only the order ID, not the full order object const orderId = event.orderId; // Fetch data within each step as needed await context.step('validate-order', async () => { const order = await orderService.getOrder(orderId); return validateOrder(order); }); await context.step('process-payment', async () => { const order = await orderService.getOrder(orderId); return processPayment(order); }); return { statusCode: 200, orderId }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext @durable_execution def handler(event, context: DurableContext): # Store only the order ID, not the full order object order_id = event['orderId'] # Fetch data within each step as needed context.step( lambda _: validate_order(order_service.get_order(order_id)), name='validate-order' ) context.step( lambda _: process_payment(order_service.get_order(order_id)), name='process-payment' ) return {'statusCode': 200, 'orderId': order_id}

효과적인 단계 설계

단계는 지속성 함수의 기본 작업 단위입니다. 잘 설계된 단계를 통해 워크플로를 더 쉽게 이해하고 디버깅하고 유지 관리할 수 있습니다.

단계 설계 원칙:

  • 설명이 포함된 이름 사용 - 로그와 오류를 쉽게 이해할 수 있도록 step1 대신 validate-order 같은 이름을 사용합니다.

  • 이름을 정적 상태로 유지 - 타임스탬프 또는 임의 값이 포함된 동적 이름은 사용하지 않습니다. 재생을 위해 단계 이름은 결정론적이어야 합니다.

  • 세분화 균형 조정 - 복잡한 작업을 보다 집중된 여러 단계로 나누되 체크포인트 오버헤드를 높일 수 있으므로 지나치게 작은 단계는 생성하지 않습니다.

  • 그룹 관련 작업 - 함께 성공 또는 실패해야 하는 작업은 동일한 단계에 속합니다.

효율적으로 대기 작업 사용

대기 작업은 리소스를 소비하거나 비용을 발생시키지 않고 실행을 일시 중지합니다. Lambda를 계속 실행하는 대신 대기 작업을 사용하세요.

시간 기반 대기: setTimeout 또는 sleep 대신 지연에 context.wait()을 사용합니다.

외부 콜백: 외부 시스템을 기다릴 때 context.waitForCallback()을 사용합니다. 무기한 대기를 방지하려면 항상 제한 시간을 설정합니다.

폴링: 지수 백오프와 함께 context.waitForCondition()을 사용하여 과부하 없이 외부 서비스를 폴링합니다.

TypeScript
// Wait 24 hours without cost await context.wait({ seconds: 86400 }); // Wait for external callback with timeout const result = await context.waitForCallback( 'external-job', async (callbackId) => { await externalService.submitJob({ data: event.data, webhookUrl: `https://api.example.com/callbacks/${callbackId}` }); }, { timeout: { seconds: 3600 } } );
Python
# Wait 24 hours without cost context.wait(86400) # Wait for external callback with timeout result = context.wait_for_callback( lambda callback_id: external_service.submit_job( data=event['data'], webhook_url=f'https://api.example.com/callbacks/{callback_id}' ), name='external-job', config=WaitForCallbackConfig(timeout_seconds=3600) )

추가 고려 사항

오류 처리: 네트워크 제한 시간 및 속도 제한과 같은 일시적 실패가 발생하면 재시도합니다. 잘못된 입력 또는 인증 오류와 같은 영구적 실패는 재시도하지 않습니다. 적절한 최대 시도 횟수 및 백오프 속도로 재시도 전략을 구성합니다. 자세한 예제는 오류 처리 및 재시도를 참조하세요.

성능: 전체 페이로드 대신 참조를 저장하여 체크포인트 크기를 최소화합니다. context.parallel()context.map()을 사용하여 독립적인 작업을 동시에 실행합니다. 관련 작업을 배치로 처리하여 체크포인트 오버헤드를 줄이세요.

버전 관리: 버전 번호 또는 별칭이 있는 함수를 간접 호출하여 실행을 특정 코드 버전에 고정합니다. 새 코드 버전이 이전 버전의 상태를 처리할 수 있어야 합니다. 재생이 중단되는 방식으로 단계의 이름을 바꾸거나 동작을 변경하지 않습니다.

직렬화: 작업 입력 및 결과에 JSON 호환 유형을 사용합니다. 지속성 작업에 전달하기 전에 날짜를 ISO 문자열로 변환하고 사용자 지정 객체를 일반 객체로 변환합니다.

모니터링: 실행 ID 및 단계 이름을 포함하여 구조화된 로깅을 활성화합니다. 오류 발생률 및 실행 기간에 대한 CloudWatch 경보를 설정합니다. 추적을 사용하여 병목 현상을 식별합니다. 자세한 지침은 모니터링 및 디버깅을 참조하세요.

테스트: 해피 패스, 오류 처리 및 재생 동작을 테스트합니다. 콜백 및 대기에 대한 제한 시간 시나리오를 테스트합니다. 로컬 테스트를 사용하여 반복 시간을 줄입니다. 자세한 지침은 지속성 함수 테스트를 참조하세요.

피해야 할 일반적인 실수: context.step() 호출을 중첩하지 않고, 대신 하위 컨텍스트를 사용합니다. 비결정론적 작업을 단계로 래핑합니다. 항상 콜백에 대한 제한 시간을 설정합니다. 단계 세분성과 체크포인트 오버헤드 사이의 균형을 맞춥니다. 큰 객체 대신 참조를 상태로 저장합니다.

추가 리소스