- Published on
GCP Cloud Run 503와 콜드스타트 지연 원인·튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스의 장점(자동 확장, 운영 부담 감소) 뒤에는 특유의 장애 패턴이 있습니다. GCP Cloud Run에서 대표적인 것이 **503(Service Unavailable)**와 콜드스타트(cold start) 지연입니다. 둘은 별개처럼 보이지만 실제로는 같은 축(인스턴스 준비/확장/의존성/타임아웃)에서 파생되는 경우가 많습니다.
이 글에서는 Cloud Run 503이 어떤 상황에서 발생하는지, 콜드스타트가 왜 길어지는지, 그리고 설정 튜닝(플랫폼 레벨) + **코드/아키텍처 튜닝(애플리케이션 레벨)**로 어떻게 줄이는지까지 단계적으로 정리합니다.
1) Cloud Run의 503은 어떤 의미인가
Cloud Run의 503은 “애플리케이션이 503을 반환했다”라기보다, 대개 플랫폼이 요청을 정상 처리할 수 없어서 반환하는 경우가 많습니다. 대표 시나리오는 다음과 같습니다.
1.1 인스턴스가 아직 준비되지 않았는데 트래픽이 들어옴
- 새 인스턴스를 띄우는 중(이미지 pull, 컨테이너 부팅, 앱 초기화)
- readiness에 해당하는 상태가 만족되지 않음
- 이때 요청이 들어오면 플랫폼이 대기/재시도하다가 요청 타임아웃/용량 부족으로 503이 나기도 합니다.
1.2 순간 트래픽 스파이크 + 확장 지연
- Cloud Run은 요청량에 맞춰 인스턴스를 자동으로 늘리지만, 확장에는 시간이 필요합니다.
- 특히 동시성(concurrency)이 낮고(=인스턴스가 빨리 포화), min instances가 0이면 스파이크가 곧바로 503으로 이어질 수 있습니다.
1.3 외부 의존성(예: DB, Redis, 외부 API) 지연/실패
- 앱은 살아있지만 DB 커넥션 획득 지연, DNS 지연, 외부 API 타임아웃 등으로 요청 처리가 밀리면
- 결과적으로 인스턴스가 처리 가능한 동시 요청을 초과하고, 큐잉이 길어지며 503/timeout로 악화됩니다.
> DB 커넥션 문제는 콜드스타트 때 특히 증폭됩니다. Spring 계열이라면 HikariCP 설정/누수도 함께 점검하세요: Spring Boot DB 커넥션 누수? HikariCP 원인 9가지
2) 콜드스타트가 길어지는 핵심 원인
Cloud Run 콜드스타트는 크게 3단계 비용의 합입니다.
- 인프라 단계: 새 인스턴스 할당, 이미지 pull
- 컨테이너 단계: 런타임 부팅(Node/Java/Python), 네이티브 라이브러리 로드
- 앱 단계: 프레임워크 초기화, DI 컨테이너 구성, DB 연결/마이그레이션, 캐시 워밍 등
여기서 길어지는 대표 원인:
2.1 이미지가 크거나 레이어 캐시가 비효율적
- 베이스 이미지가 무거움(예: full JDK, 불필요한 패키지)
- 레이어가 자주 깨져서 pull 최적화가 안 됨
2.2 런타임/프레임워크 초기화 비용
- Java/Spring Boot는 초기화가 무거운 편
- Node도 번들 크기, 동적 import, 초기화 시점의 I/O가 많으면 느려짐
2.3 시작 시 외부 I/O를 동기적으로 수행
- 시작하자마자 DB 연결 테스트, 외부 API 호출, 비밀 조회(Secret Manager), 대용량 설정 로드
- 콜드스타트에서 가장 흔한 병목입니다.
2.4 CPU 할당 정책과 초기화 CPU 부족
Cloud Run은 설정에 따라 CPU가 요청 처리 중에만 할당되는 모드가 일반적입니다. 콜드스타트 초기화는 “요청을 받는 순간” 시작되므로, CPU가 부족하면 초기화가 더 길어집니다.
3) 먼저 관측부터: 어디서 시간이 새는지 확인
튜닝은 감으로 하면 실패합니다. 아래 3가지를 먼저 확보하세요.
3.1 Cloud Logging에서 요청 로그/에러 패턴 확인
- 503이 특정 시간대/특정 경로에서만 발생하는지
revision변경 직후(배포 직후)만 발생하는지- 특정 외부 의존성 호출이 느린지
3.2 Cloud Monitoring에서 핵심 지표 보기
- Request count, Request latency(p50/p95/p99)
- Instance count(동시에 몇 개가 떠 있는지)
- Container startup latency(가능한 경우)
- CPU/Memory 사용률(특히 메모리 OOM로 재시작이 있는지)
3.3 Trace/Profiler로 “초기화 구간”을 쪼개기
OpenTelemetry/Cloud Trace를 붙이면 “앱이 준비되기 전”과 “요청 처리 중” 병목을 분리하는 데 도움이 됩니다.
4) 플랫폼 설정 튜닝: 503과 콜드스타트를 줄이는 레버
아래는 Cloud Run에서 즉시 효과가 큰 설정들입니다.
4.1 최소 인스턴스(min instances) 설정
- min instances = 0: 비용 최적화, 대신 첫 요청은 거의 무조건 콜드스타트
- min instances = 1 이상: 콜드스타트/503 급감(특히 트래픽이 간헐적인 서비스)
권장:
- 사용자-facing API면 최소 1~2로 시작
- 배치/내부용이면 0 유지하되 스케줄러로 워밍업 고려
4.2 동시성(concurrency) 조정
동시성은 “인스턴스 1개가 동시에 처리할 수 있는 요청 수”입니다.
- 동시성이 너무 낮으면: 인스턴스가 빨리 포화 → 스파이크에 503
- 동시성이 너무 높으면: 앱이 병렬 처리 못 함 → 지연 증가, 타임아웃 증가
가이드:
- CPU 바운드(이미지 처리/암호화 등): 낮게(1~10)
- I/O 바운드(API 게이트웨이/DB 조회 위주): 높게(20~80)부터 실험
4.3 타임아웃(timeout)과 재시도(retry) 정책
- Cloud Run 요청 타임아웃이 너무 짧으면 콜드스타트 시 503/504로 보일 수 있습니다.
- 반대로 너무 길면 “느린 요청”이 쌓여 인스턴스가 잠기고 503으로 번질 수 있습니다.
권장:
- API는 10~30초 내로 강제(업무에 맞게)
- 외부 API 호출은 더 짧은 타임아웃 + 회로 차단(circuit breaker)
4.4 CPU/메모리 증설 및 CPU always allocated 검토
- 콜드스타트가 “초기화 CPU 부족”이라면 CPU를 올리는 것만으로도 큰 개선이 납니다.
- 백그라운드 워밍/캐시 준비가 필요하면 CPU always allocated(항상 CPU 할당)도 고려할 수 있습니다(비용 증가).
4.5 최대 인스턴스(max instances)로 폭주 제어
무제한 확장은 외부 의존성(DB)을 터뜨릴 수 있습니다.
- max instances를 제한해 DB/외부 API 보호
- 대신 초과 트래픽은 큐/버퍼링(Cloud Tasks/PubSub)로 흡수하는 구조가 더 안전합니다.
5) 애플리케이션 튜닝: 콜드스타트 시간을 직접 깎기
설정만으로 한계가 있으면 코드/빌드/런타임을 손봐야 합니다.
5.1 컨테이너 이미지 최적화(Dockerfile)
- 멀티 스테이지 빌드
- distroless 또는 slim 베이스
- 의존성 설치 레이어를 최대한 캐시되게 구성
예시: Node.js 멀티 스테이지 + 프로덕션 의존성만
# build stage
FROM node:20-bookworm AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# runtime stage
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./package.json
ENV NODE_ENV=production
CMD ["dist/server.js"]
포인트:
npm ci레이어가 자주 깨지지 않게package*.json먼저 복사- 런타임 이미지를 작게 유지해 pull 시간을 줄임
5.2 “시작 시점 동기 I/O” 제거: 지연 로딩(lazy init)
콜드스타트 때 DB를 반드시 붙여야 하는지, 캐시를 미리 채워야 하는지부터 의심하세요.
- 시작 시 DB 연결을 강제하지 말고, 첫 요청에서 필요할 때 연결
- 또는 readiness 이전에 외부 I/O를 몰아서 하지 않기
예시: Express에서 DB 연결을 lazy로
import express from "express";
import { Pool } from "pg";
const app = express();
let pool;
function getPool() {
if (!pool) {
pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
}
return pool;
}
app.get("/healthz", (req, res) => res.status(200).send("ok"));
app.get("/users/:id", async (req, res) => {
const db = getPool();
const { rows } = await db.query("select * from users where id=$1", [req.params.id]);
res.json(rows[0] ?? null);
});
app.listen(process.env.PORT || 8080);
5.3 헬스체크(healthz)와 무거운 핸들러 분리
/healthz는 절대 DB/외부 API를 호출하지 않게- “앱 프로세스가 떠 있는지”만 빠르게 확인
- Cloud Run의 트래픽 라우팅/오토스케일 판단에 불필요한 부하를 주지 않습니다.
5.4 외부 호출에 타임아웃/회로 차단 적용
콜드스타트 이후에도 503을 만드는 주범은 외부 의존성 지연입니다.
예시: fetch에 AbortController로 타임아웃
async function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return res;
} finally {
clearTimeout(t);
}
}
5.5 DB 커넥션 풀 사이즈를 “인스턴스 수 × 동시성” 관점으로 재설계
Cloud Run은 스케일 아웃이 빠르기 때문에, 인스턴스마다 큰 풀을 잡으면 DB가 먼저 죽습니다.
- 인스턴스당 풀은 작게(예: 2~10)
- 동시성은 앱 특성에 맞게 조정
- 필요하면 PgBouncer/Cloud SQL Auth Proxy/커넥션 재사용 전략 검토
DB 관련 이슈가 의심되면 커넥션 누수/풀 고갈부터 확인하는 것이 좋습니다: Spring Boot DB 커넥션 누수? HikariCP 원인 9가지
6) 503을 “재현 가능”하게 만들기: 부하 테스트 전략
튜닝은 재현이 가능해야 반복 개선이 됩니다.
6.1 스파이크 트래픽 테스트
- k6/hey/wrk로 짧은 시간에 RPS를 급격히 올려 503 발생 지점을 찾습니다.
k6 예시
import http from "k6/http";
import { sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 5 },
{ duration: "30s", target: 50 },
{ duration: "30s", target: 200 },
{ duration: "30s", target: 0 },
],
};
export default function () {
http.get(__ENV.TARGET_URL);
sleep(1);
}
6.2 배포 직후 첫 요청 테스트(콜드스타트 측정)
- 새 revision 배포 후 첫 요청 p95/p99를 따로 측정
- min instances를 0/1로 바꿔 효과 비교
7) 실전 튜닝 체크리스트(우선순위)
다음 순서로 하면 시행착오가 줄어듭니다.
- 로그/모니터링으로 503 유형 분류: 배포 직후? 스파이크? 특정 엔드포인트?
- min instances 1 적용(사용자-facing 서비스라면) → 즉시 콜드스타트/503 완화
- concurrency 조정: 너무 낮으면 확장 지연으로 503, 너무 높으면 지연 폭증
- CPU/메모리 상향: 특히 Java/Spring, 이미지 처리, 암호화 작업
- 이미지 슬림화 + 레이어 캐시 최적화
- 초기화 시 동기 외부 I/O 제거(lazy init)
- 외부 호출 타임아웃/회로 차단으로 꼬리 지연 차단
- DB 풀 재설계(인스턴스 수 증가를 전제로)
8) 자주 하는 오해 3가지
8.1 “503이면 무조건 Cloud Run이 불안정하다”
대부분은 설정/의존성/확장 지연의 결과입니다. 특히 DB/외부 API가 느려지면 Cloud Run이 아니라도 동일하게 터집니다.
8.2 “동시성은 무조건 높이면 좋다”
I/O 바운드 서비스에서만 유리합니다. CPU 바운드에서 동시성을 올리면 p99가 급격히 악화됩니다.
8.3 “콜드스타트는 어쩔 수 없다”
min instances, 이미지 최적화, 초기화 I/O 제거만 해도 체감이 크게 줄어듭니다. 비용과 지연 사이에서 의도적으로 선택하면 됩니다.
9) 마무리: 503과 콜드스타트는 ‘확장’ 문제로 묶어서 보자
Cloud Run의 503과 콜드스타트 지연은 서로 다른 문제가 아니라, 인스턴스가 준비되는 시간과 준비된 인스턴스가 처리할 수 있는 용량을 어떻게 설계했는지의 문제로 수렴합니다.
- 트래픽이 간헐적이면 min instances로 사용자 경험을 지키고
- 스파이크가 크면 concurrency/timeout/max instances로 폭주를 제어하며
- 앱은 **가벼운 이미지 + 빠른 초기화 + 외부 의존성 방어(타임아웃/풀)**로 기반 체력을 올리세요.
네트워크/인그레스 계층에서 비슷한 “겉으로는 503/접속 실패”가 발생하는 경우도 많습니다. 다른 플랫폼에서의 진단 흐름을 참고하면 원인 분류에 도움이 됩니다: EKS에서 Pod egress는 되는데 ingress만 실패할 때