Published on

AWS Lambda Python 콜드 스타트가 갑자기 2~5초로 늘어날 때 컨테이너 이미지 레이어 의존성 import 병목과 Provisioned Concurrency로 80% 줄이는 실전 가이드

Authors

서버리스는 “평소엔 빠른데 어느 날 갑자기 느려지는” 순간이 가장 치명적입니다. 특히 Python Lambda는 평소 p50이 100300ms 수준이더라도, 배포 이후 또는 트래픽 패턴이 바뀐 뒤 콜드 스타트가 25초로 튀면 사용자 경험이 바로 무너집니다. 더 곤란한 점은 CloudWatch에서 Duration만 보면 원인이 뭉뚱그려져 보이고, “Lambda가 느려졌다”로 끝나버리기 쉽다는 것입니다.

이 글은 콜드 스타트가 갑자기 늘어났을 때 컨테이너 이미지/레이어 용량, 의존성(NumPy·Pydantic·Boto3) import 병목, 런타임 초기화(Init) 단계, 그리고 **Provisioned Concurrency(PC)**까지 한 번에 정리해 “재현 → 측정 → 제거 → 완화” 순서로 실전 최적화 가이드를 제공합니다.

1) 먼저 확인할 것 콜드 스타트가 진짜 원인인가

Lambda 지연은 크게 3가지로 나뉩니다.

  1. Init(초기화) 지연: 런타임 부팅, 코드/레이어/이미지 로드, 모듈 import, 글로벌 초기화 수행
  2. Invoke(실행) 지연: 핸들러 로직, 네트워크 호출, DB/API, JSON 직렬화 등
  3. 외부 요인: VPC ENI 부착, DNS, NAT, 타임아웃 재시도, 다운스트림 병목

콜드 스타트 여부는 CloudWatch Logs에서 아래 패턴으로 빠르게 구분할 수 있습니다.

  • Zip/Layer 런타임: REPORT ... Init Duration: xxx ms가 찍히는지
  • 컨테이너 이미지: Init Duration이 나오기도 하지만, 환경에 따라 로그 형태가 다를 수 있어 AWS X-Ray/EMF로 함께 보는 것이 좋습니다.

CloudWatch Logs Insights로 콜드 스타트만 집계

fields @timestamp, @message
| filter @message like /REPORT/
| parse @message /Init Duration: (?<init>[^ ]+) ms/ 
| stats
    count(*) as invocations,
    pct(init, 50) as p50_init,
    pct(init, 90) as p90_init,
    pct(init, 99) as p99_init
  • Init Duration이 갑자기 증가했다면 import/초기화/패키지 로딩 문제일 확률이 큽니다.
  • Init Duration은 그대로인데 Duration만 늘었다면 핸들러 내부 로직/외부 호출을 의심해야 합니다.

2) 콜드 스타트가 갑자기 늘어나는 대표 트리거

2.1 컨테이너 이미지로 전환했거나, 이미지가 커졌을 때

ECR 기반 컨테이너 이미지 Lambda는 콜드 스타트 시점에 이미지 레이어를 가져오고(캐시 여부에 따라), 파일시스템을 준비합니다. 다음이 자주 원인입니다.

  • base image 변경(예: python:3.11public.ecr.aws/lambda/python:3.11로 바꾸면서 의존성 설치 방식 변화)
  • pip install 결과물 증가(NumPy, SciPy, Pandas, cryptography 등)
  • 불필요한 빌드 아티팩트 포함(tests/, .pyc, __pycache__, docs)
  • 멀티스테이지 빌드 미사용으로 빌드 도구(gcc, make)가 그대로 들어감

2.2 Lambda Layer를 추가했거나 레이어가 비대해졌을 때

레이어는 편하지만, “공유”가 곧 “비만”이 되기 쉽습니다.

  • 모든 함수가 쓰지 않는 공용 레이어에 NumPy/Pydantic/Boto3를 다 넣음
  • 레이어에 중복 패키지(함수 코드에도 있고 레이어에도 있음)
  • 레이어 버전이 늘어나면서 실제로는 더 큰 의존성 세트를 로딩

