Published on

GCP Cloud Run 503·콜드스타트 지연 해결법

Authors

Cloud Run은 운영 부담이 적고 확장이 빠르지만, 트래픽이 들쭉날쭉하거나 의존 서비스가 많은 API에서는 503과 콜드스타트 지연이 체감 성능을 크게 떨어뜨립니다. 특히 “가끔만 느리다” “로그는 정상인데 사용자만 실패한다” 같은 형태로 나타나서 원인 파악이 어렵습니다.

이 글에서는 Cloud Run의 503어떤 503인지부터 분류하고, 콜드스타트를 줄이는 설정과 코드 패턴, 그리고 관측 지표까지 한 번에 정리합니다.

관련해서 쿠버네티스 환경에서 준비 상태 문제를 진단하는 접근도 도움이 됩니다. Cloud Run은 쿠버네티스가 직접 노출되진 않지만, 증상과 원인(준비 전 트래픽 유입, 타임아웃, 외부 의존성) 구조가 유사합니다. 참고: EKS에서 Readiness 실패인데 로그는 정상일 때

1) Cloud Run에서 503이 의미하는 것부터 분해하기

Cloud Run의 503 Service Unavailable은 크게 3가지로 나눠 보는 게 좋습니다.

1-1. 인스턴스가 준비되기 전: 콜드스타트·프로브 유사 문제

  • 트래픽이 들어왔는데 컨테이너가 아직 뜨는 중
  • 애플리케이션은 부팅 중이라 포트를 열지 못함
  • 부팅 후에도 DB 연결, 마이그레이션, 키 로딩 등으로 첫 요청이 오래 걸림

이 경우는 지연이 먼저 나타나고, 타임아웃이 겹치면 503으로 보일 수 있습니다.

1-2. 동시성 과부하: 인스턴스는 떠 있지만 처리량이 부족

  • 한 인스턴스에 너무 많은 동시 요청이 몰림
  • CPU가 부족하거나, 블로킹 I/O 때문에 이벤트 루프가 막힘
  • 외부 API 호출이 느려서 워커가 점유됨

이 경우는 5xx가 늘고, 지연 분포가 두꺼워집니다. 특히 Node.js, Python, Java에서 블로킹/동기 작업이 섞이면 체감이 급격히 나빠집니다. 비슷한 관점의 진단법은 런타임이 멈춘 것처럼 보이는 상황에서 유용합니다. 참고: Rust Tokio runtime 멈춤? 블로킹 I/O 진단법

1-3. 플랫폼/네트워크 계층: 연결 실패, VPC 커넥터, NAT, DNS

  • Serverless VPC Access를 붙였는데 NAT 또는 라우팅이 병목
  • 외부로 나가는 egress가 막혀 재시도 폭증
  • DNS 지연으로 외부 의존성이 느려짐

이 경우는 애플리케이션 로그에 “상위 타임아웃”만 남고, 원인은 네트워크에 있는 경우가 많습니다.

2) 가장 먼저 확인할 체크리스트 (10분 컷)

아래는 Cloud Run 503과 콜드스타트에서 가장 자주 걸리는 설정들입니다.

2-1. 컨테이너 포트와 리슨 주소

Cloud Run은 기본적으로 PORT 환경변수(대개 8080)로 들어오는 트래픽을 컨테이너가 받아야 합니다.

  • 포트를 다른 값으로 열었는데 PORT를 무시함
  • 127.0.0.1로만 바인딩해서 외부에서 접근 불가

Node.js 예시

import express from 'express';

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

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

// 반드시 0.0.0.0 로 바인딩
app.listen(port, '0.0.0.0', () => {
  console.log(`listening on ${port}`);
});

2-2. 요청 타임아웃과 상위 프록시 타임아웃 불일치

Cloud Run 서비스 타임아웃(예: 60초)보다 상위(Load Balancer, CDN, 클라이언트)의 타임아웃이 짧으면, 사용자는 실패를 보지만 서버는 계속 처리 중일 수 있습니다.

  • Cloud Run timeout: 60초
  • 클라이언트 timeout: 10초
  • 첫 요청(콜드스타트 포함): 12초

이때 사용자는 실패(종종 5xx로 보임), 서버는 뒤늦게 성공 로그를 남깁니다.

2-3. 최소 인스턴스 min-instances가 0인지

min-instances가 0이면 유휴 시 인스턴스가 0으로 내려가며 콜드스타트가 발생합니다. 지연 민감 API라면 최소 1 이상을 고려하세요.

3) 콜드스타트 지연을 줄이는 설정 전략

콜드스타트는 완전히 없애기 어렵지만, “사용자가 느끼는 첫 요청”을 줄이는 방법은 많습니다.

3-1. min-instances로 워밍 유지

  • 장점: 첫 요청 지연을 크게 줄임
  • 단점: 비용 증가

