Published on

GCP Cloud Run 503·콜드스타트 폭증 해결 가이드

Authors

서버리스처럼 보이지만, Cloud Run의 503과 콜드스타트 폭증은 대부분 아주 현실적인 병목(용량/부팅/네트워크/의존성) 에서 시작합니다. 특히 트래픽이 갑자기 튀는 순간(캠페인/배치 종료/크론 동시 실행/큐 적체 해소)에 503이 늘어나면 “Cloud Run이 알아서 스케일해주는데 왜?”라는 질문이 나오는데, 실제로는 스케일이 되기 전까지의 공백 또는 새 인스턴스가 준비되기 전까지의 공백이 503으로 관측되는 경우가 많습니다.

이 글은 원인을 크게 3가지로 나눠 빠르게 좁히고, 설정과 코드 레벨에서 재발을 막는 방법을 다룹니다.

  • (A) 용량 부족/스케일 공백: 인스턴스가 늘기 전까지 요청이 밀려 503
  • (B) 부팅/초기화 지연(콜드스타트): 새 인스턴스가 뜨지만 준비가 늦어 503
  • (C) 의존성 병목(외부 API/DB/DNS/TLS): 앱은 떴는데 첫 요청에서 외부 호출로 지연·실패

1) 503을 먼저 분류해야 한다: 어디서 503이 났나

Cloud Run의 503은 “애플리케이션이 503을 반환”한 것일 수도 있고, “플랫폼/프록시 레이어에서 503을 만든 것”일 수도 있습니다. 분류가 되면 해결책이 거의 자동으로 따라옵니다.

1-1. 관측 포인트 3개

  1. Cloud Logging(요청 로그): httpRequest.status=503 + severity + jsonPayload 확인
  2. Cloud Monitoring(메트릭):
    • Request count / 5xx rate
    • Instance count
    • Container startup latency(간접적으로는 cold start 지표)
  3. 애플리케이션 로그: 503 시점에 앱이 살아있었는지(로그가 찍혔는지)

플랫폼에서 503이 나면 애플리케이션 로그가 거의 없거나, 요청이 앱까지 도달하지 못합니다.

1-2. 로그 탐색 쿼리 예시

아래는 Cloud Run 요청 로그에서 503만 빠르게 보는 예시입니다.

resource.type="cloud_run_revision"
httpRequest.status=503

추가로, 특정 서비스/리비전만 보려면:

resource.type="cloud_run_revision"
resource.labels.service_name="YOUR_SERVICE"
httpRequest.status=503

2) (A) 용량 부족/스케일 공백: 동시성·최대 인스턴스·큐잉을 점검

Cloud Run은 요청이 늘면 인스턴스를 늘리지만, 늘어나는 속도각 인스턴스가 처리할 수 있는 동시 요청 수가 현재 트래픽 패턴과 맞지 않으면 503이 발생합니다.

2-1. 동시성(concurrency) 튜닝: “CPU-bound vs I/O-bound”로 결정

  • CPU-bound(이미지 처리, PDF 생성, 암호화 등): concurrency를 낮게(예: 1~10)
  • I/O-bound(외부 API, DB 쿼리 대기): concurrency를 높게(예: 40~200)

동시성이 너무 높으면 한 인스턴스에 요청이 몰려 p95/p99 지연이 급증하고, 타임아웃/리트라이로 더 큰 폭주가 발생합니다.

gcloud 설정 예시

gcloud run services update YOUR_SERVICE \
  --concurrency=40 \
  --timeout=30s \
  --region=asia-northeast3

2-2. max-instances가 낮아 스케일이 막히는 경우

비용을 아끼려고 max-instances를 낮게 걸어두면, 스파이크에서 절대 처리량이 부족해지고 503이 늘어납니다.

gcloud run services update YOUR_SERVICE \
  --max-instances=200 \
  --region=asia-northeast3

2-3. “스파이크를 흡수할 버퍼”가 필요하면: Cloud Tasks/ Pub/Sub로 큐잉

HTTP 트래픽 스파이크를 그대로 Cloud Run에 던지면, 스케일 공백 동안 503이 발생할 수 있습니다. 업무 특성상 즉시 처리가 아니라면, Cloud Tasks나 Pub/Sub로 큐잉해서 Cloud Run이 일정 속도로 소비하도록 바꾸는 것이 가장 강력한 해결책입니다.

(큐잉은 재시도/중복 처리/멱등성까지 설계해야 하므로, 단순 설정 튜닝보다 근본적입니다.)


3) (B) 콜드스타트 폭증: 최소 인스턴스 + Startup CPU Boost + 초기화 최적화

콜드스타트는 “새 인스턴스 생성 + 컨테이너 시작 + 앱 부팅 + 준비 완료”까지의 시간입니다. 이 구간이 길어지면 스케일이 되어도 사용자 입장에서는 503/지연으로 보입니다.

