Published on

GCP Cloud Run 503·콜드스타트 5분 튜닝 가이드

Authors

Cloud Run을 운영하다 보면 두 가지가 가장 사람을 괴롭힙니다. 첫째는 "잘 되다가 가끔" 터지는 503이고, 둘째는 트래픽이 뜸한 서비스에서 첫 요청이 유독 느린 콜드스타트입니다.

이 글은 원인 분석을 길게 하기보다, 5분 안에 적용 가능한 튜닝 순서로 정리합니다. 핵심은 503을 하나의 에러로 보지 말고 플랫폼/컨테이너/애플리케이션/의존성 계층으로 쪼개서 관측하고, Cloud Run의 동시성·타임아웃·최소 인스턴스·시작 CPU 부스트를 상황에 맞게 조합하는 것입니다.

1) Cloud Run의 503은 대부분 "용량/준비/시간" 문제다

Cloud Run에서 503은 대개 아래 범주 중 하나로 귀결됩니다.

  • 인스턴스가 아직 준비되지 않음: 새 인스턴스가 뜨는 중(콜드스타트)인데 요청이 먼저 도착
  • 요청 처리 시간이 제한을 초과: Cloud Run 요청 타임아웃 또는 프록시/클라이언트 타임아웃
  • 동시성 과부하: 한 인스턴스가 너무 많은 요청을 동시에 받아 큐잉이 길어짐
  • 컨테이너가 비정상 상태: 앱 크래시, OOM, 스레드 고갈, 이벤트 루프 블로킹
  • 의존성 병목: DB 커넥션 풀 고갈, 외부 API 지연, DNS/TLS 핸드셰이크 지연

중요한 포인트는 503이 "서버가 죽었다"가 아니라, 요청을 정상 처리할 준비가 안 됐거나 처리할 수 없었다는 신호라는 점입니다.

2) 5분 튜닝 체크리스트 (우선순위대로)

아래 6가지는 실제로 체감 효과가 크고, 설정 변경만으로도 즉시 개선되는 경우가 많습니다.

(1) 최소 인스턴스 min-instances로 콜드스타트 제거

트래픽이 뜸한 서비스라면 콜드스타트는 구조적으로 발생합니다. 가장 확실한 해결은 최소 인스턴스1 이상으로 두는 것입니다.

  • 장점: 첫 요청 지연이 사실상 사라짐
  • 단점: 유휴 비용 발생

gcloud 예시:

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --min-instances=1

운영 팁:

  • 내부 관리자 API, 결제/로그인 같은 "첫 요청이 중요한" 엔드포인트는 min-instances=1이 비용 대비 효과가 큽니다.
  • 배치성/비정기 서비스는 0 유지 후 아래 튜닝으로 완화하는 편이 낫습니다.

(2) 시작 CPU 부스트로 "초기화" 시간을 줄이기

Cloud Run은 콜드스타트 시 컨테이너 초기화(의존성 로딩, JIT, 마이그레이션 체크 등)가 느리면 첫 요청이 길어집니다. 시작 CPU 부스트는 이 구간을 단축하는 데 도움이 됩니다.

gcloud 예시(환경/세대에 따라 플래그가 다를 수 있어 콘솔 설정도 병행 권장):

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --cpu-boost

적용 팁:

  • Node.js, Python처럼 런타임 초기화가 짧은 편인 서비스는 효과가 제한적일 수 있습니다.
  • Java/Spring, 대형 번들(SSR), 무거운 ML 라이브러리 로딩은 효과가 체감되는 경우가 많습니다.

(3) 인스턴스 동시성 concurrency를 현실적으로 낮추기

Cloud Run의 동시성은 "한 인스턴스가 동시에 처리할 요청 수"입니다. 값이 너무 높으면 한 인스턴스에 요청이 몰려 큐잉 지연이 커지고, 결과적으로 타임아웃과 503이 발생할 수 있습니다.

  • CPU 바운드(이미지 처리, 암호화, PDF 생성): concurrency를 낮게
  • I/O 바운드(외부 API 호출 중심): 다소 높게도 가능하지만, 커넥션 풀/레이트리밋을 고려