운영 팁:

  • B2C API는 최소 1로 시작해 실제 트래픽 패턴을 보고 조정
  • 배치/내부 API는 0 유지 + 스케줄러로 주기적 웜업

3-2. CPU 할당 정책: 요청 중만 CPU vs 항상 CPU

Cloud Run은 설정에 따라 요청 처리 중에만 CPU를 주거나, 항상 CPU를 줄 수 있습니다.

  • 요청 중만 CPU: 유휴 비용 절감, 대신 유휴 상태에서 백그라운드 초기화 불가
  • 항상 CPU: 유휴 중에도 초기화/캐시 준비 가능, 비용 증가

콜드스타트가 큰 서비스는 항상 CPU가 체감 개선에 도움이 될 때가 많습니다. 예를 들어 앱 부팅 후 캐시 워밍, DNS 프리패치, 커넥션 풀 준비 등을 유휴 시간에 해둘 수 있습니다.

3-3. 동시성 concurrency 조절

동시성이 높으면 인스턴스 수가 덜 늘어 비용은 줄지만, 한 인스턴스가 처리해야 할 동시 요청이 늘어 지연과 503 위험이 커집니다.

  • CPU 1, 메모리 512Mi 같은 작은 인스턴스에서 동시성 80은 종종 과함
  • 외부 API 호출이 많은 서비스는 동시성이 높을수록 커넥션/소켓이 고갈될 수 있음

경험칙:

  • CPU 1 기준으로 동시성 10부터 시작해 지표를 보고 올리기
  • CPU 2 이상이면 20~40도 가능하지만, 앱 특성에 따라 다름

3-4. 인스턴스 크기(특히 CPU) 올리기

콜드스타트는 “컨테이너 시작 + 앱 초기화”의 합입니다. 앱 초기화가 CPU 바운드(클래스 로딩, JIT, 번들 로딩, 암호화 키 파싱)라면 CPU를 올리는 게 가장 단순하고 확실한 개선입니다.

4) 503을 만드는 흔한 코드 패턴과 개선

4-1. 부팅 시 외부 의존성에 강하게 결합

앱 시작 시 DB, Redis, 외부 API가 잠깐만 느려도 컨테이너가 준비되지 못해 첫 요청이 실패합니다.

개선 방향:

  • “부팅 시 필수”와 “요청 시 필요”를 분리
  • 부팅 시에는 최소한의 준비만 하고, 나머지는 지연 초기화(lazy init)
  • 외부 의존성은 타임아웃을 짧게, 재시도는 지수 백오프

Python 예시: 전역에서 무거운 초기화를 피하기

import os
import time
from flask import Flask

app = Flask(__name__)

_client = None

def get_client():
    global _client
    if _client is None:
        # 여기서 무거운 초기화(예: 외부 연결)를 수행
        time.sleep(0.2)
        _client = object()
    return _client

@app.get('/healthz')
def healthz():
    return 'ok', 200

@app.get('/api')
def api():
    c = get_client()
    return {'ok': True}, 200

if __name__ == '__main__':
    port = int(os.environ.get('PORT', '8080'))
    app.run(host='0.0.0.0', port=port)

핵심은 “컨테이너가 트래픽을 받을 준비”를 빨리 끝내는 것입니다.

4-2. 블로킹 작업을 요청 경로에 넣기

예: 이미지 변환, PDF 생성, 대용량 압축, 암호화, 동기 파일 I/O.

개선 방향:

  • 무거운 작업은 Cloud Tasks + 별도 Cloud Run worker로 분리
  • 또는 Pub/Sub 비동기 처리
  • 요청은 빠르게 202로 응답하고 결과는 폴링/웹훅

4-3. 커넥션 풀/소켓 고갈

동시 요청이 늘 때 DB 커넥션 풀이 작거나, 외부 HTTP 클라이언트가 keep-alive 없이 매번 연결하면 지연이 폭발합니다.

Node.js 예시: keep-alive 에이전트

import https from 'https';
import fetch from 'node-fetch';

const agent = new https.Agent({ keepAlive: true, maxSockets: 100 });

export async function callUpstream(url) {
  const res = await fetch(url, { agent, timeout: 3000 });
  if (!res.ok) throw new Error(`upstream status ${res.status}`);
  return res.text();
}

주의: 라이브러리마다 keep-alive 설정 방식이 다릅니다. 기본값이 비효율적인 경우가 많습니다.

5) Cloud Run 설정 예시 (gcloud)

아래는 503과 콜드스타트 완화에 자주 쓰는 조합입니다. 서비스 특성에 맞게 조정하세요.

gcloud run deploy my-api \
  --image=asia-northeast3-docker.pkg.dev/myproj/myrepo/my-api:2026-02-24 \
  --region=asia-northeast3 \
  --min-instances=1 \
  --max-instances=50 \
  --concurrency=20 \
  --cpu=2 \
  --memory=1Gi \
  --timeout=60 \
  --set-env-vars=NODE_ENV=production

