Published on

SageMaker 실시간 엔드포인트 504 타임아웃 해결

Authors

SageMaker 실시간(Real-time) 엔드포인트에서 504 타임아웃은 “요청이 엔드포인트까지 도달했지만, 정해진 시간 안에 정상 응답을 못 받았다”는 의미로 보는 게 실무적으로 가장 정확합니다. 문제는 타임아웃이 모델 추론 자체가 느린 경우뿐 아니라, 컨테이너 초기화 지연, 워커 병목, 페이로드 크기, 네트워크 레이어, 오토스케일 이벤트 등 여러 층에서 발생한다는 점입니다.

이 글에서는 504레이어별로 분해해 원인을 빠르게 좁히고, 재발을 막는 설정과 코드 패턴까지 정리합니다.

1) 504가 의미하는 것: 어디에서 끊겼나

SageMaker 실시간 엔드포인트 호출 흐름을 단순화하면 아래와 같습니다.

  • 클라이언트가 엔드포인트 URL로 요청
  • SageMaker 호스팅(프록시/로드밸런싱 계층)이 컨테이너로 전달
  • 컨테이너(모델 서버)가 요청을 받아 전처리-추론-후처리 후 응답

504는 보통 “프록시 계층이 upstream(컨테이너)에서 응답을 제시간에 못 받음”으로 귀결됩니다. 즉, 애플리케이션 레벨에서 500이 나지 않아도, 응답이 늦으면 504로 보일 수 있습니다.

실무에서 특히 많이 보는 케이스는 다음 4가지입니다.

  1. Cold start: 새 인스턴스가 뜨는 동안 모델 로드가 길어 요청이 타임아웃
  2. 동시성 병목: 단일 워커/단일 스레드로 요청이 큐에 쌓여 타임아웃
  3. 추론이 원래 느림: 입력 길이 증가, 배치 미사용, GPU 미사용 등
  4. 페이로드/네트워크 이슈: 큰 입력/출력, 직렬화 지연, 클라이언트 타임아웃

2) 먼저 확인할 지표와 로그(진단 순서)

원인 파악은 “엔드포인트 레벨 지표 → 컨테이너 로그 → 모델/코드 프로파일링” 순서가 가장 빠릅니다.

2.1 CloudWatch 지표에서 병목 찾기

엔드포인트 관련 CloudWatch 지표(대표적으로 아래)를 확인합니다.

  • ModelLatency: 컨테이너가 요청을 처리하는 데 걸린 시간
  • OverheadLatency: SageMaker 호스팅 계층 오버헤드(라우팅/직렬화 등)
  • Invocation4XXErrors, Invocation5XXErrors
  • CPUUtilization, MemoryUtilization, GPU 사용률(가능한 경우)

판단 기준(경험칙):

  • ModelLatency가 높으면 모델/서버 내부 문제일 확률이 큽니다.
  • OverheadLatency가 비정상적으로 높으면 페이로드 크기, 네트워크, 직렬화/압축, 호출 패턴 문제를 의심합니다.

2.2 컨테이너 로그에서 “어디서 멈췄는지” 확인

컨테이너가 아래 중 어디까지 진행했는지 로그로 남기면 원인이 급격히 좁혀집니다.

  • 요청 수신 시각
  • 전처리 시작/끝
  • 추론 시작/끝
  • 후처리/응답 직렬화 끝

SageMaker에서 기본 프레임워크 컨테이너를 쓰든, BYOC(Bring Your Own Container)를 쓰든, 타임스탬프 로그는 가장 값싼 보험입니다.

3) 대표 원인 1: Cold start(모델 로딩)로 인한 504

오토스케일로 인스턴스가 늘거나, 배포 직후 첫 요청이 들어오면 모델 로딩이 길어져 504가 발생할 수 있습니다.

해결 체크리스트

  • 모델 로딩을 프로세스 시작 시점에 끝내고, 요청 핸들러에서는 재사용
  • 큰 모델은 로딩 시 I/O 병목이 생기므로 EBS 볼륨/네트워크 스토리지 속도도 고려
  • min_capacity를 1 이상으로 유지해 완전한 scale-to-zero를 피함(실시간 엔드포인트는 본질적으로 상시 대기형)
  • 워밍업 요청을 배포 파이프라인에 포함

