Published on

Cloud Run 503·콜드스타트 7분 지연 해결 가이드

Authors

Cloud Run은 설정을 크게 건드리지 않아도 안정적으로 돌아가는 편이지만, 특정 조건이 겹치면 첫 요청이 수 분(심하면 7분) 동안 지연되거나, 그 과정에서 503이 튀는 케이스가 실제로 발생합니다. 특히 트래픽이 뜸한 서비스, 이미지가 큰 서비스, 외부 의존성이 많은 서비스에서 잘 드러납니다.

이 글은 “왜 7분까지 늘어났는지”를 플랫폼 레벨(프로비저닝) + 컨테이너 레벨(부팅) + 앱 레벨(초기화) + 네트워크/의존성 레벨로 쪼개서 확인하고, 재발 방지까지 묶어 정리합니다.

증상 정의: 503과 7분 지연은 같은 문제일까

Cloud Run에서 503은 여러 의미를 가집니다.

  • 인스턴스가 아직 준비되지 않음: 새 인스턴스를 띄우는 중인데 요청이 먼저 들어옴
  • 컨테이너가 기동했지만 포트를 열지 못함: 앱이 PORT로 리슨하기 전에 타임아웃
  • 요청 처리 중 앱이 죽거나 OOM: 컨테이너 재시작 루프
  • 업스트림(외부 API/DB) 대기: 앱이 초기화에서 외부 호출을 기다리다가 응답 지연, 그 사이 플랫폼 타임아웃으로 503

“7분” 같은 비정상적으로 긴 지연은 보통 아래 조합에서 나옵니다.

  1. 이미지 Pull 및 시작이 느림(대형 이미지, 콜드 노드) +
  2. 앱 초기화가 외부 의존성에 강하게 묶임(예: DB 마이그레이션, 시크릿/메타데이터 호출) +
  3. Cloud Run 요청/기동 타임아웃 및 동시성 설정이 부적절

먼저 확인할 것: 로그와 지표로 구간 나누기

1) Cloud Logging에서 503의 출처 확인

Cloud Run 서비스 로그에서 다음을 우선 찾습니다.

  • 요청 로그의 status=503Cloud Run 자체에서 반환인지
  • 컨테이너 로그에 앱 에러/패닉/OOM이 있는지
  • Container called exit(1) 류의 메시지

필터 예시(콘솔의 로그 탐색기에서):

  • 리소스: Cloud Run Revision
  • 조건: httpRequest.status=503

그리고 같은 시각대에 컨테이너 표준 출력/에러 로그를 같이 봅니다. 503이 찍히는데 앱 로그가 전혀 없다면, 앱까지 요청이 도달하지 못했을 확률이 큽니다(프로비저닝/기동 지연).

2) Cloud Monitoring에서 Cold start 지표 확인

Cloud Run은 인스턴스 수, 요청 지연, CPU/메모리 사용량을 통해 “어디서 막히는지” 감이 옵니다.

  • 새 리비전 배포 직후 인스턴스 증가가 느린가
  • 인스턴스는 올라왔는데 요청 지연만 긴가
  • CPU가 0에 가깝게 유지되는데 지연만 긴가(대기 I/O 가능성)

원인 1: 이미지가 너무 크거나 Pull이 느림

콜드스타트가 길어지는 가장 흔한 원인 중 하나가 대형 이미지입니다. 특히 다음이 겹치면 Pull 시간이 길어집니다.

  • 베이스 이미지가 무겁다(예: 풀 OS 이미지)
  • 멀티스테이지 빌드 없이 런타임에 빌드 툴체인까지 포함
  • 레이어 캐시가 깨져 매번 대량 레이어가 바뀜

해결: 멀티스테이지 + 슬림 베이스 + 캐시 안정화

Node.js 예시(멀티스테이지):

# syntax=docker/dockerfile:1

FROM node:20-bookworm AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/server.js"]
  • 런타임 이미지는 -slim 계열로 축소
  • npm ci 레이어가 불필요하게 깨지지 않도록 COPY 순서 고정

빌드 캐시가 자주 깨져 이미지가 비대해지는 문제는 Docker 빌드 최적화와도 연결됩니다. 빌드 캐시 관점은 이 글도 함께 보면 도움이 됩니다.

