Published on

GCP Cloud Run 503·Cold Start 지연 최소화 7가지

Authors

서버리스답게 잘 돌아가던 Cloud Run 서비스가 갑자기 503을 뱉거나, 트래픽이 몰릴 때 첫 요청이 유독 느린(Cold Start) 상황은 생각보다 흔합니다. 문제는 “Cloud Run이 느리다”가 아니라, 대부분 인스턴스 생성/초기화/동시성/네트워크/헬스체크 중 하나가 병목이 되어 요청이 라우팅되기 전에 실패하거나 처리 큐가 밀려 타임아웃으로 이어진다는 점입니다.

이 글은 Cloud Run에서 503과 Cold Start 지연을 줄이기 위한 7가지 방법을 원인 → 조치 → 확인 순서로 정리합니다. (전제: Cloud Run fully managed 기준)


0. 먼저 “503의 종류”부터 구분하기

Cloud Run의 503은 크게 두 부류로 나뉩니다.

  1. 플랫폼 레벨 503: 인스턴스가 없거나(스케일 0), 생성 중이거나, 리비전이 준비되지 않았거나, 용량/쿼터/네트워크 문제로 라우팅이 실패

  2. 애플리케이션 레벨 503: 앱이 503을 반환(예: upstream 장애를 503으로 매핑)

로그/지표에서 빠르게 판별

  • Cloud Logging에서 resource.type="cloud_run_revision"로 필터링 후,
    • httpRequest.status=503가 찍히는데 애플리케이션 로그가 없다면 플랫폼 레벨 가능성이 큽니다.
    • 반대로 앱 로그에 요청 핸들러 진입 기록이 있고 503을 반환했다면 앱 레벨입니다.

또한 Cloud Run 메트릭에서 다음을 같이 봐야 합니다.

  • container/startup_latencies (또는 유사한 Startup latency)
  • request_latencies
  • instance_count, max_request_concurrency_utilization

이제부터의 7가지는 플랫폼/앱 양쪽을 모두 포함하지만, 특히 플랫폼 레벨 503 + Cold Start에 효과가 큽니다.


1) min-instances로 “스케일 0” 제거 (가장 확실한 Cold Start 완화)

Cloud Run의 Cold Start는 대부분 0 → 1 인스턴스로 올라오는 순간 발생합니다. min-instances를 1 이상으로 두면 항상 warm 인스턴스가 유지되어 첫 요청이 빨라지고, “인스턴스가 아직 없음”으로 인한 503 가능성도 크게 줄어듭니다.

권장 설정

  • API/웹 서비스: min-instances=1~2
  • 트래픽이 급격히 튀는 서비스: min-instances를 조금 더 올리고, 비용은 모니터링으로 최적화

gcloud 예시

gcloud run services update my-svc \
  --region=asia-northeast3 \
  --min-instances=1

확인 포인트

  • 새 배포 후에도 인스턴스가 0으로 떨어지지 않는지
  • 첫 요청 P95/P99가 유의미하게 감소하는지

> 비용이 늘어나는 대신, Cold Start 체감은 가장 즉각적으로 개선됩니다.


2) startup CPU boost + CPU always 할당 전략

Cold Start의 큰 비중은 **컨테이너 초기화(의존성 로딩, JIT, 모델 로딩, DB 커넥션 준비)**입니다. Cloud Run은 기본적으로 요청이 있을 때만 CPU가 할당되는 모드가 흔한데, 초기화가 무거운 서비스는 아래 옵션 조합이 도움이 됩니다.

  • Startup CPU boost: 시작 구간에 CPU를 더 줘서 부팅 시간을 단축
  • CPU always allocated: 유휴 시에도 CPU를 유지해 백그라운드 초기화/캐시 준비가 가능

언제 쓰나

  • Python/FastAPI에서 import가 무겁다
  • Node.js에서 번들/초기화가 크다
  • JVM(특히 Spring)처럼 워밍업이 큰 런타임
  • LLM/ML 모델 로딩, 폰트/템플릿 캐시 등

gcloud 예시(개념)

gcloud run services update my-svc \
  --region=asia-northeast3 \
  --cpu=2 \
  --execution-environment=gen2 \
  --cpu-boost

> CPU always allocated는 콘솔에서 설정하거나, IaC(Terraform)로 명시하는 편이 안전합니다(조직 표준화 관점).

확인 포인트

  • Startup latency 감소
  • 유휴 비용 증가 여부(항상 CPU 할당 시)