3-1. 최소 인스턴스(min-instances): 가장 확실한 완화책

  • 트래픽이 상시 있거나, 특정 시간대에 급증한다면 min-instances를 1 이상으로 두는 것만으로도 체감이 크게 좋아집니다.
  • 비용은 늘지만, 503과 p99 지연을 돈으로 사는 가장 단순한 방법입니다.
gcloud run services update YOUR_SERVICE \
  --min-instances=2 \
  --region=asia-northeast3

3-2. Startup CPU Boost: “부팅만 빨리” 하고 싶을 때

Cloud Run에는 부팅 단계에서 CPU를 더 주는 옵션이 있습니다(환경/세대에 따라 콘솔에서 토글). 앱이 초기화에서 CPU를 많이 쓰는 경우(프레임워크 부팅, DI 컨테이너 구성, 번들 로딩 등) 효과가 큽니다.

체크 포인트:

  • 부팅 시간은 줄었는데 런타임 비용이 크게 늘지 않는지(부팅 구간만 부스트)
  • 부팅이 빨라져도 외부 의존성 대기가 길면 효과가 제한적

3-3. 초기화 최적화: “첫 요청”에 모든 걸 하지 말기

콜드스타트 폭증의 흔한 패턴:

  • 앱 시작 시 DB 마이그레이션/스키마 체크
  • 외부 API 토큰 발급/키 로딩을 동기적으로 수행
  • 큰 파일(모델, 룰셋, 인증서 번들)을 매번 로딩

원칙:

  • 부팅은 최소화하고, 필요하면 lazy init + 캐시
  • 단, 첫 요청 지연이 SLA를 깨면 미리 워밍업(min-instances)로 해결

Node.js(Express) 예시: DB 연결은 “한 번만” 만들고 재사용

import express from "express";
import { Pool } from "pg";

const app = express();

// 전역 풀: 인스턴스 생명주기 동안 재사용
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 5,
  idleTimeoutMillis: 30000,
});

app.get("/healthz", async (req, res) => {
  // 헬스체크는 외부 의존성을 꼭 확인할지 정책 결정 필요
  res.status(200).send("ok");
});

app.get("/users/:id", async (req, res) => {
  const { rows } = await pool.query("select * from users where id=$1", [req.params.id]);
  res.json(rows[0] ?? null);
});

const port = process.env.PORT || 8080;
app.listen(port, () => console.log(`listening on ${port}`));

> DB/TLS/DNS 지연이 부팅을 잡아먹는 경우도 많습니다. 쿠버네티스에서 TLS 지연을 진단하는 관점은 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS 글의 접근이 Cloud Run 외부 호출 문제를 볼 때도 도움이 됩니다(원인 분해 방식).


4) (C) 의존성 병목: DNS/TLS/커넥션/레이트리밋이 첫 요청을 죽인다

Cloud Run 자체는 잘 뜨는데, 첫 요청에서 외부 서비스 호출이 느리거나 실패하면 503/타임아웃/리트라이 폭풍이 납니다. 특히 다음이 자주 문제입니다.

  • DB 커넥션 폭증(스파이크 시 커넥션 제한 초과)
  • 외부 API 레이트리밋으로 429/5xx → 재시도로 더 악화
  • DNS 이슈 또는 TLS 핸드셰이크 지연
  • VPC Connector/NAT 경유 시 egress 병목

이 경우 해결의 핵심은 커넥션 재사용 + 타임아웃/재시도 정책 + 동시성 제어입니다.

4-1. HTTP 클라이언트 keep-alive를 켜라(Node.js)

기본 설정에서 keep-alive가 꺼져 있거나, 라이브러리/런타임 설정이 애매하면 요청마다 새 TCP/TLS 연결을 만들며 지연이 커집니다.

import https from "https";
import fetch from "node-fetch";

const agent = new https.Agent({
  keepAlive: true,
  maxSockets: 50,
  timeout: 10_000,
});

export async function callApi(url) {
  const res = await fetch(url, {
    agent,
    // 타임아웃/재시도는 라이브러리별로 명시적으로 설정 권장
  });
  if (!res.ok) throw new Error(`upstream ${res.status}`);
  return res.json();
}

4-2. 재시도는 “무조건”이 아니라 “조건부 + 지터”로

503이 발생하면 클라이언트/게이트웨이가 재시도를 걸고, 그게 다시 503을 만들며 폭증합니다.

  • 재시도 대상: 네트워크 오류, 502/503/504 등 일부만
  • 재시도 횟수: 1~2회로 제한
  • 지터(jitter): 동시에 재시도하지 않게 랜덤 지연

4-3. DB 커넥션 풀 제한과 Cloud Run 동시성의 곱을 계산

예:

  • concurrency=80
  • max-instances=100
  • 각 인스턴스가 DB 커넥션을 10개까지 열 수 있음

최악의 순간 DB에는 1,000 커넥션이 몰립니다. DB가 감당 못 하면 첫 요청부터 실패하고, 재시도까지 겹쳐 장애처럼 보입니다.

