Published on

TorchServe 500/타임아웃 진단·튜닝 8선

Authors

TorchServe를 운영하다 보면 가장 괴로운 장애가 HTTP 500timeout 입니다. 둘 다 “서버가 응답을 못했다”는 결과만 남기기 때문에, 원인을 빨리 분류할 수 있는 체크리스트가 없으면 재현도 어렵고 튜닝도 감으로 하게 됩니다.

이 글은 TorchServe의 요청 흐름(프런트엔드 HTTP Netty → 워커 프로세스 → 모델 핸들러 → 응답) 기준으로, 500/타임아웃을 원인별로 진단하고 즉시 적용 가능한 튜닝 포인트 8개를 정리합니다. 각 항목은 “증상 → 확인 로그/지표 → 해결/튜닝” 순서로 구성했습니다.

운영 환경이 Kubernetes라면, 애플리케이션 이슈와 인프라 이슈가 섞여 보일 수 있습니다. Pod 재시작/Probe 문제까지 같이 의심된다면 K8s CrashLoopBackOff 원인별 로그·Probe 해결 가이드도 함께 보시면 분리 진단에 도움이 됩니다.

1) 워커 부팅 지연 또는 워커 수 부족으로 큐 적체

대표 증상

  • 트래픽이 조금만 올라가도 timeout 이 늘고, 간헐적으로 500 이 섞임
  • 콜드 스타트(모델 로딩) 구간에 특히 심함

확인 포인트

  • TorchServe 로그에서 워커 로딩/스폰 지연 메시지
  • 지표가 있다면 요청 대기열 증가(큐잉), 워커 busy 비율 상승

해결·튜닝

  • 모델별 워커 수를 명시하고, 워커의 스케일 정책을 분리합니다.
  • 모델 로딩이 큰 경우 lazy 로딩 대신 프로비저닝 단계에서 워커를 미리 올려 콜드 스타트를 줄입니다.

config.properties 예시(인라인의 = 값은 환경에 맞게 조정):

inference_address=http://0.0.0.0:8080
management_address=http://0.0.0.0:8081
metrics_address=http://0.0.0.0:8082

# 워커 프로세스 수와 큐 관련 기본값을 상황에 맞게 조정
default_workers_per_model=2
job_queue_size=100

모델 등록 시 워커를 명시하는 예시:

curl -X POST "http://localhost:8081/models?url=my-model.mar&initial_workers=2&synchronous=true"

2) 배치/동시성 설정 미스매치로 인한 지연 폭증

대표 증상

  • GPU는 놀고 있는데 레이턴시만 길어짐(혹은 반대로 GPU 100%인데 타임아웃)
  • 특정 입력 크기에서만 급격히 느려짐

확인 포인트

  • 핸들러에서 배치 텐서 생성/패딩 비용이 과도한지
  • 배치 크기 대비 maxBatchDelay 가 과도하게 크거나 작지 않은지

해결·튜닝

  • TorchServe의 배치는 “대기 시간을 조금 주고 묶어서 처리”하는 전략입니다. 트래픽이 낮은데 배치 지연을 크게 주면 그냥 느려집니다.
  • 반대로 트래픽이 높은데 배치가 꺼져 있으면 GPU 효율이 떨어지고 워커가 과도하게 늘어납니다.

model-config.yaml 예시:

models:
  my_model:
    minWorkers: 1
    maxWorkers: 4
    batchSize: 8
    maxBatchDelay: 50

운영 팁:

  • 온라인 서빙은 보통 maxBatchDelay 를 짧게(수십 ms) 두고, batchSize 는 P95 레이턴시 목표를 깨지 않는 선에서 올립니다.
  • 입력 길이/해상도가 가변이면 “배치로 묶는 비용”이 커질 수 있어, 배치가 오히려 손해가 될 수 있습니다.

3) 핸들러(preprocess/postprocess) 병목 또는 Python GIL 영향

