Published on

GCP Cloud Run 503·콜드스타트 10분 지연 진단

Authors

Cloud Run은 “서버리스라 운영 부담이 적다”는 기대가 크지만, 트래픽이 끊겼다가 다시 들어오는 순간 503이 터지거나 콜드스타트가 비정상적으로 10분까지 늘어지면 이야기가 달라집니다. 특히 10분 지연은 단순한 이미지 풀(pull) 지연이나 자바 워밍업 같은 수준을 넘어, 요청이 라우팅되기 전 단계(프로비저닝/네트워크/의존성)에서 막히는 케이스가 많습니다.

이 글은 “원인을 우아하게 추측”하는 글이 아니라, 10분 안에 범위를 좁히는 체크리스트Cloud Logging/Monitoring에서 바로 확인할 쿼리와 지표, 그리고 재현/완화 설정을 묶은 실전 가이드입니다.

> 네트워크 타임아웃/엔드포인트 이슈가 의심될 때는 VPC·NAT·DNS 관점의 진단법도 함께 보세요: EKS STS 엔드포인트 타임아웃 - VPC·NAT·DNS 해결

1) 먼저 결론부터: “10분 콜드스타트”가 의미하는 것

정상적인 Cloud Run 콜드스타트는 보통 수 초~수십 초(언어/이미지 크기/초기화 로직에 따라)입니다. 10분은 대개 아래 중 하나입니다.

  • 컨테이너가 READY 상태로 못 올라옴
    • 앱이 포트를 열지 못함(리스닝 실패)
    • startup probe/헬스체크에 계속 실패
    • 초기화 코드가 외부 의존성(DB, Redis, 외부 API)에 매달려 무한 대기
  • 트래픽이 인스턴스로 라우팅되기 전에 막힘
    • VPC Connector/NAT/DNS 문제로 egress가 막혀 초기화가 지연
    • Secret Manager/Artifact Registry 접근 실패
  • 요청이 들어오긴 했지만 플랫폼이 503을 반환
    • 인스턴스가 없고(또는 준비 중) 동시성/최대 인스턴스 제한으로 큐잉이 길어짐
    • “인스턴스 준비 실패”로 결국 503

핵심은 “콜드스타트가 느리다”가 아니라 어느 단계에서 느린지를 가르는 것입니다.

2) 10분 트리아지 로드맵(순서대로)

아래 순서로 보면 대부분의 케이스를 빠르게 좁힐 수 있습니다.

  1. 503이 플랫폼(Cloud Run)에서 난 건지, 앱이 503을 응답한 건지 구분
  2. 해당 시간대 Revision/배포 변경 여부 확인
  3. 인스턴스 생성/시작 실패 로그 확인(가장 중요)
  4. 요청 지연이 “큐잉”인지 “앱 처리”인지 메트릭으로 분해
  5. VPC Connector/NAT/DNS/외부 의존성 확인
  6. 완화(최소 인스턴스/동시성/타임아웃/초기화 분리) 적용

3) 503의 정체: 플랫폼 503 vs 애플리케이션 503

3.1 Cloud Logging에서 503 소스 구분 쿼리

Cloud Run 요청 로그에서 httpRequest.status=503만 보면 “503이네”로 끝납니다. 중요한 건 어디서 503이 만들어졌는지입니다.

resource.type="cloud_run_revision"
httpRequest.status=503
severity>=DEFAULT

여기서 다음을 같이 봅니다.

  • jsonPayload.statusDetails (혹은 유사 필드): 플랫폼이 넣는 상세 사유가 뜨는 경우가 많습니다.
  • jsonPayload.latency 또는 httpRequest.latency: 503이 즉시 떨어지는지(라우팅/프로비저닝 문제) vs 오래 기다리다 떨어지는지(큐잉/의존성 문제)
  • trace/spanId: Cloud Trace 연동 중이면 병목 위치를 더 빨리 찾습니다.

3.2 “앱이 503을 반환”한 경우의 특징

  • 애플리케이션 로그에 503 응답 생성 코드/예외가 남습니다.
  • 같은 시점에 컨테이너는 정상적으로 READY 상태였을 가능성이 큽니다.
  • DB 풀 고갈, 외부 API rate limit, 내부 타임아웃 설계 문제일 수 있습니다.

이 경우 재시도/백오프/타임아웃 설계가 중요합니다. gRPC 기반이면 특히 요청이 오래 붙잡히며 장애를 증폭시킬 수 있어요: Go gRPC DEADLINE_EXCEEDED 원인과 재시도·타임아웃 설계