설명:

  • min-instances=1: 콜드스타트 빈도 감소
  • cpu=2: 초기화 및 피크 처리 개선
  • concurrency=20: 과도한 동시성으로 인한 tail latency 방지
  • timeout=60: 상위 타임아웃과 함께 조정 필요

6) 관측으로 원인을 확정하는 방법 (지표·로그)

“느리다/503이다”를 해결하려면, 아래 질문에 답할 수 있어야 합니다.

6-1. 503이 언제 늘어나는가

  • 배포 직후에만 늘어나는가
  • 트래픽 스파이크 때만 늘어나는가
  • 특정 리전/특정 ISP에서만 늘어나는가

Cloud Monitoring에서 확인할 것:

  • 요청 수, 오류율, p95/p99 지연
  • 인스턴스 수 변화(스케일아웃이 늦는지)
  • CPU/메모리 사용률

6-2. 로그에서 “첫 요청이 느린 이유”를 분리

애플리케이션 로그에 다음을 남기면 원인 분리가 쉬워집니다.

  • 프로세스 시작 시각
  • 서버가 리슨 시작한 시각
  • 첫 요청 처리 시작 시각
  • 외부 의존성 호출 시간(예: DB connect, secrets fetch)

Node.js 예시: 간단한 부팅 타임 로깅

const bootAt = Date.now();
console.log(`boot_start_ms=${bootAt}`);

app.listen(port, '0.0.0.0', () => {
  console.log(`boot_listen_ms=${Date.now() - bootAt}`);
});

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`path=${req.path} status=${res.statusCode} dur_ms=${Date.now() - start}`);
  });
  next();
});

이렇게만 해도 “컨테이너 시작이 느린지” vs “요청 처리 로직이 느린지”가 갈립니다.

7) 배포/트래픽 전환에서 발생하는 503 줄이기

7-1. 새 리비전이 준비되기 전에 트래픽을 너무 빨리 주지 않기

Cloud Run은 리비전 단위로 트래픽 분할이 가능합니다.

  • 100% 즉시 전환 대신 1%부터 점진 전환
  • 문제가 있으면 즉시 롤백

점진 전환은 “새 버전에서만 발생하는 콜드스타트/초기화 문제”를 빠르게 격리하는 데 효과적입니다.

7-2. 스타트업에서 마이그레이션/대규모 캐시 구축 금지

배포 직후 모든 인스턴스가 동시에 무거운 작업을 하면, 준비 지연이 겹치며 503이 튈 수 있습니다.

대안:

  • 마이그레이션은 별도 잡(Cloud Build, Cloud Run Job 등)으로 분리
  • 캐시는 백그라운드 워밍 또는 점진 로딩

8) 네트워크/VPC 커넥터를 쓰는 경우의 함정

Serverless VPC Access를 붙이면 “사설 DB 접근”은 쉬워지지만, 다음 문제가 자주 생깁니다.

  • egress가 모두 VPC로 빨려 들어가 NAT 병목
  • 외부 API 호출도 VPC를 타면서 지연 증가
  • 커넥터 스케일 한계로 순간 트래픽에서 큐잉

대응:

  • 필요한 경우에만 VPC egress를 사용(설정에서 private ranges only 고려)
  • NAT 용량/커넥터 용량 점검
  • 외부 호출이 많은 서비스는 VPC 경유가 꼭 필요한지 재검토

9) 실전 권장 조합 (상황별)

9-1. 사용자-facing API, 첫 요청 지연이 치명적

  • min-instances=1 이상
  • CPU 상향(특히 Java/Spring)
  • 동시성 낮게 시작(예: 10~20)
  • 부팅 시 외부 의존성 최소화

9-2. 내부 API, 비용 민감, 지연은 어느 정도 허용

  • min-instances=0
  • 스케줄 기반 웜업(필요 시)
  • 타임아웃/재시도 정책 엄격히

9-3. 외부 API 의존성이 많은 BFF

  • keep-alive, 커넥션 재사용
  • 타임아웃 짧게, 서킷 브레이커 도입
  • 동시성 과도하게 올리지 않기

10) 마무리: 503과 콜드스타트는 “설정+코드+관측”의 합

Cloud Run의 503과 콜드스타트는 하나의 버튼으로 해결되기보다, 다음 3가지를 동시에 맞춰야 안정화됩니다.

  1. 설정: min-instances, CPU 정책, concurrency, timeout
  2. 코드: 부팅 경량화, 블로킹 제거, 커넥션 재사용, 외부 의존성 타임아웃
  3. 관측: 부팅/첫 요청/외부 호출 시간을 분리해 로그와 지표로 확정

문제가 “준비 상태는 정상인데 사용자는 실패”처럼 보일 때는, 준비/전환/타임아웃의 경계에서 발생하는 경우가 많습니다. 유사한 진단 관점은 EKS에서 Readiness 실패인데 로그는 정상일 때도 함께 참고하면 원인 분해에 도움이 됩니다.