대표 증상

  • 모델 추론은 빠른데 전체 응답이 느림
  • CPU 사용률이 높고, 워커를 늘려도 선형으로 개선되지 않음

확인 포인트

  • 핸들러에서 이미지 디코딩, 토크나이징, JSON 직렬화가 과도한지
  • 불필요한 numpytorch 변환이 반복되는지

해결·튜닝

  • 전처리/후처리는 가능한 한 벡터화하고, 반복 변환을 줄입니다.
  • 큰 결과를 JSON으로 그대로 내보내면 직렬화가 병목이 됩니다. 필요한 필드만 반환하거나 바이너리 포맷을 고려합니다.

핸들러에서 타이밍을 찍는 최소 예시:

import time

class MyHandler:
    def handle(self, data, context):
        t0 = time.perf_counter()
        inp = self.preprocess(data)
        t1 = time.perf_counter()
        out = self.inference(inp)
        t2 = time.perf_counter()
        res = self.postprocess(out)
        t3 = time.perf_counter()

        context.metrics.add_time("preprocess_ms", (t1 - t0) * 1000)
        context.metrics.add_time("inference_ms", (t2 - t1) * 1000)
        context.metrics.add_time("postprocess_ms", (t3 - t2) * 1000)
        return res

4) GPU 메모리 부족(OOM) 또는 메모리 단편화

대표 증상

  • 특정 시점부터 500 이 급증하고 워커가 재시작되거나 응답이 끊김
  • 로그에 CUDA out of memory 또는 유사 메시지

확인 포인트

  • TorchServe 워커 로그에서 OOM 스택트레이스
  • 같은 모델이라도 입력 크기 증가(예: 긴 시퀀스)에서만 발생하는지

해결·튜닝

  • 배치 크기를 줄이거나, 입력 상한을 강제합니다.
  • 혼합정밀(fp16, bf16) 또는 torch.compile 같은 최적화는 도움이 되지만, 먼저 “입력 상한/배치 상한”으로 OOM을 막는 게 우선입니다.
  • 워커 수를 늘리면 GPU 메모리도 그만큼 쪼개 쓰게 되어 OOM이 더 잘 날 수 있습니다. GPU 1장에 워커를 너무 많이 붙이지 마세요.

입력 길이 상한을 두는 예시(텍스트):

def preprocess(self, data):
    text = data[0].get("body", "")
    if len(text) > 2000:
        raise ValueError("input too long")
    return text

5) 응답 크기/직렬화 문제로 Netty 레이어에서 지연 또는 500

대표 증상

  • 큰 텐서/대형 리스트를 반환할 때만 500 또는 타임아웃
  • 클라이언트가 연결을 끊거나 프록시가 응답을 잘라먹는 듯한 증상

확인 포인트

  • 반환 JSON이 과도하게 큰지(수 MB 이상)
  • base64 인코딩 이미지/벡터를 그대로 반환하는지

해결·튜닝

  • Top-K만 반환, float 배열은 압축 또는 다른 저장소로 오프로드 후 URL만 반환
  • 응답 포맷을 단순화하고, 필요 시 gzip 같은 압축을 프록시 레이어에서 적용

후처리에서 Top-K만 반환하는 예시:

import torch

def postprocess(self, logits):
    probs = torch.softmax(logits, dim=-1)
    topk = torch.topk(probs, k=5)
    return {
        "indices": topk.indices.tolist(),
        "scores": topk.values.tolist(),
    }

6) 타임아웃의 진짜 원인이 프록시/로드밸런서인 경우

대표 증상

  • TorchServe 자체 로그에는 에러가 없는데 클라이언트는 504 또는 타임아웃
  • 특정 경로에서만 재현(예: Ingress를 통과할 때만)

확인 포인트

  • Ingress, ALB, Nginx, API Gateway의 idle timeout / read timeout
  • 서버는 처리 중인데 중간 프록시가 먼저 끊는지

