Lambda 실행 환경 수명주기 이해
Lambda 실행 환경 수명주기 이해 (Understanding the Lambda execution environment lifecycle)
Lambda 실행 환경은 표준 함수(최대 15분)와 Durable Functions(최대 1년)를 모두 지원해요. 둘 다 동일한 기본 수명주기를 공유하지만, Durable Functions는 장기 실행 워크플로를 위한 상태 관리 기능을 추가해요.
본문
Lambda는 보안되고 격리된 런타임 환경을 제공하는 실행 환경에서 함수를 호출해요. 실행 환경은 함수를 실행하는 데 필요한 리소스를 관리해요. 또한 함수의 런타임과 함수와 연결된 외부 확장에 대한 수명주기 지원도 제공해요.
Durable Functions의 경우 실행 환경에는 다음을 위한 추가 구성 요소가 포함돼요.
- 단계 간 상태 유지(persistence)
- 체크포인팅 관리
- 대기 상태 조정
- 진행 상황 추적
Lambda Managed Instances 실행 환경
Lambda Managed Instances를 사용하는 경우 실행 환경은 Lambda(기본) 함수와 중요한 차이점이 있어요. Managed Instances는 동시 호출을 지원하고, 다른 수명주기 모델을 사용하며, 고객 소유 인프라에서 실행돼요. Managed Instances 실행 환경에 대한 자세한 내용은 Lambda Managed Instances 실행 환경 이해를 참고하세요.
함수의 런타임은 Runtime API를 사용해 Lambda와 통신해요. 확장은 Extensions API를 사용해 Lambda와 통신해요. 확장은 Telemetry API를 사용해 함수의 로그 메시지와 기타 텔레메트리도 수신할 수 있어요.
Lambda 함수를 만들 때 메모리 양, 최대 실행 시간 등 구성 정보를 지정해요. Lambda는 이 정보를 사용해 실행 환경을 설정해요.
함수의 런타임과 각 외부 확장은 실행 환경 내에서 실행되는 프로세스예요. 권한, 리소스, 자격 증명, 환경 변수는 함수와 확장 간에 공유돼요.
Lambda 실행 환경 수명주기 (Lambda execution environment lifecycle)
각 단계는 Lambda가 런타임과 등록된 모든 확장에 보내는 이벤트로 시작해요. 런타임과 각 확장은 Next API 요청을 보내 완료를 알려요. 런타임과 각 확장이 완료되고 보류 중인 이벤트가 없으면 Lambda는 실행 환경을 정지(freeze)해요.
Durable Functions의 수명주기 단계에는 다음이 포함돼요.
- Init : 표준 초기화 + durable 상태 설정
- Invoke : 자동 체크포인팅이 포함된 여러 단계 실행 가능
- Wait : 함수가 리소스를 소비하지 않고 실행을 일시 중지할 수 있음
- Resume : 함수가 마지막 체크포인트에서 다시 시작
- Shutdown : durable 상태와 리소스 정리
Init 단계 (Init phase)
Init 단계에서 Lambda는 세 가지 작업을 수행해요.
- 모든 확장 시작 (
Extension init) - 런타임 부트스트랩 (
Runtime init) - 함수의 정적 코드 실행 (
Function init) - (Lambda SnapStart 전용) 체크포인트 이전 런타임 훅 실행
Init 단계는 런타임과 모든 확장이 Next API 요청을 보내 준비되었음을 알릴 때 종료돼요. Init 단계는 10초로 제한돼요. 세 작업이 모두 10초 안에 완료되지 않으면 Lambda는 구성된 함수 타임아웃으로 첫 함수 호출 시점에 Init 단계를 다시 시도해요.
SnapStart가 활성화되면 함수 버전을 게시할 때 Init 단계가 발생해요. Lambda는 초기화된 실행 환경의 메모리와 디스크 상태 스냅샷을 저장하고, 암호화된 스냅샷을 유지하며, 저지연 액세스를 위해 캐시해요. 체크포인트 이전 런타임 훅이 있으면 코드는 Init 단계 끝에 실행돼요.
참고
10초 타임아웃은 프로비저닝된 동시성, SnapStart, Lambda Managed Instances를 사용하는 함수에는 적용되지 않아요. 프로비저닝된 동시성, SnapStart, Managed Instances 함수의 경우 초기화 코드는 최대 15분 동안 실행될 수 있어요. 시간 제한은 130초 또는 구성된 함수 타임아웃(최대 900초) 중 더 높은 값이에요.
프로비저닝된 동시성을 사용하면 함수에 대한 PC 설정을 구성할 때 Lambda가 실행 환경을 초기화해요. Lambda는 호출보다 앞서 초기화된 실행 환경이 항상 사용 가능하도록 보장해요. 함수의 호출 단계와 초기화 단계 사이에 간격이 보일 수 있어요. 함수의 런타임과 메모리 구성에 따라 초기화된 실행 환경에서 첫 호출 시 변동 지연 시간이 발생할 수도 있어요.
온디맨드 동시성을 사용하는 함수의 경우 Lambda가 호출 요청보다 앞서 실행 환경을 초기화할 때가 있어요. 이 경우 함수의 초기화 단계와 호출 단계 사이에 시간 간격이 관찰될 수 있어요. 이 동작에 의존하지 않는 것을 권장해요.
Init 단계 중 오류 (Failures during the Init phase)
Init 단계 중 함수가 크래시하거나 타임아웃되면 Lambda는 INIT_REPORT 로그에 오류 정보를 기록해요.
예제 — 타임아웃에 대한 INIT_REPORT 로그
INIT_REPORT Init Duration: 1236.04 ms Phase: init Status: timeout
예제 — 확장 오류에 대한 INIT_REPORT 로그
INIT_REPORT Init Duration: 1236.04 ms Phase: init Status: error Error Type: Extension.Crash
Init 단계가 성공하면 SnapStart나 프로비저닝된 동시성이 활성화되지 않는 한 Lambda는 INIT_REPORT 로그를 기록하지 않아요. SnapStart와 프로비저닝된 동시성 함수는 항상 INIT_REPORT 를 기록해요. 자세한 내용은 Lambda SnapStart 모니터링을 참고하세요.
복원 단계 (Lambda SnapStart 전용) (Restore phase)
SnapStart 함수를 처음 호출하고 함수가 확장됨에 따라 Lambda는 처음부터 함수를 초기화하는 대신 유지된 스냅샷에서 새 실행 환경을 재개해요. 복원 후 런타임 훅이 있으면 코드는 Restore 단계 끝에 실행돼요. 복원 후 런타임 훅의 지속 시간에 대해 요금이 부과돼요. 런타임은 로드되고 복원 후 런타임 훅은 타임아웃 제한(10초) 내에 완료되어야 해요. 그렇지 않으면 SnapStartTimeoutException 이 발생해요. Restore 단계가 완료되면 Lambda는 함수 핸들러를 호출해요(Invoke 단계).
Restore 단계 중 오류
Restore 단계가 실패하면 Lambda는 RESTORE_REPORT 로그에 오류 정보를 기록해요.
예제 — 타임아웃에 대한 RESTORE_REPORT 로그
RESTORE_REPORT Restore Duration: 1236.04 ms Status: timeout
예제 — 런타임 훅 오류에 대한 RESTORE_REPORT 로그
RESTORE_REPORT Restore Duration: 1236.04 ms Status: error Error Type: Runtime.ExitError
RESTORE_REPORT 로그에 대한 자세한 내용은 Lambda SnapStart 모니터링을 참고하세요.
Invoke 단계 (Invoke phase)
Next API 요청에 대한 응답으로 Lambda 함수가 호출되면 Lambda는 Invoke 이벤트를 런타임과 각 확장에 보내요.
함수의 타임아웃 설정은 전체 Invoke 단계의 지속 시간을 제한해요. 예를 들어 함수 타임아웃을 360초로 설정하면 함수와 모든 확장은 360초 안에 완료되어야 해요. 독립적인 post-invoke 단계는 없다는 점에 유의하세요. 지속 시간은 모든 호출 시간(런타임 + 확장)의 합이며, 함수와 모든 확장이 실행을 마칠 때까지 계산되지 않아요.
Invoke 단계는 런타임과 모든 확장이 Next API 요청을 보내 완료를 알릴 때 종료돼요.
Invoke 단계 중 오류 (Failures during the invoke phase)
Lambda 함수가 Invoke 단계 중 크래시하거나 타임아웃되면 Lambda는 실행 환경을 재설정해요. 이 동작을 요약하면 다음과 같아요.
- 첫 번째 단계는 오류 없이 실행되는 INIT 단계예요.
- 두 번째 단계는 오류 없이 실행되는 INVOKE 단계예요.
- 어느 시점에 함수에 호출 오류가 발생한다고 가정해요(일반적인 원인은 함수 타임아웃, 런타임 오류, 메모리 고갈, VPC 연결 문제, 권한 오류, 동시성 제한, 다양한 구성 문제). 가능한 호출 오류 전체 목록은 Lambda에서 호출 문제 해결을 참고하세요.
이 경우 Lambda 서비스는 재설정을 수행해요. 재설정은
Shutdown이벤트처럼 동작해요. 먼저 Lambda가 런타임을 종료한 다음 등록된 각 외부 확장에Shutdown이벤트를 보내요. 이벤트에는 종료 사유가 포함돼요. 이 환경이 새 호출에 사용되면 Lambda는 다음 호출과 함께 확장과 런타임을 다시 초기화해요. Lambda 재설정은 다음 init 단계 전에/tmp디렉터리 내용을 지우지 않아요. 이 동작은 일반 shutdown 단계와 일관돼요. - 재설정 직후의 INVOKE 단계에서 Lambda는 INIT 단계를 다시 실행해 환경을 다시 초기화해요. 이를 억제된 init(suppressed init) 이라고 해요. 억제된 init가 발생하면 Lambda는 CloudWatch Logs에서 별도의 INIT 단계를 명시적으로 보고하지 않아요. 대신 REPORT 줄의 지속 시간에 추가 INIT 지속 시간 + INVOKE 지속 시간이 포함된 것을 확인할 수 있어요.
- 다섯 번째 단계는 오류 없이 실행되는 SHUTDOWN 단계예요.
참고
AWS는 현재 Lambda 서비스에 변경 사항을 적용하고 있어요. 이 변경으로 인해 AWS 계정의 다른 Lambda 함수가 내보내는 시스템 로그 메시지와 추적 세그먼트의 구조와 내용에 약간의 차이가 보일 수 있어요. 함수의 시스템 로그 구성이 일반 텍스트로 설정된 경우 함수에 호출 오류가 발생할 때 CloudWatch Logs에 캡처되는 로그 메시지에 이 변경이 영향을 미쳐요. 이 변경은 향후 몇 주 내에 적용되며, 중국 및 GovCloud 리전을 제외한 모든 AWS 리전의 모든 함수가 새 형식의 로그 메시지와 추적 세그먼트로 전환돼요.
새 형식의 CloudWatch 로그에는 REPORT 줄에 추가 status 필드가 포함돼요. 런타임 또는 확장 크래시의 경우 REPORT 줄에 ErrorType 필드도 포함돼요.
예제 CloudWatch Logs 로그 출력(런타임 또는 확장 크래시) — 기존 형식
START RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Version: $LATEST
RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Error: Runtime exited without providing a reason
Runtime.ExitError
END RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1
REPORT RequestId: c3252230-c73d-49f6-8844-968c01d1e2e1 Duration: 933.59 ms Billed Duration: 934 ms Memory Size: 128 MB Max Memory Used: 9 MB
예제 CloudWatch Logs 로그 출력(함수 타임아웃) — 기존 형식
START RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21 Version: $LATEST
2024-03-04T17:22:38.033Z b70435cc-261c-4438-b9b6-efe4c8f04b21 Task timed out after 3.00 seconds
END RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21
REPORT RequestId: b70435cc-261c-4438-b9b6-efe4c8f04b21 Duration: 3004.92 ms Billed Duration: 3117 ms Memory Size: 128 MB Max Memory Used: 33 MB Init Duration: 111.23 ms
예제 CloudWatch Logs 로그 출력(런타임 또는 확장 크래시) — 새 형식
START RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd Version: $LATEST
END RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd
REPORT RequestId: 5b866fb1-7154-4af6-8078-6ef6ca4c2ddd Duration: 133.61 ms Billed Duration: 214 ms Memory Size: 128 MB Max Memory Used: 31 MB Init Duration: 80.00 ms Status: error Error Type: Runtime.ExitError
예제 CloudWatch Logs 로그 출력(함수 타임아웃) — 새 형식
START RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda Version: $LATEST
END RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda
REPORT RequestId: 527cb862-4f5e-49a9-9ae4-a7edc90f0fda Duration: 3016.78 ms Billed Duration: 3101 ms Memory Size: 128 MB Max Memory Used: 31 MB Init Duration: 84.00 ms Status: timeout
억제된 init가 발생하는 시점을 더 자세히 파악하려면 Telemetry API를 사용해 확장용 실시간 텔레메트리 데이터 액세스를 사용할 수 있어요. Telemetry API는 invoke 단계 사이에 억제된 init가 발생할 때마다 phase=invoke 인 INIT_START, INIT_RUNTIME_DONE, INIT_REPORT 이벤트를 내보내요.
Shutdown 단계 (Shutdown phase)
Lambda가 런타임을 종료하려고 할 때 등록된 각 외부 확장에 Shutdown 이벤트를 보내요. 확장은 이 시간을 사용해 최종 정리 작업을 수행할 수 있어요. Shutdown 이벤트는 Next API 요청에 대한 응답이에요.
지속 시간 제한 : Shutdown 단계의 최대 지속 시간은 등록된 확장의 구성에 따라 달라져요.
- 0 ms — 등록된 확장이 없는 함수
- 500 ms — 등록된 내부 확장이 있는 함수
- 2,000 ms — 외부 확장이 하나 이상 등록된 함수
런타임이나 확장이 제한 시간 내에 Shutdown 이벤트에 응답하지 않으면 Lambda는 SIGKILL 신호로 프로세스를 종료해요.
함수와 모든 확장이 완료된 후 Lambda는 또 다른 함수 호출을 예상해 실행 환경을 잠시 유지해요. 그러나 Lambda는 런타임 업데이트와 유지 관리를 허용하기 위해 — 지속적으로 호출되는 함수라도 — 몇 시간마다 실행 환경을 종료해요. 실행 환경이 무기한 유지될 것이라고 가정하면 안 돼요. 자세한 내용은 함수에서 상태 비저장(statelessness) 구현을 참고하세요.
함수가 다시 호출되면 Lambda는 환경을 해동(thaw)해 재사용해요. 실행 환경을 재사용하면 다음과 같은 의미가 있어요.
- 함수의 핸들러 메서드 밖에서 선언된 객체는 초기화된 상태로 유지되어 함수가 다시 호출될 때 추가 최적화를 제공해요. 예를 들어 Lambda 함수가 데이터베이스 연결을 설정하면 연결을 다시 설정하는 대신 원래 연결이 후속 호출에서 사용돼요. 새 연결을 만들기 전에 연결이 이미 존재하는지 확인하는 로직을 코드에 추가하는 것을 권장해요.
- 각 실행 환경은
/tmp디렉터리에 512MB에서 10,240MB(1MB 단위)의 디스크 공간을 제공해요. 실행 환경이 정지될 때 디렉터리 내용이 유지되어 여러 호출에 사용할 수 있는 임시 캐시를 제공해요. 캐시에 저장한 데이터가 있는지 확인하는 추가 코드를 추가할 수 있어요. 배포 크기 제한에 대한 자세한 내용은 Lambda 할당량을 참고하세요. - Lambda 함수가 시작했고 함수 종료 시 완료되지 않은 백그라운드 프로세스나 콜백은 Lambda가 실행 환경을 재사용하면 다시 시작돼요. 코드가 종료되기 전에 코드의 백그라운드 프로세스나 콜백이 완료되었는지 확인하세요.
콜드 스타트와 지연 시간 (Cold starts and latency)
Lambda가 Lambda API를 통해 함수 실행 요청을 받으면 서비스는 먼저 실행 환경을 준비해요. 이 초기화 단계에서 서비스는 코드를 다운로드하고, 환경을 시작하며, 기본 핸들러 밖의 초기화 코드를 실행해요. 마지막으로 Lambda는 핸들러 코드를 실행해요.
코드 다운로드와 환경 설정이라는 처음 두 단계를 흔히 "콜드 스타트" 라고 해요. 이 시간에 대해 요금이 부과되며, 전체 호출 지속 시간에 지연 시간을 더해요.
호출이 완료된 후 실행 환경이 정지돼요. 리소스 관리와 성능을 개선하기 위해 Lambda는 실행 환경을 일정 기간 유지해요. 이 기간 동안 같은 함수에 대한 다른 요청이 도착하면 Lambda는 환경을 재사용할 수 있어요. 실행 환경이 이미 완전히 설정되어 있으므로 두 번째 요청은 일반적으로 더 빨리 완료돼요. 이것을 "웜 스타트" 라고 해요.
콜드 스타트는 일반적으로 호출의 1% 미만에서 발생해요. 콜드 스타트 지속 시간은 100ms 미만에서 1초 이상까지 다양해요. 일반적으로 콜드 스타트는 프로덕션 워크로드보다 개발 및 테스트 함수에서 더 흔해요. 개발 및 테스트 함수는 일반적으로 호출 빈도가 낮기 때문이에요.
프로비저닝된 동시성으로 콜드 스타트 줄이기 (Reducing cold starts with Provisioned Concurrency)
워크로드에 예측 가능한 함수 시작 시간이 필요하면 프로비저닝된 동시성이 가장 낮은 지연 시간을 보장하는 권장 솔루션이에요. 이 기능은 실행 환경을 미리 초기화하여 콜드 스타트를 줄여요.
예를 들어 프로비저닝된 동시성이 6인 함수는 6개의 실행 환경이 미리 워밍되어 있어요.
정적 초기화 최적화 (Optimizing static initialization)
정적 초기화는 핸들러 코드가 함수에서 실행을 시작하기 전에 발생해요. 이것은 기본 핸들러 밖에 있는, 여러분이 제공하는 초기화 코드예요. 이 코드는 라이브러리와 종속성을 import 하고, 구성을 설정하며, 다른 서비스에 대한 연결을 초기화하는 데 자주 사용돼요.
다음 Python 예제는 invoke 중 lambda_handler 함수가 실행되기 전에 초기화 단계에서 import, 모듈 구성, Amazon S3 클라이언트 생성을 보여줘요.
import os
import json
import cv2
import logging
import boto3
s3 = boto3.client('s3')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# Handler logic...
함수 실행 전 지연 시간의 가장 큰 원인은 초기화 코드에서 발생해요. 이 코드는 새 실행 환경이 처음 생성될 때 실행돼요. 호출이 웜 실행 환경을 사용하면 초기화 코드는 다시 실행되지 않아요. 초기화 코드 지연 시간에 영향을 주는 요인은 다음과 같아요.
- import 한 라이브러리, 종속성, Lambda 레이어 측면의 함수 패키지 크기
- 코드와 초기화 작업의 양
- 연결 및 기타 리소스 설정에 있어 라이브러리와 다른 서비스의 성능
정적 초기화 지연 시간을 최적화하기 위해 개발자가 취할 수 있는 여러 단계가 있어요. 함수에 객체와 연결이 많으면 단일 함수를 여러 개의 특화된 함수로 재구성할 수 있어요. 각 함수는 개별적으로 더 작고 초기화 코드도 더 적어요.
함수는 필요한 라이브러리와 종속성만 import 하는 것이 중요해요. 예를 들어 AWS SDK에서 Amazon DynamoDB만 사용한다면 전체 SDK 대신 개별 서비스를 require 할 수 있어요.
// const AWS = require('aws-sdk') 대신:
const DynamoDB = require('aws-sdk/clients/dynamodb')
// const AWSXRay = require('aws-xray-sdk') 대신:
const AWSXRay = require('aws-xray-sdk-core')
// const AWS = AWSXRay.captureAWS(require('aws-sdk')) 대신:
const dynamodb = new DynamoDB.DocumentClient()
AWSXRay.captureAWSClient(dynamodb.service)
정적 초기화는 함수가 동일한 실행 환경에 대한 여러 호출에서 연결을 재사용할 수 있도록 데이터베이스 연결을 여는 가장 좋은 위치이기도 해요. 그러나 함수의 특정 실행 경로에서만 사용되는 객체가 많을 수도 있어요. 이 경우 전역 범위에서 변수를 지연 로드(lazy load)해 정적 초기화 지속 시간을 줄일 수 있어요.
컨텍스트별 정보에 전역 변수를 사용하지 마세요. 함수에 단일 호출 수명 동안만 사용되고 다음 호출을 위해 재설정되는 전역 변수가 있다면 핸들러에 로컬인 변수 범위를 사용해요. 이렇게 하면 호출 간 전역 변수 누수를 방지할 뿐만 아니라 정적 초기화 성능도 개선돼요.