gcloud 예시:

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --concurrency=10

운영 팁:

  • "갑자기 느려졌다"는 경우, concurrency를 절반으로 줄여보면 원인이 CPU/스레드/이벤트루프 포화인지 빠르게 판별됩니다.
  • 동시성을 낮추면 인스턴스가 더 많이 생길 수 있으니 max-instances와 함께 조절하세요.

(4) 요청 타임아웃 timeout을 서비스 성격에 맞게

Cloud Run 요청 타임아웃이 너무 짧으면 정상적인 장기 요청이 503으로 보일 수 있습니다. 반대로 너무 길면 장애 시 "오래 매달리다가" 폭발합니다.

gcloud 예시:

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --timeout=60s

권장 접근:

  • HTTP 요청은 가능한 10s에서 60s 사이로 관리
  • 오래 걸리는 작업은 요청-응답 모델이 아니라 큐/비동기 작업으로 분리

외부 API 호출이 원인이라면 재시도/백오프 설계도 같이 보세요: OpenAI API 429 RateLimit 재시도와 큐 설계

(5) max-instances로 폭주 시나리오를 통제

트래픽 급증 시 Cloud Run은 인스턴스를 늘려서 대응합니다. 하지만 의존성(DB, 외부 API)이 그 확장을 못 버티면 오히려 전체가 무너집니다.

  • DB가 약하면 max-instances로 상한을 걸고
  • 애플리케이션 레벨에서 큐잉/거절(429 등)로 품질을 통제하는 편이 낫습니다.

gcloud 예시:

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --max-instances=50

(6) 메모리/CPU를 "한 단계" 올려서 콜드스타트를 줄이기

콜드스타트는 단순히 "인스턴스가 없어서"만이 아니라, 인스턴스가 뜨는 속도도 포함합니다. 컨테이너 초기화가 빡빡하면 CPU/메모리를 올리는 것이 즉효인 경우가 많습니다.

gcloud run services update SERVICE_NAME \
  --region=REGION \
  --cpu=2 \
  --memory=1Gi

3) 로그로 503을 "어느 계층" 문제인지 1분 안에 분류하기

Cloud Logging에서 최소한 이것만 본다

  • HTTP 요청 로그에서 상태코드 503 필터
  • 같은 시각의 애플리케이션 로그(예외, 타임아웃, DB 에러)
  • 인스턴스 시작/종료 이벤트(콜드스타트 타이밍)

로그에서 자주 보이는 패턴:

  • context deadline exceeded 류: 타임아웃/의존성 지연
  • OOMKilled 또는 메모리 관련 크래시: 메모리 상향 또는 누수 추적
  • 특정 엔드포인트에서만 발생: 그 엔드포인트의 초기화/외부 호출/쿼리 병목

Kubernetes에서의 장애 패턴과 유사한 부분도 많습니다. 원인별로 로그와 리소스를 찢어보는 방식은 이 글이 참고됩니다: Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅

4) 콜드스타트 체감의 절반은 "앱 초기화"가 만든다

Cloud Run 설정을 건드려도 첫 요청이 여전히 느리다면, 앱이 시작 시점에 너무 많은 일을 하고 있을 확률이 큽니다.

(1) 서버 시작 시 무거운 초기화를 요청 경로에서 분리

나쁜 예(첫 요청 때 마이그레이션 체크, 원격 설정 다운로드, 대형 모델 로딩 등):

// Node.js 예시 (나쁜 패턴)
app.get('/api', async (req, res) => {
  await loadBigConfigFromRemote();
  await warmupDb();
  res.json({ ok: true });
});

개선 예(프로세스 시작 시 1회 수행, 실패 시 빠르게 감지):

let ready = false;

async function bootstrap() {
  await loadBigConfigFromRemote();
  await warmupDb();
  ready = true;
}

bootstrap().catch((e) => {
  console.error('bootstrap failed', e);
  process.exit(1);
});