2.3 의존성 import 비용이 폭발하는 순간들

  • NumPy: 바이너리 로딩 + 내부 초기화가 무겁고, 환경에 따라 로딩 시간이 크게 흔들립니다.
  • Pydantic: v1은 모델 생성/검증이 런타임 비용이 크고, v2는 개선됐지만 여전히 import 체인이 길면 초기화에 영향을 줍니다.
  • Boto3: “AWS SDK니까 기본으로 있어도 되지”라는 착각이 잦습니다. boto3는 import 체인이 길고(봇코어 포함), 실제로는 많은 모듈을 끌고 들어옵니다.

3) 측정이 먼저다 import 병목을 숫자로 쪼개기

3.1 가장 빠른 1차 진단 import 타임 로그 찍기

핸들러 파일 최상단에서 “어떤 import가 얼마나 걸리는지”를 직접 찍어보면, 의외로 10분 안에 범인이 나옵니다.

# app.py
import time
_t0 = time.perf_counter()

import json
_t1 = time.perf_counter()

import boto3
_t2 = time.perf_counter()

import numpy as np
_t3 = time.perf_counter()

from pydantic import BaseModel
_t4 = time.perf_counter()

print({
    "import_json_ms": round((_t1-_t0)*1000, 2),
    "import_boto3_ms": round((_t2-_t1)*1000, 2),
    "import_numpy_ms": round((_t3-_t2)*1000, 2),
    "import_pydantic_ms": round((_t4-_t3)*1000, 2),
    "import_total_ms": round((_t4-_t0)*1000, 2),
})


def handler(event, context):
    return {"ok": True}
  • 이 로그는 콜드 스타트에서만 의미가 큽니다.
  • 배포 직후 몇 번 호출해서 평균/분산을 보세요.

3.2 정밀 진단 Python import 프로파일링

컨테이너 이미지라면 python -X importtime로 빌드 단계에서 import 비용을 확인해볼 수 있습니다.

# Dockerfile (진단용)
RUN python -X importtime -c "import boto3, numpy, pydantic" 2> /tmp/importtime.log \
 && tail -n 50 /tmp/importtime.log
  • 실제 Lambda 런타임과 100% 동일하진 않지만, import 체인/병목 모듈을 찾는 데 충분히 유용합니다.

4) 원인별 최적화 레시피

4.1 컨테이너 이미지 최적화 레이어와 용량 줄이기

멀티스테이지 빌드로 빌드 도구 제거

# syntax=docker/dockerfile:1

FROM public.ecr.aws/lambda/python:3.11 AS runtime

# (선택) 빌드 스테이지를 따로 두고 wheel만 옮기는 방식 추천
FROM public.ecr.aws/lambda/python:3.11 AS build

RUN python -m pip install --upgrade pip

COPY requirements.txt .
RUN pip wheel --no-cache-dir -r requirements.txt -w /wheels

FROM runtime
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* \
 && rm -rf /wheels

COPY app.py ${LAMBDA_TASK_ROOT}
CMD ["app.handler"]

핵심은:

  • pip wheel로 빌드 산출물을 고정
  • --no-cache-dir로 캐시 제거
  • build stage의 gcc/헤더 등을 runtime에 남기지 않기

불필요 파일 제거

컨테이너 이미지/레이어 어디에든 아래가 섞이면 로딩이 느려지고 용량이 늘어납니다.

  • tests/, *.dist-info 중 불필요한 문서
  • __pycache__/, *.pyc

다만 dist-info를 무작정 지우면 패키지 메타데이터가 깨질 수 있으니, 제거는 신중하게 하세요.

4.2 Layer 전략 공용 레이어는 얇게, 무거운 건 함수별로

실무에서 가장 많이 보는 안티패턴은 “공용 레이어 하나에 다 넣기”입니다.

  • Pydantic/NumPy 같은 무거운 의존성은 사용하는 함수에만 포함
  • 여러 함수가 공유해야 한다면 레이어를 기능별로 분리
    • 예: layer-observability, layer-aws-sdk-extensions, layer-ml-numpy
  • 레이어와 함수 코드에 중복 설치 금지(둘 다에 boto3 들어가면 로딩/충돌 리스크)