원인 2: 앱이 PORT 리슨 전에 “무거운 초기화”를 함

Cloud Run은 컨테이너가 지정된 포트(기본 8080, 환경변수 PORT)를 리슨해야 준비 완료로 봅니다. 그런데 앱이 다음을 “서버 리슨 이전”에 수행하면, 그 시간만큼 콜드스타트가 늘어납니다.

  • DB 연결 풀 생성 및 연결 테스트(타임아웃이 길면 수 분)
  • 외부 API 헬스체크
  • 마이그레이션 실행
  • 대용량 모델/파일 다운로드

해결: 리슨을 먼저, 초기화는 비동기로

Express 예시(서버 리슨 먼저):

import express from "express";

const app = express();

app.get("/healthz", (req, res) => res.status(200).send("ok"));

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log("listening on", port);
  // 무거운 초기화는 리슨 이후 비동기로
  warmup().catch((e) => console.error("warmup failed", e));
});

async function warmup() {
  // 예: DB 연결, 캐시 프라임 등
}

중요 포인트는 “첫 요청을 받기 전에 반드시 끝내야 하는 작업”과 “백그라운드로 돌려도 되는 작업”을 분리하는 것입니다. 첫 요청이 반드시 의존해야 한다면, 그 의존성의 타임아웃을 짧게 잡고 실패 시 빠르게 실패시키는 편이 7분 지연보다 낫습니다.

원인 3: 외부 의존성(특히 DB) 연결이 느리거나 막힘

7분 지연은 DB 연결 타임아웃(기본이 길게 잡힌 라이브러리)에서 자주 나옵니다.

  • Cloud SQL로 가는데 커넥터 설정이 잘못됨
  • VPC 커넥터 경유로 라우팅되는데 방화벽/NAT/라우트 문제
  • DNS가 느리거나 간헐 실패

해결: 연결 타임아웃을 명시하고, 실패를 빨리 감지

예: PostgreSQL 클라이언트에서 타임아웃을 명시합니다.

import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  statement_timeout: 5000,
  query_timeout: 5000,
  connectionTimeoutMillis: 5000
});

또한 Cloud Run에서 VPC 커넥터를 쓴다면, egress 설정(모든 트래픽 vs 프라이빗만)과 함께 DNS/라우팅을 점검해야 합니다. 쿠버네티스 사례지만 “DNS는 살아있는데 간헐 실패” 류의 패턴 분석은 진단 방식이 유사합니다.

원인 4: CPU 할당 정책 때문에 스타트업이 느려짐

Cloud Run은 요청 처리 중에만 CPU를 주는 모드가 기본입니다. 이 경우 “요청이 오기 전” 백그라운드 초기화가 느리거나 사실상 진행이 안 될 수 있습니다.

  • 첫 요청이 들어와야 CPU가 생김
  • 그런데 앱은 첫 요청을 처리하기 위해 초기화를 해야 함
  • 초기화가 CPU를 많이 쓰면 첫 요청 지연이 커짐

해결: Startup CPU boost 또는 항상 CPU 할당 고려

  • Startup CPU boost를 켜서 콜드스타트 구간 CPU를 더 받게 함
  • 또는 CPU를 항상 할당(비용 증가)하여 백그라운드 초기화 가능

이 설정은 “트래픽이 뜸한데 첫 요청이 항상 느리다” 유형에서 체감이 큽니다.

원인 5: 최소 인스턴스가 0이라 완전 콜드가 발생

트래픽이 없을 때 인스턴스가 0으로 내려가면, 다음 요청은 무조건 콜드스타트입니다. 서비스가 B2B, 배치성, 관리자 페이지처럼 “가끔 호출”되는 구조면 콜드스타트를 피하기 어렵습니다.

해결: min instances로 워밍 유지

운영에서 흔한 선택지는 다음입니다.

  • min instances=1로 최소 1개는 항상 유지
  • 트래픽이 많아지는 시간대만 스케줄링 워밍(Cloud Scheduler로 주기 호출)

비용과 지연 사이의 트레이드오프를 명확히 정하고 결정하는 게 좋습니다.