4) “인스턴스가 안 뜬다/준비가 안 된다”를 가장 빨리 확인하는 법

10분 지연의 다수는 여기서 걸립니다.

4.1 Revision 이벤트/시작 실패 로그 확인

Cloud Run 콘솔에서 해당 서비스 → Revisions → 문제 시간대 revision 클릭 후, 로그에서 다음 키워드를 찾습니다.

  • Container failed to start
  • Failed to start and then listen on the port defined by the PORT environment variable
  • Health check failed
  • Startup probe failed
  • Image pull failed
  • Permission denied (Secret/Registry 접근)

로그 탐색기에서 더 직접적으로는 아래처럼 검색합니다.

resource.type="cloud_run_revision"
(
  "Container failed to start" OR
  "PORT" OR
  "Health check" OR
  "Startup" OR
  "Image pull" OR
  "permission" OR
  "denied"
)

4.2 PORT 리스닝 실패(가장 흔한 초보 함정)

Cloud Run은 PORT 환경변수(기본 8080)에 반드시 바인딩해야 합니다. 로컬에서는 3000으로 잘 뜨는데 Cloud Run에서는 8080을 열지 않아 무한 대기 → 결국 503으로 이어집니다.

Node.js 예시:

import express from "express";

const app = express();
const port = process.env.PORT || 8080;

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

app.listen(port, "0.0.0.0", () => {
  console.log(`listening on ${port}`);
});

Java/Spring Boot는 server.port=${PORT:8080} 형태로 맞추는지 확인하세요.

5) “콜드스타트 10분”을 만드는 4대 원인과 진단 포인트

5.1 VPC Connector/NAT/DNS로 egress가 막혀 초기화가 멈춤

Cloud Run이 VPC Connector를 통해 사설망으로 나가도록 구성했거나, 모든 egress를 VPC로 보내는 설정이라면 NAT/DNS 문제가 곧바로 콜드스타트 지연으로 나타납니다.

증상 패턴:

  • 컨테이너는 뜨지만 앱 초기화에서 DB/Redis/외부 API 호출이 무한 대기
  • 로그에 타임아웃이 없거나(기본 소켓 타임아웃이 매우 김) “계속 대기”
  • 특정 리전/서브넷에서만 재현

확인할 것:

  • Serverless VPC Access Connector 상태/용량
  • Cloud NAT 설정(서브넷/라우팅/포트 고갈)
  • Cloud DNS/외부 DNS 해석 지연

NAT 비용/트래픽 이상 징후까지 같이 보이면 이 글도 도움이 됩니다: VPC NAT Gateway 비용 폭증 10분 진단·절감

5.2 외부 의존성(Secret Manager, DB, Redis, 외부 API) 초기화가 “부팅 경로”에 있음

콜드스타트 때 다음을 “앱 시작 시점”에 해버리면, 장애가 곧바로 인스턴스 준비 지연으로 전파됩니다.

  • Secret Manager에서 모든 시크릿을 동기 로딩
  • DB 마이그레이션 실행
  • Redis 연결이 될 때까지 무한 재시도
  • 외부 API에 웜업 호출

해결 방향:

  • 초기화는 빠르게 끝내고, 의존성 연결은 요청 처리 단계에서 lazy 하게
  • 반드시 타임아웃을 짧게(예: 2~5초) 걸고 실패 시 degrade
  • readiness(준비)와 liveness(생존)를 분리

Python(예: requests) 타임아웃 미설정은 재앙입니다.

import os
import requests

API = os.environ["UPSTREAM_URL"]

def call_upstream():
    # (connect timeout, read timeout)
    r = requests.get(API, timeout=(2, 3))
    r.raise_for_status()
    return r.json()

5.3 동시성/최대 인스턴스 제한으로 큐잉이 10분까지 늘어남

“콜드스타트가 10분”처럼 보이지만 사실은 요청이 인스턴스에 배정되지 못하고 대기하는 경우가 있습니다.

  • max-instances가 너무 낮음(또는 1)
  • concurrency가 너무 낮고, 요청 처리 시간이 길어짐
  • 트래픽 스파이크 + scale-out 속도보다 요청 유입이 큼

진단 포인트:

  • Cloud Monitoring에서 Request count는 증가하는데 Instance count가 제한에 걸림
  • Request latency가 길고, 애플리케이션 로그는 아예 남지 않음(요청이 컨테이너까지 못 감)

완화:

  • max-instances 상향
  • concurrency 상향(단, 앱이 thread-safe/async-friendly 해야 함)
  • 최소 인스턴스(min instances)로 워밍 유지