4.3 의존성별 최적화 포인트

NumPy 꼭 필요한가부터 다시 확인

  • 단순 통계/집계라면 Python 표준 라이브러리나 경량 라이브러리로 대체 가능할 때가 많습니다.
  • 정말 필요하다면:
    • 함수 핸들러가 항상 NumPy를 쓰는지 확인
    • 일부 요청에서만 쓰면 지연 import로 콜드 스타트/웜 스타트 모두 최적화 가능
# 지연 import 예시

def handler(event, context):
    if event.get("need_numpy"):
        import numpy as np
        return {"sum": float(np.sum([1, 2, 3]))}
    return {"ok": True}

지연 import는 만능은 아닙니다. “첫 NumPy 요청”이 느려질 수 있으니, 트래픽 패턴에 따라 결정하세요.

Pydantic 모델 생성 비용 줄이기

  • 가능한 경우 Pydantic v2로 업그레이드(검증/직렬화 성능 개선)
  • 모델을 요청마다 동적으로 만들지 말고 모듈 전역에 고정
  • 불필요한 필드/복잡한 validator 남발 금지
from pydantic import BaseModel, Field

class Input(BaseModel):
    user_id: str = Field(min_length=1)
    limit: int = Field(default=10, ge=1, le=100)


def handler(event, context):
    data = Input.model_validate(event)  # v2
    return {"user_id": data.user_id, "limit": data.limit}

Boto3 import 및 클라이언트 생성 최적화

  • boto3 import 자체가 무거우므로, 사용하지 않는 함수에는 포함하지 않기
  • 클라이언트 생성은 전역 캐시(재사용) 권장
import boto3

_s3 = boto3.client("s3")


def handler(event, context):
    # 웜 스타트에서는 _s3 재사용
    resp = _s3.list_buckets()
    return {"count": len(resp.get("Buckets", []))}

추가로, AWS SDK 호출이 느려졌다면 네트워크/재시도 정책도 함께 봐야 합니다. 외부 API 레이트리밋/재시도 설계는 Lambda 지연과 비용을 동시에 폭증시키므로, 재시도 전략은 별도로 점검하는 게 좋습니다. (참고: OpenAI API 429 폭탄 대응 실전 가이드 지수 백오프 큐잉 토큰 버짓으로 비용과 지연을 함께 줄이기)

5) Provisioned Concurrency로 체감 지연 80% 줄이기

콜드 스타트를 “없애는” 가장 확실한 방법은 Provisioned Concurrency(PC) 입니다. PC는 지정한 동시성만큼 실행 환경을 미리 띄워두고 초기화까지 끝내기 때문에, 사용자 요청이 바로 웜 환경으로 들어옵니다.

언제 PC가 정답인가

  • p95/p99 지연이 SLA에 치명적일 때
  • 콜드 스타트가 2~5초 이상으로 불규칙할 때
  • 트래픽이 예측 가능(업무시간 피크, 배치 직후 등)하거나, 최소 동시성을 상시 유지할 가치가 있을 때

PC 적용 절차 체크리스트

  1. Alias 기반 배포로 운영 트래픽을 별칭으로 받기
  2. Alias에 Provisioned Concurrency 설정
  3. 오토스케일링이 필요하면 Application Auto Scaling으로 PC를 스케줄/타겟 추적
  4. 배포 시점에 PC 워밍업이 깨지지 않도록(새 버전 publish 후 alias 전환) 파이프라인 구성

실전 팁 PC만으로 끝내지 말고 Init도 줄여라

PC는 비용을 지불하고 지연을 사는 구조입니다. Init을 30~50% 줄이면 같은 성능을 더 적은 PC로 달성할 수 있습니다.

  • 무거운 import 제거/지연 import
  • 이미지/레이어 슬림화
  • 전역 초기화에서 네트워크 호출 금지(Secrets Manager/SSM 호출을 init에 넣는 실수 빈번)