3) 동시성(concurrency)과 인스턴스 수(max-instances)로 503(과부하) 방지

Cloud Run의 503은 “인스턴스가 없어서”뿐 아니라 “인스턴스는 있는데 동시 처리 한계를 넘어 큐가 밀리거나 타임아웃”에서도 자주 발생합니다.

핵심은 두 가지 균형

  • Concurrency(요청 동시 처리 수): 한 인스턴스가 동시에 처리할 요청 수
  • Max instances(최대 인스턴스 수): 스케일 아웃 상한

패턴 A: CPU 바운드(이미지 처리, 암호화, ML 추론)

  • concurrency를 낮게(예: 1~4)
  • 인스턴스를 더 늘려 수평 확장

패턴 B: I/O 바운드(외부 API 호출, DB 대기)

  • concurrency를 높여도 됨(예: 20~80)
  • 단, DB 커넥션/외부 API 레이트리밋이 병목이 될 수 있음

gcloud 예시

gcloud run services update my-svc \
  --region=asia-northeast3 \
  --concurrency=20 \
  --max-instances=50

확인 포인트

  • request_latencies가 늘어나기 전에 instance_count가 충분히 올라오는지
  • 과도한 concurrency로 인해 앱 내부(스레드/이벤트루프/DB풀)가 먼저 터지지 않는지

4) 앱 초기화는 “요청 경로 밖”으로: lazy load + warm-up 엔드포인트

Cold Start 지연의 절반은 코드 문제일 때가 많습니다. 특히 Python/FastAPI에서 다음 패턴이 흔합니다.

  • 모듈 import 시점에 무거운 작업 수행(모델 로딩, 원격 설정 fetch)
  • 전역 영역에서 DB 연결 시도
  • 첫 요청에서만 캐시/템플릿 컴파일

원칙

  • 프로세스 시작 시 필수 최소만
  • 나머지는 lazy load 하되, 첫 사용자 요청에서 하지 않도록 warm-up으로 미리 실행

FastAPI 예시: startup 이벤트로 최소 준비 + 백그라운드 워밍업

from fastapi import FastAPI
import asyncio

app = FastAPI()

model = None

async def load_model():
    # 무거운 로딩을 함수로 격리
    global model
    await asyncio.sleep(0.1)  # 예시
    model = "loaded"

@app.on_event("startup")
async def on_startup():
    # 완전 로딩이 아니라 "예약"만 걸고, 부팅을 빠르게 끝내는 전략도 가능
    asyncio.create_task(load_model())

@app.get("/healthz")
def healthz():
    return {"ok": True}

@app.get("/infer")
def infer():
    if model is None:
        # 최후의 안전장치: 아직 로딩 안 됐으면 빠르게 실패/재시도 유도
        return {"error": "warming"}
    return {"result": "..."}

warm-up 트래픽 주기적 호출

  • Cloud Scheduler → HTTP 호출로 /healthz 또는 /warmup 주기 호출
  • 단, min-instances=0인 상태에서만 의미가 큼(스케일 0 방지 목적)

> 프록시/타임아웃/버퍼 설정 때문에 스트리밍이 끊기는 류의 문제는 503과 함께 나타나기도 합니다. FastAPI 스트리밍을 쓴다면 FastAPI Uvicorn에서 SSE 웹소켓 LLM 스트리밍이 프록시 뒤에서 끊길 때...도 함께 점검하는 게 좋습니다.


5) 헬스체크/Readiness 관점: “포트 바인딩”을 최대한 빨리

Cloud Run은 컨테이너가 지정 포트($PORT) 에 리슨하기 전까지 트래픽을 정상적으로 붙이지 못합니다. 많은 프레임워크에서 다음이 지연을 키웁니다.

  • 서버 시작 전에 마이그레이션/외부 호출/모델 로딩 등을 수행
  • 로깅/설정 로딩이 네트워크 I/O를 동반

원칙

  1. 먼저 포트 리슨
  2. 그 다음에 무거운 초기화는 백그라운드로
  3. readiness가 필요하면 앱 레벨에서 “준비됨” 상태를 분리

Node.js(Express) 예시: 서버 먼저 띄우고 백그라운드 init

import express from "express";

const app = express();
let ready = false;

app.get("/healthz", (req, res) => {
  res.status(200).json({ ok: true, ready });
});