해결:

  • 인스턴스당 풀 크기를 줄이기
  • concurrency를 낮추기
  • DB 프록시/풀러(예: Cloud SQL Auth Proxy, PgBouncer 등) 사용 고려

5) Cloud Run 설정 체크리스트(실전)

아래는 503/콜드스타트 이슈에서 가장 많이 “놓친 설정”들입니다.

5-1. 타임아웃(timeout)과 업스트림 타임아웃을 맞춰라

  • Cloud Run 요청 timeout이 60s인데, 내부에서 120s 기다리면 의미가 없습니다.
  • 반대로 Cloud Run timeout이 너무 짧으면 콜드스타트/초기화에서 쉽게 끊깁니다.
gcloud run services update YOUR_SERVICE \
  --timeout=60s \
  --region=asia-northeast3

5-2. CPU/메모리 부족은 “느린 콜드스타트”로 나타난다

메모리가 부족하면 OOM이 나기도 하지만, 그 이전에 GC/스와핑 같은 현상으로 부팅이 느려집니다.

gcloud run services update YOUR_SERVICE \
  --cpu=2 \
  --memory=1Gi \
  --region=asia-northeast3

5-3. readiness/health 엔드포인트를 가볍게 유지

헬스체크에서 DB를 강하게 의존하면, DB가 잠깐 느려져도 인스턴스가 준비 실패로 간주되어 스케일이 더 꼬일 수 있습니다.

권장 패턴:

  • /healthz: 프로세스 생존 확인(가벼움)
  • /readyz: 의존성까지 포함(필요 시)

6) “재현 → 측정 → 고치기”를 위한 부하 테스트 루틴

콜드스타트 폭증은 재현이 어려워 보이지만, 의도적으로 만들 수 있습니다.

6-1. 강제 콜드스타트 조건 만들기

  • min-instances=0
  • 트래픽을 끊고(수 분) 다시 스파이크
  • 새 리비전 배포 직후 트래픽 유입

6-2. k6로 간단 스파이크 테스트

import http from "k6/http";
import { sleep } from "k6";

export const options = {
  stages: [
    { duration: "10s", target: 0 },
    { duration: "10s", target: 200 }, // 스파이크
    { duration: "30s", target: 200 },
    { duration: "10s", target: 0 },
  ],
};

export default function () {
  http.get("https://YOUR_RUN_URL/healthz");
  sleep(1);
}

이 테스트를 돌리면서 다음을 같이 봅니다.

  • 503 발생 시점에 instance count가 어떻게 변하는지
  • p95/p99 latency가 언제 급증하는지
  • 애플리케이션 로그가 찍히는지(요청이 도달했는지)

7) 자주 나오는 원인별 처방전(요약)

7-1. 503이 스파이크 초반에만 튄다

  • min-instances 올리기
  • concurrency 조정(너무 높으면 지연 폭증)
  • max-instances 제한 해제/완화
  • 큐잉(Cloud Tasks/PubSub) 도입 고려

7-2. 배포 직후 503이 튄다

  • 새 리비전 부팅 시간 단축(Startup CPU Boost, 초기화 최적화)
  • 점진적 트래픽 이동(트래픽 분할)
  • 워밍업 요청(내부에서 주기적으로 호출) + min-instances

7-3. 외부 API/DB 호출이 많은 서비스에서만 503

  • keep-alive/커넥션 풀
  • 타임아웃/재시도 정책 정리
  • 레이트리밋 대응(백오프, 캐시)

네트워크 병목을 추적하는 방법론은 쿠버네티스 환경에서 egress를 파고드는 글인 EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법도 참고할 만합니다. Cloud Run에서도 VPC 커넥터/NAT/외부 구간이 병목이면 “증상-지표-가설-검증” 흐름이 동일합니다.


8) 결론: Cloud Run 503은 ‘스케일’이 아니라 ‘준비’ 문제인 경우가 많다

Cloud Run에서 503과 콜드스타트 폭증을 잡는 가장 빠른 길은, 원인을 (A) 용량 부족, (B) 부팅 지연, (C) 의존성 병목으로 나눠 각각에 맞는 레버를 당기는 것입니다.

  • 트래픽 스파이크를 즉시 흡수해야 하면: min-instances + 적절한 concurrency + max-instances
  • 부팅이 느리면: Startup CPU Boost + 초기화 최소화 + 이미지/런타임 최적화
  • 외부 호출이 문제면: 커넥션 재사용 + 타임아웃/재시도 설계 + 풀 크기/동시성 계산

마지막으로, 503을 완전히 없애려 하기보다 p95/p99 지연과 5xx 비율을 SLO로 정의하고, 스파이크 시나리오를 k6 등으로 정기적으로 재현해 “설정 변경이 실제로 효과가 있는지”를 검증하는 루틴을 만들면 재발 확률이 크게 줄어듭니다.