app.get('/healthz', (req, res) => {
  if (!ready) return res.status(503).send('not ready');
  res.status(200).send('ok');
});

핵심은 "첫 비즈니스 요청"이 부트스트랩을 떠안지 않게 하는 것입니다.

(2) DB 커넥션 풀을 Cloud Run 스케일에 맞게 다시 잡기

Cloud Run은 인스턴스가 수평 확장됩니다. 인스턴스당 커넥션 풀이 너무 크면 DB가 먼저 죽습니다.

권장 접근:

  • 인스턴스당 풀을 작게(예: 5에서 10) 시작
  • max-instances와 곱해서 DB 최대 커넥션을 넘지 않게 설계
  • 가능하면 Cloud SQL 사용 시 커넥션 관리 계층(프록시/풀링)을 적극 활용

PostgreSQL 쪽 병목이 의심되면 VACUUM/인덱스도 함께 점검하세요: PostgreSQL VACUUM 안 도는 이유 7가지와 해법

5) 503이 "콜드스타트"가 아니라 "의존성 지연"인 경우

운영에서 더 흔한 케이스는 사실 이쪽입니다. 콜드스타트처럼 보이지만, 실제로는 특정 의존성이 느려서 첫 요청이 길어지고 타임아웃이 나면서 503이 됩니다.

체크 포인트:

  • 외부 API 호출에 타임아웃이 설정되어 있는가
  • DNS resolve, TLS handshake가 매 요청마다 반복되는가
  • HTTP keep-alive, 커넥션 재사용이 되는가

Node.js에서 keep-alive를 명시하는 예:

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

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

async function callUpstream() {
  const res = await fetch('https://api.example.com/data', {
    agent,
    // 라이브러리에 따라 timeout 옵션 형태가 다릅니다.
  });
  if (!res.ok) throw new Error(`upstream error: ${res.status}`);
  return res.json();
}

Python httpx 예:

import httpx

client = httpx.Client(
    timeout=httpx.Timeout(5.0, connect=2.0),
    limits=httpx.Limits(max_connections=50, max_keepalive_connections=20),
)

def call_upstream():
    r = client.get("https://api.example.com/data")
    r.raise_for_status()
    return r.json()

이런 기본기만 잡아도 "가끔" 나는 503이 크게 줄어듭니다.

6) 실전: 5분 안에 적용하는 추천 조합 3가지

A. 트래픽이 뜸하고 첫 요청이 중요한 서비스

  • min-instances=1
  • 시작 CPU 부스트 켜기
  • concurrency는 보수적으로(예: 10)
  • 타임아웃 30s에서 시작

B. 트래픽은 꾸준한데 피크 때 503이 나는 서비스

  • concurrency 낮추기(예: 80에서 10 또는 20)
  • max-instances로 DB/외부 API 보호
  • 외부 호출 타임아웃/재시도 정책 정리

C. CPU 바운드 작업이 섞인 API

  • concurrency=1 또는 2로 시작
  • CPU 2 이상
  • 오래 걸리는 작업은 비동기화(큐)

7) 마무리: 503을 "없애는" 게 아니라 "통제"하는 것

Cloud Run에서 503과 콜드스타트는 완전히 0으로 만들기 어렵습니다. 대신 아래를 목표로 하면 운영 난이도가 급격히 내려갑니다.

  • 콜드스타트는 min-instances 또는 시작 CPU 부스트로 체감 제거
  • 503은 동시성/타임아웃/의존성 타임아웃으로 발생 조건을 좁히기
  • max-instances로 의존성 보호하고, 과부하 시엔 빠르게 실패(또는 큐잉)하도록 품질을 통제

위 체크리스트를 순서대로 적용하면, 대부분의 Cloud Run 503은 "원인 모를 간헐 장애"에서 "재현 가능한 설정/병목"으로 바뀝니다. 그 다음부터는 관측 지표(지연, 에러율, 인스턴스 수, DB 커넥션)를 기준으로 숫자로 튜닝하면 됩니다.