- Published on
SageMaker 서버리스 504 해결 - 콜드스타트·타임아웃
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스 추론을 붙이고 나서 가장 흔하게 만나는 장애가 504 입니다. 특히 트래픽이 뜸하거나 배포 직후 첫 요청에서만 터지면 90%는 콜드스타트 계열 문제입니다. 문제는 504라는 결과만 보고는 어디에서 시간이 소모됐는지 감이 안 온다는 점입니다. 이 글은 SageMaker Serverless Inference에서 504가 발생하는 경로를 나눠서 진단하고, 콜드스타트와 타임아웃을 현실적으로 줄이는 방법을 정리합니다.
참고로 서버리스는 "항상 켜진 엔드포인트"가 아니라, 요청이 들어올 때 컨테이너를 띄우고 모델을 메모리에 올린 뒤 추론을 수행합니다. 즉, 지연시간은 대략 아래 합으로 생각하면 됩니다.
- 컨테이너 준비 시간(이미지 pull, 런타임 부팅)
- 애플리케이션 초기화(의존성 import, 모델 파일 다운로드, 모델 로딩)
- 추론 실행 시간
- 응답 직렬화 및 네트워크 왕복
504는 어디서 발생하나: 타임아웃 체인부터 확인
504 Gateway Timeout은 "앞단 게이트웨이가 뒤의 응답을 제시간에 못 받았다"는 뜻입니다. SageMaker Serverless를 호출하는 경로에 따라 타임아웃 한계가 달라집니다.
자주 쓰는 호출 경로별 체크포인트
- 클라이언트
->API Gateway->Lambda->SageMaker Runtime - 클라이언트
->ALB->백엔드->SageMaker Runtime - 백엔드
->SageMaker Runtime 직접 호출
여기서 앞단이 API Gateway라면, API Gateway 자체의 최대 통합 타임아웃(일반적으로 29초)이 상한이 됩니다. 즉, SageMaker 쪽에서 40초에 응답해도 API Gateway는 먼저 504를 반환합니다. 반대로 백엔드가 직접 InvokeEndpoint를 호출한다면, SDK의 읽기 타임아웃과 서버리스의 내부 제한이 상한이 됩니다.
따라서 첫 단계는 "누가 504를 내고 있는가"를 로그로 분리하는 것입니다.
- API Gateway 액세스 로그에서
integrationLatency가 상한에 붙어있나 - Lambda 로그에서 SageMaker 호출이 타임아웃 났나
- CloudWatch에서 SageMaker Serverless의 invocation 로그(컨테이너 로그) 자체가 남았나
컨테이너 로그가 아예 없다면, 컨테이너 준비 단계에서 시간이 초과됐거나 앞단에서 먼저 끊은 경우가 많습니다.
콜드스타트가 길어지는 6가지 실전 원인
서버리스의 콜드스타트는 "컨테이너 뜸" 하나로 끝나지 않고, 보통 아래 원인들이 겹칩니다.
1) 이미지가 크고 레이어가 많다
대형 베이스 이미지, CUDA 포함, 불필요한 빌드 도구 포함 등은 pull과 부팅 시간을 늘립니다.
- 불필요한 패키지 제거
- 멀티 스테이지 빌드로 런타임만 남기기
- 가능하면 CPU 전용 경량 이미지 사용
2) 첫 요청에서 모델을 S3에서 내려받는다
컨테이너 시작 시점에 S3에서 모델 아티팩트를 다운로드하면 네트워크 및 압축 해제 시간이 그대로 콜드스타트에 포함됩니다.
- 모델을 이미지에 포함(가능한 경우)
- 또는
/opt/ml/model에 빠르게 풀리도록 압축 포맷과 구조 최적화
3) 프레임워크 import와 그래프 초기화가 무겁다
PyTorch, Transformers, tokenizers 초기화 자체가 수 초에서 수십 초까지 튈 수 있습니다.
- 프로세스 시작 시 1회만 로딩하고 재사용
- 불필요한 모듈 import 지연 로딩
4) 동시성 스파이크로 초기 컨테이너가 여러 개 뜬다
서버리스는 요청이 늘면 컨테이너를 더 띄웁니다. 초기에 한 번에 여러 개가 뜨면 콜드스타트가 병렬로 발생해 504가 더 잘 보입니다.
5) 타임아웃 설정이 앞단과 불일치
앞단이 29초 제한인데, 서버리스가 60초 안에만 응답하면 된다고 생각하면 계속 504를 봅니다.
6) 페이로드/응답이 크다
입력 JSON이 크거나, base64 이미지/오디오를 그대로 보내면 직렬화와 네트워크 시간이 크게 늘고, Lambda나 게이트웨이 제한에도 걸립니다.
관측부터: 무엇이 느린지 로그로 쪼개기
서버리스 최적화는 "추측"보다 "분해"가 먼저입니다. 컨테이너 로그에 단계별 시간을 찍어두면, 콜드스타트인지 추론 자체인지 바로 갈립니다.
Python 핸들러에 단계별 타이밍 로그 넣기
아래는 SageMaker inference toolkit 스타일을 가정한 단순 예시입니다. 핵심은 모델 로딩과 추론 시간을 분리해 CloudWatch에서 비교하는 것입니다.
import json
import time
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
_model = None
_tokenizer = None
def model_fn(model_dir: str):
global _model, _tokenizer
t0 = time.perf_counter()
# 예: 모델/토크나이저 로딩
# _tokenizer = AutoTokenizer.from_pretrained(model_dir)
# _model = AutoModelForSequenceClassification.from_pretrained(model_dir)
t1 = time.perf_counter()
logger.info(json.dumps({
"event": "model_loaded",
"elapsed_ms": int((t1 - t0) * 1000)
}))
return {"model": _model, "tokenizer": _tokenizer}
def predict_fn(data, model_bundle):
t0 = time.perf_counter()
# 예: 전처리/추론
# inputs = model_bundle["tokenizer"](...)
# outputs = model_bundle["model"](**inputs)
t1 = time.perf_counter()
logger.info(json.dumps({
"event": "inference_done",
"elapsed_ms": int((t1 - t0) * 1000)
}))
return {"ok": True}
이 로그가 남는다면 컨테이너는 뜬 것입니다. 그런데도 호출자가 504를 받는다면 "앞단 타임아웃" 또는 "응답 전송" 문제일 확률이 큽니다.
해결 전략 1: 프로비저닝 동시성으로 콜드스타트 제거
SageMaker Serverless Inference는 프로비저닝 동시성(Provisioned Concurrency에 준하는 개념)을 통해 "항상 준비된" 인스턴스를 일정 수 유지할 수 있습니다. 비용이 늘지만, 첫 요청 504를 가장 확실하게 없애는 방법입니다.
- 트래픽이 하루에 몇 번만 들어와도, 첫 요청이 중요한 서비스라면 강력 추천
- 동시성 1만 잡아도 "첫 요청" 문제는 대부분 사라짐
boto3로 엔드포인트 설정 예시
아래는 개념 예시입니다. 실제 파라미터는 배포 방식과 리전, SDK 버전에 따라 달라질 수 있으니 문서 기준으로 확인하세요.
import boto3
sm = boto3.client("sagemaker")
endpoint_config_name = "my-epc"
sm.create_endpoint_config(
EndpointConfigName=endpoint_config_name,
ProductionVariants=[
{
"VariantName": "AllTraffic",
"ModelName": "my-model",
"ServerlessConfig": {
"MemorySizeInMB": 4096,
"MaxConcurrency": 10,
# 일부 환경에서는 여기에 프로비저닝/프로비저닝된 동시성 설정을 추가
},
}
],
)
프로비저닝 동시성을 쓸 수 없는 상황이라면, 다음 전략(초기화/모델 로딩 자체를 줄이기)로 콜드스타트 시간을 깎아야 합니다.
해결 전략 2: 모델 로딩과 이미지 크기 줄이기(가장 효과적인 최적화)
콜드스타트의 대부분은 "다운로드 + 압축해제 + 로딩"입니다. 아래 체크리스트로 접근하면 체감 효과가 큽니다.
Docker 이미지 경량화 예시(멀티 스테이지)
불필요한 빌드 도구를 런타임 이미지에 남기지 않는 것만으로도 pull 시간이 줄어듭니다.
# build stage
FROM python:3.11-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip wheel -r requirements.txt -w /wheels
# runtime stage
FROM python:3.11-slim
WORKDIR /app
COPY /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
CMD ["python", "serve.py"]
모델 아티팩트 구조 최적화 팁
- 작은 파일이 수천 개면 압축해제와 파일 I/O가 느려집니다
- 가능하면 파일 수를 줄이거나, 로딩 시 필요한 것만 풀리도록 구성
- 토크나이저/사전 파일도 크면 병목이 되므로 최소화
메모리를 올려서 오히려 빨라지는 경우
서버리스는 메모리 사이즈가 커질수록 CPU 자원이 같이 늘어나는 형태인 경우가 많습니다. 메모리를 올리면 비용은 오르지만, 초기화와 추론이 빨라져 전체 비용이 내려갈 때도 있습니다(짧게 끝나니까).
MemorySizeInMB를 한 단계씩 올려서p95지연시간과 비용을 같이 비교
해결 전략 3: 앞단 타임아웃 회피(비동기/큐 기반)
API Gateway 29초 같은 상한이 있는 구조라면, 서버리스 최적화만으로는 한계가 있습니다. 이때는 동기 호출을 포기하고 비동기 패턴으로 바꾸는 것이 정석입니다.
패턴 A: 요청 접수는 즉시 202로 반환, 결과는 폴링
- 클라이언트
->API: 작업 생성 - API
->SQS 또는 DynamoDB에 job 저장 - 워커가 SageMaker 호출 후 결과 저장
- 클라이언트는
GET /jobs/{id}로 확인
패턴 B: SageMaker Asynchronous Inference 고려
서버리스가 아니라도, "응답을 오래 기다리는" 문제 자체를 구조적으로 해결합니다.
해결 전략 4: 동시성 스파이크 제어와 워밍업
프로비저닝 동시성이 없거나 최소화해야 한다면, 워밍업을 "운영"으로 해결하는 방법도 있습니다.
- 정기적으로 가벼운 요청을 보내 컨테이너를 유지(완벽하진 않음)
- 배포 직후 트래픽을 천천히 올리는 램프업
- 클라이언트에서 재시도 정책을 둬서 첫 요청 실패를 흡수
지수 백오프 재시도 예시(boto3 config)
네트워크/일시적 타임아웃은 재시도로 완화되는 경우가 많습니다.
import boto3
from botocore.config import Config
config = Config(
retries={"max_attempts": 5, "mode": "standard"},
read_timeout=60,
connect_timeout=5,
)
runtime = boto3.client("sagemaker-runtime", config=config)
resp = runtime.invoke_endpoint(
EndpointName="my-endpoint",
ContentType="application/json",
Body=b"{\"text\": \"hello\"}",
)
print(resp["Body"].read())
주의할 점은 "앞단이 API Gateway"인 경우, 백엔드에서 아무리 read_timeout을 늘려도 게이트웨이 상한을 넘기면 결국 504가 난다는 것입니다. 이 경우는 구조 변경이 필요합니다.
해결 전략 5: 페이로드를 줄이고 직렬화 병목 제거
서버리스에서 의외로 자주 보는 병목이 JSON 직렬화/역직렬화입니다.
- 입력은 텍스트만 보내고, 바이너리는 S3 프리사인 URL로 전달
- 응답도 큰 배열을 그대로 반환하지 말고 요약/압축/저장 후 URL 반환
운영 체크리스트: 재발 방지를 위한 최소 세트
아래 6가지를 갖추면 504는 "가끔"이 아니라 "관리 가능한 이벤트"가 됩니다.
- CloudWatch에서
p50/p95/p99지연시간 대시보드 - 컨테이너 로그에
model_loaded,inference_done같은 단계 로그 - 앞단(API Gateway, Lambda, ALB)의 타임아웃 상한 문서화
- 모델 아티팩트 크기, 이미지 크기, 로딩 시간의 회귀 테스트
- 재시도 정책(단, 멱등성 보장 필요)
- 트래픽 패턴에 맞춘 프로비저닝 동시성 또는 비동기 전환
흔한 오해 3가지
오해 1: "서버리스니까 첫 요청도 빨라야 한다"
서버리스는 유휴 시 리소스를 내립니다. 첫 요청이 느려지는 것은 정상 동작이며, 이를 없애려면 비용을 내고 준비된 용량을 사야 합니다.
오해 2: "메모리를 줄이면 비용이 줄어든다"
메모리를 줄이면 시간이 늘어 총 비용이 비슷하거나 더 비싸질 수 있습니다. 반드시 "시간 x 단가" 관점으로 봐야 합니다.
오해 3: "504면 SageMaker가 죽었다"
실제로는 앞단 제한(API Gateway 29초)이나 Lambda 타임아웃인 경우가 많습니다. 504의 발급 주체부터 분리하세요.
마무리: 504를 없애는 가장 현실적인 조합
- 첫 요청이 반드시 성공해야 한다: 프로비저닝 동시성
1이상 + 이미지/모델 경량화 - 30초 이상 걸릴 수 있다: 비동기 패턴으로 전환(큐/잡) + 결과 폴링
- 트래픽이 들쭉날쭉하다: 경량화 + 재시도 + 램프업(배포 직후)
추론 인프라에서 타임아웃은 "코드"만의 문제가 아니라 "경로"의 문제입니다. 네트워크 앞단의 상한을 먼저 고정하고, 그 안에서 콜드스타트와 모델 로딩을 줄이는 순서로 접근하면 504는 대부분 정리됩니다.
추가로, 운영 환경에서 원인 추적을 빠르게 하려면 로그 기반 디버깅 습관이 중요합니다. DB나 런타임 이슈를 로그로 쪼개는 접근은 다른 장애에서도 그대로 통합니다. 예를 들어 MySQL InnoDB 데드락(1213) 로그로 범인 찾기처럼 "증상 -> 로그 -> 재현 조건"으로 분해하는 방식이 서버리스 타임아웃에도 잘 맞습니다. 또한 배포 파이프라인에서 이미지 캐시가 깨져 pull 시간이 늘면 콜드스타트가 악화될 수 있는데, 이때는 GitHub Actions 캐시가 안 먹을 때 - key·restore-keys·권한 같은 체크리스트가 도움이 됩니다.