- Published on
GCP Cloud Run 503·Cold Start 지연 최소화 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스답게 잘 돌아가던 Cloud Run 서비스가 갑자기 503을 뱉거나, 트래픽이 몰릴 때 첫 요청이 유독 느린(Cold Start) 상황은 생각보다 흔합니다. 문제는 “Cloud Run이 느리다”가 아니라, 대부분 인스턴스 생성/초기화/동시성/네트워크/헬스체크 중 하나가 병목이 되어 요청이 라우팅되기 전에 실패하거나 처리 큐가 밀려 타임아웃으로 이어진다는 점입니다.
이 글은 Cloud Run에서 503과 Cold Start 지연을 줄이기 위한 7가지 방법을 원인 → 조치 → 확인 순서로 정리합니다. (전제: Cloud Run fully managed 기준)
0. 먼저 “503의 종류”부터 구분하기
Cloud Run의 503은 크게 두 부류로 나뉩니다.
플랫폼 레벨 503: 인스턴스가 없거나(스케일 0), 생성 중이거나, 리비전이 준비되지 않았거나, 용량/쿼터/네트워크 문제로 라우팅이 실패
애플리케이션 레벨 503: 앱이 503을 반환(예: upstream 장애를 503으로 매핑)
로그/지표에서 빠르게 판별
- Cloud Logging에서
resource.type="cloud_run_revision"로 필터링 후,httpRequest.status=503가 찍히는데 애플리케이션 로그가 없다면 플랫폼 레벨 가능성이 큽니다.- 반대로 앱 로그에 요청 핸들러 진입 기록이 있고 503을 반환했다면 앱 레벨입니다.
또한 Cloud Run 메트릭에서 다음을 같이 봐야 합니다.
container/startup_latencies(또는 유사한 Startup latency)request_latenciesinstance_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를 동반
원칙
- 먼저 포트 리슨
- 그 다음에 무거운 초기화는 백그라운드로
- 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 튜닝 우선순위(추천 순서)
현장에서 가장 빠르게 효과가 나는 순서를 정리하면 다음과 같습니다.
min-instances로 스케일 0 제거- concurrency/max-instances로 과부하 503 방지
- 앱 초기화 경량화(요청 경로 밖으로 이동)
- startup CPU boost/CPU always allocated 검토
- 포트 리슨을 최대한 빨리(헬스/ready 분리)
- 외부 의존성 커넥션 재사용/풀/타임아웃
- 점진적 롤아웃 + 지표 기반 검증
마무리: “플랫폼 503”을 “제어 가능한 실패”로 바꾸는 게 핵심
Cloud Run의 503과 Cold Start는 완전히 없애기 어렵지만, **발생 지점을 앞당기거나(포트 리슨), warm 인스턴스를 유지하거나(min-instances), 과부하를 설계로 흡수(concurrency/풀링)**하면 체감은 크게 줄일 수 있습니다.
다음 단계로는 실제 로그 필터/메트릭 대시보드를 기준으로 “내 서비스의 503이 어떤 타입인지”를 먼저 분류해 보세요. 분류만 정확하면 위 7가지 중 2~3개 조합으로도 대부분 안정화됩니다.