gcloud 예시:

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

5.4 이미지/런타임 자체가 너무 무거움(풀/압축해제/자바 워밍업)

이 경우는 10분까지 가는 건 드물지만, 네트워크가 느리거나 이미지가 비정상적으로 크면 가능합니다.

체크:

  • 컨테이너 이미지 크기(수 GB면 위험)
  • 불필요한 빌드 아티팩트/캐시 포함 여부
  • 베이스 이미지(예: ubuntu:latest) 남용

Dockerfile 최적화 예시(멀티스테이지 + slim):

# build stage
FROM node:20 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 --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["dist/server.js"]

6) 503 재현/관측을 위한 최소 계측(로그 + 타임아웃)

6.1 요청 단위로 “대기 vs 처리”를 나누는 로그

애플리케이션 레벨에서 최소한 아래는 남기세요.

  • 요청 수신 시각
  • 핵심 의존성(DB/Redis/외부 API) 호출 시작/종료
  • 타임아웃 발생 여부

Node 예시(간단한 미들웨어):

app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    console.log(JSON.stringify({
      path: req.path,
      status: res.statusCode,
      ms: Date.now() - start,
    }));
  });
  next();
});

6.2 타임아웃을 “기본값에 맡기지 않기”

콜드스타트 10분 케이스의 상당수는 소켓 타임아웃/DB 커넥션 타임아웃이 길거나 무한대라서 발생합니다. 언어/라이브러리별로 아래를 강제하세요.

  • HTTP client: connect/read timeout
  • DB pool: connection acquisition timeout
  • DNS/Resolver: 캐시/timeout(가능한 범위 내)

7) 운영 설정으로 바로 줄일 수 있는 완화책

7.1 최소 인스턴스(min instances)

  • 트래픽이 간헐적이고 첫 요청 UX가 중요하면 가장 효과적
  • 비용은 증가하지만 “첫 요청 503/지연”을 크게 줄임

7.2 Startup CPU boost / CPU always allocated

  • 언어 런타임 초기화(특히 JVM)에서 효과
  • 단, 10분 지연이 네트워크/의존성 대기라면 효과가 제한적

7.3 헬스체크(준비/생존) 설계

  • readiness는 “외부 의존성까지 모두 정상”을 강제하면 오히려 부팅이 길어질 수 있음
  • 권장: readiness는 프로세스가 요청을 처리할 준비(큐/스레드/필수 설정 로딩) 수준으로 두고, 의존성 장애는 요청 처리에서 빠르게 fail/degrade

간단한 /healthz 예시:

app.get("/healthz", (req, res) => {
  // 프로세스/이벤트루프/필수 설정 로딩 여부만 확인
  res.status(200).send("ok");
});

8) “진짜 원인”을 좁히는 체크리스트(현장용)

  • 503이 앱 응답인가, 플랫폼 응답인가?
  • 해당 시간대에 revision 교체/환경변수/시크릿/네트워크 설정 변경이 있었나?
  • Container failed to start, PORT, Startup probe failed 로그가 있나?
  • 요청 로그의 latency가 0수백 ms로 바로 503인가, 수십수백 초 기다리다 503인가?
  • VPC Connector를 쓰는가? Cloud NAT/DNS 이슈 가능성은?
  • 외부 의존성 호출에 타임아웃이 설정돼 있나?
  • max instances/concurrency 제한으로 큐잉이 발생했나?
  • 이미지 크기/부팅 시간이 과도하지 않나?

9) 마무리: 10분 지연은 “콜드스타트”가 아니라 “부팅 경로 장애”일 확률이 높다

Cloud Run에서 503과 10분 지연이 함께 보이면, 단순히 “서버리스가 느리다”가 아니라 인스턴스가 준비되지 못하는 구조적 원인(포트 리스닝/헬스체크/네트워크 egress/의존성 무한대기/스케일 제한)을 의심해야 합니다.

가장 빠른 접근은:

  1. 시작 실패 로그부터 확인하고, 2) 큐잉 vs 앱 처리를 분리한 뒤, 3) VPC/NAT/DNS와 의존성 타임아웃을 점검하는 것입니다.

원하시면 사용 중인 스택(언어/프레임워크), VPC Connector 사용 여부, max instances/concurrency, 그리고 문제 시간대의 대표 로그 한두 줄(민감정보 제거)을 기준으로 “어디가 10분을 만들었는지”를 더 구체적으로 같이 좁혀드릴게요.