해결·튜닝

  • “서버 타임아웃”과 “프록시 타임아웃”을 분리해서 맞춰야 합니다.
  • 추론이 길어질 수 있는 모델이면, 프록시 타임아웃을 늘리거나 비동기 패턴(작업 큐, 폴링, 콜백)을 고려하세요.

EKS ALB/Ingress 레벨에서도 비슷한 착시가 자주 발생합니다. 403이지만 WAF가 원인이 아닌 케이스처럼, 겉으로 보이는 상태 코드만으로 판단하면 삽질합니다. 관련 맥락은 EKS ALB Ingress 403, WAF 아닌 원인 7가지도 참고할 만합니다.

7) 헬스체크/Probe 설정으로 인한 요청 중단(재시작 루프)

대표 증상

  • 트래픽이 몰리면 Pod가 재시작되고, 그 시점에 500/timeout이 동반
  • readinessProbe 실패로 라우팅이 끊기거나, livenessProbe 가 과하게 공격적으로 설정됨

확인 포인트

  • Kubernetes 이벤트에서 Probe 실패 로그
  • TorchServe가 모델 로딩 중인데 readiness가 이미 true로 뜨는지(혹은 반대)

해결·튜닝

  • 모델 로딩 시간이 길면 startupProbe 를 사용하거나 readiness 초기 지연을 충분히 둡니다.
  • liveness는 “진짜로 죽었을 때만” 재시작하게 느슨하게 잡고, readiness로 트래픽 제어를 합니다.

Probe 예시:

readinessProbe:
  httpGet:
    path: /ping
    port: 8080
  initialDelaySeconds: 20
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 6

livenessProbe:
  httpGet:
    path: /ping
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 6

8) 모델 아카이브(.mar) 및 의존성/스레드 설정 문제

대표 증상

  • 배포 직후만 500(ImportError, 라이브러리 충돌)
  • 특정 워커만 불규칙하게 실패
  • CPU 스레드가 과도하게 늘어 컨텍스트 스위칭으로 레이턴시가 악화

확인 포인트

  • 워커 로그에서 ModuleNotFoundError, OSError(shared library) 같은 로딩 에러
  • torch 스레드 수가 노드 코어 수를 초과해 폭증하는지

해결·튜닝

  • .mar 에 포함된 requirements 와 런타임 베이스 이미지의 패키지 버전을 고정합니다.
  • CPU 스레드 관련 환경변수를 명시해 “과도한 병렬화”를 막습니다.

컨테이너 환경변수 예시:

export OMP_NUM_THREADS=1
export MKL_NUM_THREADS=1
export TORCH_NUM_THREADS=1

모델 아카이브 생성 예시:

torch-model-archiver \
  --model-name my-model \
  --version 1.0 \
  --serialized-file model.pt \
  --handler handler.py \
  --extra-files "index_to_name.json" \
  --export-path model_store

운영에서 가장 빠른 분류법(실전 체크 순서)

  1. 클라이언트 상태 코드가 500 인지 504 인지 확인하고, 중간 프록시 타임아웃을 먼저 의심합니다.
  2. TorchServe management 로그와 워커 로그를 분리해서 봅니다(워커 OOM/ImportError는 대부분 워커 로그에 있습니다).
  3. 레이턴시를 preprocess / inference / postprocess 로 쪼개 측정합니다.
  4. 그 다음에야 워커 수, 배치, 스레드, 입력 상한 같은 튜닝을 적용합니다.

장애 대응을 “원인별 체크리스트”로 굳혀두면, TorchServe 500/타임아웃은 대부분 30분 내에 1차 분류가 됩니다. 이후에는 목표(P95, 비용, 처리량)에 맞춰 워커·배치·입력 제한을 조합해 최적점을 찾는 작업으로 넘어가면 됩니다.

추가로, 운영 중 프로세스가 죽었을 때 자동 복구를 강화하려면 서비스 관리 관점에서 systemd 서비스 자동 재시작 - 죽었다 깨도 복구 같은 접근도 유용합니다(쿠버네티스가 아닌 VM/베어메탈 운영 시 특히).