원인 6: 동시성(concurrency)과 타임아웃이 부적절

동시성이 너무 높을 때

동시성이 높으면 인스턴스 수를 덜 늘려도 되지만, 콜드스타트 직후 한 인스턴스에 요청이 몰려 앱 초기화/캐시 미스/커넥션 풀 경쟁이 심해질 수 있습니다.

  • 예: 동시성 80에서 첫 인스턴스에 대량 요청 유입
  • 초기화가 완료되기 전 요청들이 대기하다가 타임아웃

해결은 보통 다음 중 하나입니다.

  • 동시성을 낮춰 인스턴스가 더 빨리 늘어나게 유도
  • 앱 내부에서 준비 완료 전 요청을 빠르게 503으로 반환하고 재시도 유도(권장도는 케이스별)

요청 타임아웃이 너무 길 때

요청 타임아웃이 길면 “사용자는 계속 기다리고, 결국 실패”가 됩니다. 차라리 짧게 실패시키고 재시도하는 편이 사용자 경험이 나을 수 있습니다.

실전 체크리스트: 7분 지연을 30초 이하로 줄이는 순서

아래는 실제 트러블슈팅에서 효과가 큰 순서입니다.

  1. 로그로 구간 분리: 503이 플랫폼인지 앱인지부터
  2. 이미지 크기 축소: 멀티스테이지, 슬림 베이스, 불필요 파일 제거
  3. 서버 리슨을 먼저: 무거운 초기화는 리슨 이후 비동기
  4. 외부 의존성 타임아웃 단축: DB/DNS/API 연결 타임아웃을 초 단위로
  5. Startup CPU boost 적용: 콜드스타트 구간 가속
  6. min instances 적용: 최소 1개 워밍 유지(핵심 서비스면 강력 추천)
  7. 동시성 재조정: 초기 폭주를 줄이거나 스케일아웃 유도

배포/설정 예시: gcloud로 핵심 옵션 적용

아래는 자주 쓰는 옵션을 한 번에 반영하는 형태의 예시입니다(서비스/환경에 맞게 조정).

gcloud run deploy my-service \
  --image=asia-northeast3-docker.pkg.dev/myproj/myrepo/myimg:sha-123 \
  --region=asia-northeast3 \
  --port=8080 \
  --min-instances=1 \
  --concurrency=20 \
  --cpu=1 \
  --memory=512Mi \
  --timeout=30

주의: Startup CPU boost, CPU always allocated 같은 옵션은 콘솔/리전/런타임 세대에 따라 노출 방식이 다를 수 있어, 콘솔에서 토글을 확인한 뒤 동일 설정을 CLI로 맞추는 흐름이 안전합니다.

“503이 배포 직후만 난다”면: 리비전 전환과 캐시/드리프트도 점검

배포 직후에만 503이 난다면 다음도 함께 봐야 합니다.

  • 새 리비전이 뜨는 동안 트래픽 전환 비율
  • 환경변수/시크릿/권한이 리비전마다 달라지는 드리프트
  • IaC 적용 충돌로 설정이 의도와 다르게 반영

특히 Terraform로 Cloud Run과 관련 리소스를 관리할 때는 apply 충돌이나 드리프트로 인해 “분명 설정했는데 적용이 안 된 상태”가 생길 수 있습니다.

결론: 7분 콜드스타트는 “한 가지”가 아니라 “연쇄 지연”이다

Cloud Run에서 7분 지연과 503은 대개 단일 원인이 아니라,

  • 큰 이미지 Pull
  • PORT 리슨 전 초기화
  • 외부 의존성 타임아웃
  • CPU 정책 및 min instances

가 연쇄적으로 겹치며 만들어집니다.

가장 재현성과 효과가 큰 처방은 다음 조합입니다.

  • 이미지 다이어트(멀티스테이지) +
  • 리슨 먼저(초기화 분리) +
  • 의존성 타임아웃 단축 +
  • Startup CPU boost 또는 min instances 1

이 4가지만 제대로 적용해도 “첫 요청 7분” 같은 극단적 케이스는 대부분 10초에서 30초 수준으로 내려오고, 운영 체감상 503도 크게 줄어듭니다.