(예시) Flask/FastAPI에서 전역 로딩 패턴

아래는 “요청마다 모델을 로드하는 실수”를 피하기 위한 전형적인 패턴입니다.

# app.py
import time
from fastapi import FastAPI

app = FastAPI()
model = None

@app.on_event("startup")
def load_model():
    global model
    t0 = time.time()
    # 무거운 로딩을 여기서 1회 수행
    model = ...  # load weights
    print({"event": "model_loaded", "sec": time.time() - t0})

@app.post("/invocations")
def predict(payload: dict):
    t0 = time.time()
    # model 재사용
    y = model(payload)
    return {"result": y, "latency_sec": time.time() - t0}

핵심은 “로드는 1회, 요청은 가볍게”입니다.

4) 대표 원인 2: 동시성(워커) 부족으로 큐 적체

504가 간헐적으로 터지고, 트래픽이 조금만 올라가도 재현된다면 대부분 동시성 설정이 문제입니다.

  • 단일 프로세스/단일 워커로 요청을 처리
  • Python GIL 영향으로 CPU 바운드 전처리/후처리에서 막힘
  • 모델 서버가 한 번에 1개 요청만 처리하도록 설정됨

해결 체크리스트

  • 모델 서버를 Gunicorn/Uvicorn worker로 띄우는 경우 worker 수를 늘림
  • 프레임워크별 권장 서버(TorchServe, Triton 등) 사용 고려
  • CPU 바운드 전처리는 멀티프로세싱/네이티브 코드/벡터화로 줄임
  • 가능하면 배치 추론(micro-batching) 도입

(예시) Gunicorn worker 튜닝

아래처럼 worker와 timeout을 조정할 수 있습니다. (단, timeout을 무작정 늘리기보다 병목을 먼저 해결하는 게 정석입니다.)

# Docker CMD 예시
gunicorn app:app \
  --workers 4 \
  --threads 2 \
  --timeout 120 \
  --bind 0.0.0.0:8080

worker를 늘리면 메모리 사용량도 함께 늘 수 있으니 MemoryUtilization을 꼭 같이 봐야 합니다.

5) 대표 원인 3: 추론 자체가 느린데 “타임아웃만” 늘린 경우

504가 항상 같은 입력에서 재현된다면, 모델 연산량이 타임아웃 한계를 넘는 상황일 수 있습니다.

흔한 패턴

  • 입력 토큰/시퀀스 길이가 길어지며 지수적으로 느려짐(특히 디코딩 기반 생성)
  • CPU 인스턴스에 GPU 모델을 올려서 병목
  • FP32로만 추론, 컴파일/최적화 미사용
  • 전처리에서 이미지 리사이즈/디코딩이 병목

해결 체크리스트

  • 더 큰 인스턴스(특히 GPU)로 스케일 업
  • mixed precision(FP16/BF16), quantization 적용
  • 모델 포맷 최적화(예: ONNX/TensorRT 계열) 고려

모델 변환/최적화 과정에서 자주 막힌다면 변환 실패 원인 정리 글이 도움이 됩니다: PyTorch→ONNX→TFLite 변환 실패 8가지 해결

6) 대표 원인 4: 페이로드 크기와 직렬화/역직렬화

SageMaker 실시간 엔드포인트는 대용량 페이로드에서 급격히 불리해질 수 있습니다.

  • base64로 이미지/오디오를 JSON에 넣으면 크기가 커지고 파싱 비용도 증가
  • 응답이 커서 네트워크 전송이 오래 걸림
  • OverheadLatency가 상승

해결 체크리스트

  • 입력/출력을 S3에 두고 엔드포인트에는 URI만 전달
  • JSON 대신 바이너리(application/octet-stream) 또는 더 효율적인 포맷 사용
  • 불필요한 필드 제거, 압축 고려

(예시) S3 URI 기반 요청 패턴

{
  "input_s3_uri": "s3://my-bucket/inputs/123.png",
  "output_s3_prefix": "s3://my-bucket/outputs/"
}

컨테이너는 S3에서 읽고 결과를 S3에 쓰며, 응답은 메타데이터만 반환합니다.

7) 타임아웃은 어디서 조절하나(클라이언트/서버/플랫폼)