app.get("/", (req, res) => {
  if (!ready) return res.status(503).send("warming");
  res.send("hello");
});

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log("listening", port);
  // 무거운 초기화는 리슨 이후
  setTimeout(() => { ready = true; }, 2000);
});

이 방식은 “첫 요청을 무조건 성공”시키는 전략은 아니지만, **플랫폼 레벨 503(라우팅 실패)**를 줄이고, 준비되지 않은 상태를 앱이 제어 가능한 503으로 바꾸어 재시도/백오프 전략을 적용하기 쉬워집니다.


6) 외부 의존성(특히 DB/HTTP) 커넥션 전략: 풀/재사용/타임아웃

Cold Start 시점에는 외부 의존성도 동시에 몰립니다.

  • 새 인스턴스가 뜰 때마다 DB 커넥션을 새로 열어 DB가 순간 과부하
  • HTTP 클라이언트를 요청마다 생성해 TLS 핸드셰이크가 반복
  • 타임아웃이 길어 요청이 쌓이고 결국 503/504로 번짐

HTTP 클라이언트는 재사용(세션/커넥션 풀)

Python aiohttp를 쓴다면 ClientSession을 요청마다 만들지 말고 앱 전역으로 재사용해야 합니다. 잘못하면 에러/리소스 누수로 성능이 더 나빠질 수 있습니다. 관련해서는 aiohttp ClientSession is closed 재현과 근본 해결을 참고하면 원인-재현-해결이 정리돼 있습니다.

DB는 Cloud SQL이라면 Connector/풀링/프록시를 명확히

  • 인스턴스당 커넥션 수 × 인스턴스 수 = DB가 받는 동시 커넥션
  • concurrency를 올리면 DB 풀도 함께 튜닝해야 함
  • 짧은 connect/read timeout, 재시도(지수 백오프) 적용

권장 체크리스트

  • (HTTP) keep-alive, 커넥션 풀 크기 제한
  • (DB) 풀 크기 제한, 커넥션 재사용, 쿼리 타임아웃
  • (공통) 외부 호출에 deadline 설정(무한 대기 금지)

7) 배포/트래픽 전환 시 503 줄이기: 점진적 롤아웃과 관측

Cloud Run은 리비전 단위로 배포되고 트래픽을 전환합니다. 이때 새 리비전이 완전히 준비되기 전에 트래픽이 붙으면 “배포 직후 503”이 발생할 수 있습니다(특히 초기화가 무거운 경우).

실전 전략

  • 트래픽 분할로 점진 전환(예: 1% → 10% → 50% → 100%)
  • 새 리비전의 startup latency/에러율을 확인하고 다음 단계 진행
  • 문제가 있으면 즉시 트래픽을 이전 리비전으로 되돌림

gcloud 예시: 트래픽 분할

# 최신 리비전에 10%, 이전 안정 리비전에 90%
gcloud run services update-traffic my-svc \
  --region=asia-northeast3 \
  --to-latest=10

관측(Observability) 포인트

  • 배포 직후 1~5분 구간의 503 비율
  • 새 리비전의 startup latency 상승 여부
  • 특정 리전/특정 시간대에만 발생하는지(쿼터/네트워크)

503·Cold Start 튜닝 우선순위(추천 순서)

현장에서 가장 빠르게 효과가 나는 순서를 정리하면 다음과 같습니다.

  1. min-instances로 스케일 0 제거
  2. concurrency/max-instances로 과부하 503 방지
  3. 앱 초기화 경량화(요청 경로 밖으로 이동)
  4. startup CPU boost/CPU always allocated 검토
  5. 포트 리슨을 최대한 빨리(헬스/ready 분리)
  6. 외부 의존성 커넥션 재사용/풀/타임아웃
  7. 점진적 롤아웃 + 지표 기반 검증

마무리: “플랫폼 503”을 “제어 가능한 실패”로 바꾸는 게 핵심

Cloud Run의 503과 Cold Start는 완전히 없애기 어렵지만, **발생 지점을 앞당기거나(포트 리슨), warm 인스턴스를 유지하거나(min-instances), 과부하를 설계로 흡수(concurrency/풀링)**하면 체감은 크게 줄일 수 있습니다.

다음 단계로는 실제 로그 필터/메트릭 대시보드를 기준으로 “내 서비스의 503이 어떤 타입인지”를 먼저 분류해 보세요. 분류만 정확하면 위 7가지 중 2~3개 조합으로도 대부분 안정화됩니다.