6) 트러블슈팅 자주 나오는 함정들

6.1 메모리 올렸더니 빨라지기도 느려지기도 한다

Lambda는 메모리 증가에 따라 CPU도 비례 증가합니다. import/압축해제/파싱이 CPU 바운드라면 메모리 증설이 곧 성능 개선입니다.

  • 콜드 스타트가 CPU 바운드(대형 모듈 import)면 메모리 상향이 즉효일 수 있음
  • 하지만 이미지/레이어가 너무 크면 네트워크/IO 영향이 커져 기대만큼 안 줄어들 수 있음

6.2 VPC 붙였더니 콜드 스타트가 늘었다

요즘은 과거보다 개선됐지만, 여전히 VPC 구성(NAT, DNS, 서브넷, 보안그룹)과 함께 보면 초기 연결이 느려지는 케이스가 있습니다. 특히 외부 통신이 많으면 NAT 병목/포트 고갈 같은 2차 문제가 동반될 수 있습니다.

DB 연결 폭주로 지연이 커지는 패턴도 흔합니다. Lambda가 느린 게 아니라 DB 커넥션이 막혀서 전체가 느려지는 경우죠. 이때는 RDS Proxy/pgBouncer 같은 연결 관리가 더 큰 효과를 냅니다. (참고: Aurora PostgreSQL remaining connection slots are reserved로 서비스가 멈출 때 RDS Proxy와 pgBouncer와 max_connections 튜닝으로 커넥션 폭주를 영구 차단하는 실전 체크리스트)

6.3 비동기 코드 경고와 함께 지연이 늘어난다

asyncio를 잘못 종료하면 이벤트 루프에 태스크가 남아 경고가 나고, 리소스 누수/예상치 못한 지연으로 이어질 수 있습니다. Lambda에서 async를 쓴다면 종료/취소 처리를 점검하세요. (참고: Python asyncio Task was destroyed but it is pending 경고 원인 5가지와 완벽 해결법)

7) Best Practice 요약 실무에서 바로 쓰는 체크리스트

7.1 패키징/의존성

  • 컨테이너 이미지: 멀티스테이지 + wheel 빌드 + 캐시 제거
  • 레이어: 공용 레이어를 얇게, 무거운 의존성은 분리
  • 중복 설치 제거(함수 코드 vs 레이어 vs 이미지)

7.2 import/초기화

  • import 타임을 로그로 수치화하고 상위 3개 병목부터 제거
  • 전역 초기화에서 네트워크 호출 금지(Secrets/SSM/외부 API)
  • boto3 클라이언트는 전역 재사용
  • NumPy/Pydantic은 “정말 필요한 곳”에만, 가능하면 지연 import

7.3 운영 전략

  • p95/p99가 중요하면 Provisioned Concurrency를 기본 옵션으로 고려
  • PC 비용을 낮추려면 Init 자체를 줄여 최소 PC로 목표 지연 달성
  • 배포 파이프라인에서 alias + PC 워밍업까지 포함

결론

Python Lambda 콜드 스타트가 갑자기 2~5초로 늘어날 때는 감으로 튜닝하면 끝이 없습니다. Init Duration을 분리해 측정하고, (1) 컨테이너 이미지/레이어 용량을 슬림화하고, (2) NumPy·Pydantic·Boto3 import 병목을 제거/지연하고, (3) SLA가 빡빡하면 Provisioned Concurrency로 콜드 스타트를 구조적으로 차단하는 순서로 접근하면, 체감 지연을 80% 수준까지 줄이는 것은 충분히 현실적입니다.

지금 운영 중인 함수 하나를 골라서 오늘 바로 해보세요.

  1. Logs Insights로 Init Duration p95를 뽑고 → 2) import 타임 상위 3개를 찾고 → 3) 이미지/레이어/의존성 중 하나를 줄인 뒤 → 4) 필요한 경우 PC를 걸어 p99를 안정화하세요.

이 4단계만 반복해도 “어느 날 갑자기 느려지는 Lambda”에서 벗어날 수 있습니다.