504를 줄이려면 “타임아웃을 올리기” 전에 어떤 타임아웃이 걸렸는지를 분리해야 합니다.

  • 클라이언트(SDK/HTTP) 타임아웃
  • 모델 서버(Gunicorn/Uvicorn/TorchServe) 타임아웃
  • SageMaker 호스팅 계층 제한

(예시) boto3 호출 타임아웃 설정

아래는 클라이언트 측에서 read timeout을 늘리는 예시입니다.

import boto3
from botocore.config import Config

cfg = Config(
    connect_timeout=5,
    read_timeout=120,
    retries={"max_attempts": 2}
)

rt = boto3.client("sagemaker-runtime", config=cfg)
resp = rt.invoke_endpoint(
    EndpointName="my-endpoint",
    ContentType="application/json",
    Body=b"{\"text\": \"hello\"}"
)
print(resp["Body"].read())

주의: 클라이언트 타임아웃만 늘리면, 서버 병목이 가려져 장애가 더 크게 번질 수 있습니다.

8) 오토스케일링으로 인한 504 줄이기

오토스케일은 비용 최적화에 유리하지만, 스케일 아웃 시점에 cold start가 섞이면 504가 튈 수 있습니다.

실무 팁

  • 최소 인스턴스 수를 1 또는 트래픽 패턴에 맞게 유지
  • 스케일 아웃 임계값을 너무 빡빡하게 두지 말고 여유를 둠
  • TargetTracking만 믿기보다, 피크 시간대 예약 스케일링(스케줄) 고려
  • 트래픽이 급증하는 서비스라면 큐잉/버퍼링 계층을 두는 게 안정적

API 호출이 몰릴 때 재시도와 큐를 설계하는 관점은 504에도 그대로 적용됩니다. 패턴 참고: OpenAI API 429 RateLimit 재시도와 큐 설계

9) 재시도는 “안전하게” 설계해야 한다

504는 네트워크/타임아웃 계열이라 재시도가 효과적인 경우가 있지만, 무조건 재시도하면 더 큰 장애를 만듭니다.

  • 멱등성 없는 요청(예: 결제, 상태 변경)은 재시도 시 중복 처리 위험
  • 추론 요청은 대체로 멱등이지만, 로그/메트릭/저장 동작이 섞이면 중복 부작용 가능

권장 재시도 정책

  • 지수 백오프 + 지터
  • 최대 재시도 횟수 제한
  • 타임아웃을 재시도 횟수에 비례해 무한정 늘리지 않기
  • 서버가 과부하일 때는 빠르게 실패(fail fast)하고 큐로 넘기기

재시도/큐잉 패턴을 더 넓게 정리한 글도 함께 보면 좋습니다: OpenAI 429/Rate Limit 재시도·큐잉 패턴 7가지

10) 최종 점검 체크리스트(현장에서 바로 쓰는 순서)

  1. CloudWatch에서 ModelLatency vs OverheadLatency로 1차 분류
  2. 컨테이너 로그에 타임스탬프를 넣어 전처리/추론/후처리 중 어디서 지연되는지 확인
  3. Cold start가 의심되면 모델 로딩 1회화, 워밍업, 최소 인스턴스 유지
  4. 동시성 병목이면 worker/threads 조정, 배치 추론, 전처리 최적화
  5. 추론이 느리면 인스턴스 스케일 업, mixed precision/quantization/최적화 포맷 검토
  6. 페이로드가 크면 S3 URI 패턴으로 전환, 직렬화 비용 제거
  7. 클라이언트 타임아웃과 재시도는 “보조 수단”으로만 사용

11) 결론: 504는 증상이고, 병목 레이어를 분리하면 빨리 끝난다

SageMaker 실시간 엔드포인트 504는 단순히 “서버가 느리다”가 아니라, cold start, 동시성, 모델 성능, 페이로드, 오토스케일 같은 서로 다른 문제들이 같은 증상으로 나타난 결과입니다.

가장 효율적인 접근은 ModelLatencyOverheadLatency로 방향을 잡고, 로그로 지연 구간을 특정한 뒤, 모델 로딩/워커/배치/페이로드 구조를 손보는 것입니다. 타임아웃 증설은 마지막 단계에서만 최소한으로 적용해야 재발을 줄일 수 있습니다.