- Published on
GCP Cloud Run 503·콜드스타트 타임아웃 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Cloud Run을 운영하다 보면 평소엔 잘 되다가 트래픽이 몰리거나 오랜 유휴 뒤 첫 요청에서 503이 튀고, 로그에는 타임아웃 또는 컨테이너 시작 지연이 보이는 경우가 많습니다. 특히 min-instances=0로 비용 최적화해 둔 서비스에서 콜드스타트가 길어지면, 로드밸런서나 클라이언트가 먼저 포기하면서 503으로 관측됩니다.
이 글은 “왜 503이 나는지”를 모호하게 뭉개지 않고, Cloud Run의 요청 경로(로드밸런서, 큐잉, 인스턴스 생성, 컨테이너 부팅, 앱 준비, 다운스트림 의존성 연결)에서 병목 지점을 분해해 원인별로 해결책을 적용하는 방식으로 정리합니다.
1) Cloud Run 503을 먼저 분류해야 한다
Cloud Run의 503은 원인이 여러 층에 걸쳐 있습니다. 같은 503이라도 해결책이 정반대일 수 있어, 먼저 아래처럼 분류합니다.
1-1. 전형적인 503 유형
- 유휴 후 첫 요청에서만 503
- 콜드스타트 또는 초기화(의존성 연결, 마이그레이션, 모델 로딩)가 느림
- 트래픽 스파이크에서 503
- 인스턴스 생성 속도보다 요청 유입이 빠름
- 동시성 설정이 과도하거나(앱이 못 버팀) 너무 낮아서(인스턴스가 과도하게 필요) 스케일 지연
- 특정 엔드포인트에서만 503
- 해당 핸들러가 느리거나, 외부 API/DB 호출이 타임아웃
- 간헐적 503 + 로그에 메모리 OOM/프로세스 크래시
- 메모리 부족, 런타임 크래시로 인스턴스가 죽고 재기동 반복
1-2. 확인해야 할 관측 포인트
- Cloud Logging
- Request 로그에서
status=503인 항목의latency와trace확인 - 컨테이너 로그에서 부팅 직후 출력, 예외 스택트레이스, OOM 메시지 확인
- Request 로그에서
- Cloud Monitoring
Instance count,Request count,Request latency,Container startup latency(유사 지표) 확인
- Cloud Run 설정
min instances,max instances,concurrency,cpu,memory,startup probe(2세대),request timeout
2) 콜드스타트가 길어지는 핵심 원인 7가지
Cloud Run 콜드스타트는 대략 “인스턴스 생성 + 컨테이너 시작 + 앱 준비 완료”의 합입니다. 아래 항목 중 하나만 길어져도 첫 요청이 타임아웃나며 503이 됩니다.
2-1. 이미지가 크고 레이어 캐시 효율이 낮다
- 대형 베이스 이미지, 불필요한 빌드 산출물 포함, 패키지 설치가 런타임에 발생
- 해결
- 멀티 스테이지 빌드로 런타임 이미지를 최소화
- 의존성 설치는 빌드 단계에서 끝내고 런타임에는 실행만
예시: Node.js 멀티 스테이지 Dockerfile
# build stage
FROM node:20-bookworm AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# runtime stage
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY /app/package*.json ./
COPY /app/node_modules ./node_modules
COPY /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/server.js"]
2-2. 앱 시작 시 무거운 초기화를 한다
- 첫 요청 전에 모델 로딩, 대규모 설정 fetch, 마이그레이션, 외부 API warm-up 등을 수행
- 해결
- 초기화는 가능한 한 지연 로딩(lazy)하거나, 캐시 가능한 것은 빌드 시 포함
- “요청 처리 가능 상태”를 빠르게 만들고, 이후 백그라운드로 보강
2-3. CPU 설정 때문에 부팅이 느려진다
Cloud Run은 CPU 할당 정책에 따라 “요청 처리 중에만 CPU 제공”을 선택할 수 있습니다. 유휴 상태에서 CPU가 없으면, 요청이 들어온 뒤에야 초기화가 본격 진행되어 콜드스타트가 체감상 더 길어질 수 있습니다.
- 해결
- 콜드스타트 민감 서비스는 “항상 CPU 할당”을 검토
- 또는
min instances로 따뜻한 인스턴스를 유지
2-4. 동시성(concurrency)이 앱 특성과 맞지 않는다
동시성이 너무 높으면 한 인스턴스에 요청이 몰려서 CPU/메모리/스레드가 포화되고, 응답 지연이 누적되어 타임아웃 503으로 관측됩니다. 반대로 동시성이 너무 낮으면 인스턴스가 많이 필요해 스케일아웃이 늦어질 수 있습니다.
- 해결
- CPU 바운드면 동시성을 낮게, I/O 바운드면 적절히 높게
- 부하 테스트로
p95지연과 오류율 기준으로 최적점 찾기
2-5. 의존성(DB, Redis, 외부 API) 연결이 병목이다
콜드스타트 시점에 DB 커넥션 풀을 크게 잡아두면, 인스턴스가 늘어날 때 DB 연결 폭주가 발생하고 결국 실패가 503으로 전이됩니다.
- 해결
- 풀 크기를 인스턴스당 작게 시작하고 점진 확장
- Cloud SQL이라면 커넥터/프록시 사용, 커넥션 재사용 최적화
DB 커넥션 폭주 패턴과 차단 전략은 RDS 사례지만 개념은 동일합니다. 커넥션 풀과 프록시로 “서버리스 스케일링이 DB를 죽이는 문제”를 막는 관점은 아래 글도 참고할 만합니다.
2-6. 메모리 부족으로 OOM이 나고 재기동한다
- 증상
- 간헐적
503 - 로그에 OOM kill, 프로세스 종료
- 간헐적
- 해결
- 메모리 상향 또는 런타임 메모리 제한 튜닝
- 캐시/버퍼/이미지 처리 등 피크 메모리 사용 구간 점검
2-7. 플랫폼 타임아웃(요청 타임아웃, 프록시 타임아웃)
Cloud Run의 요청 타임아웃, 앞단 HTTPS LB/Cloud CDN, 클라이언트 타임아웃이 서로 다르면 “서버는 처리 중인데 앞단이 먼저 끊어 503”이 됩니다.
- 해결
- 엔드투엔드 타임아웃 예산을 정하고 계층별로 정렬
- 스트리밍/비동기 작업(큐, Pub/Sub, Cloud Tasks)으로 장기 작업 분리
타임아웃/헬스체크/프록시 계층에서 오류가 증폭되는 구조는 AWS ALB의 502/504 문제와도 유사합니다. 개념 정리용으로 참고할 수 있습니다.
3) 실전 해결 체크리스트: 비용과 안정성의 균형
여기서는 “503을 줄이는 방향”으로 우선순위를 제시합니다. 모든 설정을 최대로 올리면 비용이 급증하므로, 효과 대비 비용이 좋은 순으로 적용하는 것이 핵심입니다.
3-1. min instances로 콜드스타트를 구조적으로 제거
- 권장
- 사용자-facing API, 결제/로그인 등 실패 비용이 큰 경로는
min instances=1이상 - 트래픽 패턴이 뚜렷하면 스케줄 기반으로 시간대별
min instances조정
- 사용자-facing API, 결제/로그인 등 실패 비용이 큰 경로는
단점은 유휴 시간에도 비용이 발생한다는 점이지만, 첫 요청 503로 인한 이탈 비용이 더 크면 이게 가장 확실합니다.
3-2. 동시성과 CPU/메모리를 함께 튜닝한다
- 동시성을 올리면 인스턴스 수는 줄지만, 인스턴스당 자원 요구량이 늘어납니다.
- 동시성을 낮추면 인스턴스 수가 늘어 스케일아웃 지연이 문제될 수 있습니다.
실무 팁
- CPU 바운드(이미지 변환, 암호화, ML 추론)
- 동시성을 낮추고 CPU를 올리는 편이 안정적
- I/O 바운드(외부 API, DB 쿼리 중심)
- 동시성을 중간 이상으로 두고 타임아웃/재시도/서킷브레이커로 방어
3-3. 초기화 로직을 “요청 처리 가능” 기준으로 재설계
가장 흔한 실수는 “서버 시작 시 모든 준비를 끝내야 한다”는 접근입니다. Cloud Run에서는 인스턴스가 자주 생성될 수 있으므로, 초기화는 최소화해야 합니다.
예시: Express에서 지연 초기화 패턴
import express from 'express'
const app = express()
let clientPromise
function getClient() {
if (!clientPromise) {
clientPromise = (async () => {
// 예: 외부 의존성 연결, 비밀값 로딩 등
// 반드시 타임아웃을 둔다
const client = await createClientWithTimeout(2000)
return client
})()
}
return clientPromise
}
app.get('/healthz', (req, res) => {
// 콜드스타트 중에도 빠르게 200을 주고 싶다면
// "ready"와 "alive"를 분리하는 전략을 고려
res.status(200).send('ok')
})
app.get('/api', async (req, res) => {
const client = await getClient()
const data = await client.query('select 1')
res.json({ data })
})
app.listen(8080)
포인트
- 초기화는 “요청이 실제로 필요로 하는 시점”으로 미루되
- 무한 대기 방지를 위해 타임아웃을 넣고
- 실패 시 빠르게 오류를 반환하거나 대체 경로를 제공
3-4. 재시도는 반드시 백오프와 함께, 그리고 멱등성 고려
콜드스타트나 순간적인 스케일 지연은 짧은 시간 후엔 정상화될 수 있어 재시도가 효과적입니다. 다만 무작정 재시도하면 트래픽을 더 키워 503을 증폭시킵니다.
- 권장
- 지수 백오프 + 지터
GET등 멱등 요청 중심POST는 멱등 키를 도입하거나 큐 기반으로 전환
재시도/백오프 설계는 외부 API 429 대응 글이지만, 503에도 동일한 원칙이 적용됩니다.
예시: Node fetch 재시도(백오프 + 지터)
export async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let attempt = 0
while (true) {
try {
const res = await fetch(url, options)
if (res.status >= 500 && res.status <= 599) {
throw new Error(`server error: ${res.status}`)
}
return res
} catch (e) {
if (attempt >= maxRetries) throw e
const base = 200 * Math.pow(2, attempt)
const jitter = Math.floor(Math.random() * 100)
const delayMs = base + jitter
await new Promise(r => setTimeout(r, delayMs))
attempt += 1
}
}
}
3-5. 장기 작업은 요청-응답에서 분리한다
Cloud Run은 HTTP 요청 처리에 최적화되어 있습니다. 30초, 60초, 5분짜리 작업을 동기 처리하면 타임아웃과 503이 필연적으로 늘어납니다.
- 해결
- Cloud Tasks로 비동기 작업 큐잉
- Pub/Sub로 이벤트 기반 처리
- 작업 상태는 DB나 캐시에 저장하고 폴링/웹훅으로 전달
4) 503이 계속될 때의 디버깅 루틴
설정을 이것저것 바꾸기 전에, 아래 순서로 원인을 좁히면 시간을 크게 아낍니다.
4-1. 503 발생 시점의 로그를 하나의 트레이스로 묶기
- Cloud Logging에서
trace또는 요청 ID 기준으로- 요청 수신 시각
- 컨테이너 로그의 부팅/초기화 시각
- 외부 의존성 호출 시간
- 최종 응답까지 걸린 시간
이렇게 보면 “인스턴스가 늦게 떴는지”, “떴는데 앱이 늦는지”, “앱은 빠른데 DB가 느린지”가 분리됩니다.
4-2. 스케일아웃 지연인지, 인스턴스 내부 포화인지 확인
- 스케일아웃 지연 신호
- 인스턴스 수가 늦게 증가
- 큐잉이 늘고 첫 요청이 오래 기다림
- 내부 포화 신호
- 인스턴스 수는 충분한데
p95지연이 상승 - CPU 100% 또는 메모리 압박
- 인스턴스 수는 충분한데
4-3. 의존성 장애를 503으로 착각하지 않기
예를 들어 DB 커넥션 풀이 고갈되면 앱이 응답을 못 하고 결국 503처럼 보일 수 있습니다. 이 경우 Cloud Run 튜닝이 아니라 DB/풀/쿼리 최적화가 우선입니다.
5) 권장 설정 조합 예시
정답은 서비스마다 다르지만, 운영에서 자주 쓰는 출발점은 있습니다.
5-1. 사용자-facing REST API(응답 1초 내 목표)
min instances: 1 또는 2concurrency: 20~80에서 부하 테스트로 결정- CPU: 1 이상(지연 민감하면 상향)
- 메모리: OOM 없을 만큼 여유
- 타임아웃: 엔드포인트 SLA에 맞게 짧게(예: 10~30초)
- 재시도: 클라이언트에서 제한적으로, 백오프 필수
5-2. 배치성 웹훅 처리(지연 허용, 처리량 우선)
min instances: 0concurrency: 높게(앱이 I/O 바운드라면)- 타임아웃: 작업 길이에 맞추되, 가능하면 큐로 분리
6) 마무리: 503은 “증상”이고, 콜드스타트는 “설계 변수”다
Cloud Run의 503은 단일 원인이라기보다, 스케일링 방식과 초기화 설계, 동시성/리소스, 타임아웃 예산이 맞물릴 때 나타나는 결과입니다. 가장 빠른 해결은 min instances로 콜드스타트를 제거하는 것이고, 장기적으로는 이미지/초기화/의존성 연결/재시도 전략을 다듬어 “인스턴스가 언제 생겨도 빠르게 준비되는 앱”으로 만드는 것입니다.
운영 단계에서는 다음 3가지만 기억해도 재발률이 크게 줄어듭니다.
- 콜드스타트 민감 경로는
min instances로 보호 - 동시성은 성능이 아니라 “포화 모델”에 맞춰 결정
- 타임아웃과 재시도는 계층 전체에서 예산을 맞춘다