- Published on
Cloud Run 503·Cold Start 지연 줄이는 8가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Cloud Run을 운영하다 보면 트래픽이 몰리는 순간이나 오랜 무트래픽 이후 첫 요청에서 503이 튀거나, 응답이 눈에 띄게 느려지는 구간을 만나게 됩니다. 많은 경우 이는 애플리케이션 오류라기보다 인스턴스 준비 지연(콜드 스타트), 동시성 설정 부조화, 헬스 체크 및 타임아웃 설계 미스, 외부 의존성 연결 지연 같은 운영 설계 문제에서 시작합니다.
이 글에서는 Cloud Run에서 503 및 콜드 스타트 지연을 줄이는 8가지 방법을, 실제로 효과가 큰 순서로 정리합니다. 특히 “왜 이게 503을 줄이는지”, “어떻게 검증하는지”까지 포함합니다.
관련해서 504나 극단적 지연(수 분 이상) 케이스는 아래 글도 함께 참고하면 좋습니다.
1) 최소 인스턴스(min-instances)로 콜드 스타트 자체를 없애기
콜드 스타트는 “없애는 것”이 가장 확실합니다. Cloud Run은 트래픽이 없으면 인스턴스를 0으로 내릴 수 있는데, 이때 첫 요청이 들어오면 컨테이너 생성, 이미지 풀, 런타임 부팅, 앱 초기화까지 모두 한 번에 일어나 지연이 커집니다.
min-instances를 1 이상으로 두면 항상 준비된 인스턴스가 유지되어 첫 요청 지연이 크게 줄고, 그 과정에서 발생하던 준비 실패성 503도 감소합니다.
설정 예시
gcloud run services update my-svc \
--region=asia-northeast3 \
--min-instances=1
검증 포인트
- 무트래픽 10분 이후 첫 요청의
latency가 급감하는지 확인 - Cloud Monitoring에서
container/startup latency혹은 요청 지연 분포가 평탄해지는지 확인
주의
- 비용이 증가합니다. 하지만
min-instances=1은 많은 서비스에서 “운영 안정성 보험”에 가깝습니다.
2) 동시성(concurrency)을 낮춰 준비 중 과부하와 큐잉을 줄이기
Cloud Run의 동시성은 인스턴스 1개가 동시에 처리할 수 있는 요청 수입니다. 값을 크게 잡으면 인스턴스 수를 덜 늘려 비용은 줄 수 있지만, 다음 문제가 생깁니다.
- 초기화 직후(혹은 캐시 워밍업 전) 요청이 몰리면 CPU, 메모리, DB 커넥션이 동시에 압박
- 내부 큐잉이 늘어 p95, p99 지연이 급증
- 준비가 덜 된 상태에서 에러가 발생하면
503로 관측될 수 있음
설정 예시
gcloud run services update my-svc \
--region=asia-northeast3 \
--concurrency=20
실전 가이드
- CPU 바운드 작업이 많으면
concurrency를 더 낮게 - DB 커넥션이 병목이면
concurrency를 낮추거나 커넥션 풀을 강제 제한
3) 시작 구간 최적화: 앱 부팅 시 “무거운 일”을 지연 로딩으로 바꾸기
콜드 스타트의 핵심은 컨테이너 부팅 자체보다, 애플리케이션 초기화 코드가 오래 걸리는 것인 경우가 많습니다.
대표적인 안티패턴:
- 부팅 시점에 전체 설정을 원격에서 가져오며 재시도 루프를 돈다
- DB 마이그레이션, 스키마 점검을 매번 수행한다
- 외부 API 연결 테스트를 동기적으로 수행한다
- 모델 로딩, 대형 파일 다운로드를 부팅에 포함한다
개선 전략
- “첫 요청 전에 꼭 필요”한 것만 부팅에 남기고, 나머지는 요청 처리 중 지연 로딩
- 캐시 워밍업도 동기 대신 비동기 또는 백그라운드로
- 실패해도 서비스가 뜰 수 있게, 외부 의존성은 회로 차단기 패턴으로 격리
Node.js 예시: 부팅 시점에 외부 의존성 점검 금지
import express from 'express';
const app = express();
let dbReady = false;
async function initDbLazy() {
if (dbReady) return;
// 실제로는 여기서 커넥션 풀 생성
dbReady = true;
}
app.get('/healthz', (req, res) => {
// 헬스 체크는 최대한 가볍게
res.status(200).send('ok');
});
app.get('/api', async (req, res) => {
await initDbLazy();
res.json({ ok: true });
});
const port = process.env.PORT || 8080;
app.listen(port, () => console.log('listening', port));
4) startupProbe와 헬스 체크 설계를 “가볍고 정확하게” 만들기
Cloud Run에서 컨테이너가 “준비됨”으로 판단되는 시점은 매우 중요합니다. 준비가 덜 됐는데 트래픽이 들어가면 오류가 나고, 이것이 503로 관측될 수 있습니다.
핵심은 두 가지입니다.
- 헬스 엔드포인트는 외부 의존성에 매달리지 말 것
- 준비 완료의 정의를 정확히 잡을 것
예를 들어 /healthz가 DB까지 확인하도록 만들면, DB가 순간 느려질 때마다 인스턴스가 불안정해지고 오히려 장애를 확대할 수 있습니다.
컨테이너 헬스 체크 예시(startupProbe)
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 0
timeoutSeconds: 2
periodSeconds: 2
failureThreshold: 15
운영 팁
/healthz는 “프로세스가 살아있고 이벤트 루프가 돌고 있는지” 수준으로 단순화- DB, Redis 같은 의존성은 별도
/readyz로 분리해 관측용으로만 쓰는 방식도 좋습니다
5) CPU 설정: 부팅/초기화에 CPU가 필요한 서비스는 CPU always 할당 고려
Cloud Run은 기본적으로 요청이 없으면 CPU가 제한될 수 있습니다. 일부 워크로드는 이 특성 때문에 다음이 발생합니다.
- 초기화 작업이 요청과 강하게 엮여 첫 요청이 느려짐
- 백그라운드 워밍업이 사실상 진행되지 않음
부팅 직후 캐시 구성, JIT 워밍업, 런타임 준비를 안정적으로 하려면 CPU가 항상 할당되는 설정이 도움이 됩니다.
설정 예시
gcloud run services update my-svc \
--region=asia-northeast3 \
--cpu=1 \
--no-cpu-throttling
주의
- 비용에 영향이 있습니다.
min-instances와 함께 쓰면 효과는 크지만 비용도 커질 수 있으니, p95 지연과 비용을 함께 보며 조정하세요.
6) 메모리와 인스턴스 크기: OOM 직전 스로틀이 503을 부른다
503이 꼭 콜드 스타트만 의미하진 않습니다. 메모리가 부족하면 GC 압박, 스왑 유사 현상, 라이브러리 내부 재시도가 늘면서 지연이 커지고, 결국 타임아웃이나 실패로 이어질 수 있습니다.
특히 다음은 위험 신호입니다.
- 부팅 시 메모리 사용량이 급격히 치솟는다
- 요청이 몰릴 때만
503이 증가한다 - 로그에 OOM Kill 또는 메모리 관련 에러가 보인다
조정 예시
gcloud run services update my-svc \
--region=asia-northeast3 \
--memory=1Gi
검증 포인트
- Cloud Monitoring에서 메모리 사용률이 80퍼 이상 장시간 유지되는지
- GC 시간, 응답 지연이 특정 구간에서 튀는지
7) 외부 의존성 연결 최적화: 커넥션 풀, DNS, TLS 핸드셰이크 줄이기
콜드 스타트가 길어지는 큰 이유 중 하나는 “컨테이너가 뜨자마자 외부로 나가는 연결”입니다.
- DB 연결 생성
- Redis 연결
- 외부 API 호출
- Secret Manager 호출
여기서 DNS, TLS 핸드셰이크, 커넥션 생성이 한꺼번에 발생하면 첫 요청이 무거워집니다.
실전 체크리스트
- 커넥션 풀의 최대 크기를
concurrency에 맞춰 제한 - 재시도는 짧고 빠르게 실패하도록(무한 재시도 금지)
- Secret은 가능하면 시작 시 1회 로드 후 캐시(단, 회전 정책 고려)
Python 예시: 커넥션 풀 크기 제한(개념 코드)
import os
from sqlalchemy import create_engine
DB_URL = os.environ["DB_URL"]
engine = create_engine(
DB_URL,
pool_size=5,
max_overflow=0,
pool_pre_ping=True,
)
pool_size를 무작정 키우면 인스턴스가 늘어날 때 DB가 먼저 터지고, 그 결과가 애플리케이션 503로 보일 수 있습니다.
8) 503을 줄이는 운영 관측: 로그 기반 지표와 알람을 “원인별로” 쪼개기
503이 보이면 많은 팀이 앱 코드부터 의심하지만, Cloud Run에서는 플랫폼 레벨 이벤트(스케일링, 준비 실패, 타임아웃)가 훨씬 흔합니다. 따라서 503을 하나로 뭉뚱그려 알람을 걸면, 원인 파악이 늦어집니다.
권장 관측 분해
503중에서도 특정 라우트에서만 발생하는지- 첫 요청(인스턴스 생성 직후)에서만 발생하는지
- 동시성 급증 구간과 상관관계가 있는지
- 외부 의존성 에러(예: DB timeout)와 함께 증가하는지
Cloud Logging 필터 예시
resource.type="cloud_run_revision"
severity>=ERROR
(httpRequest.status=503 OR httpRequest.status=504)
팁
- 로그에
trace를 남겨 첫 요청인지 여부, 초기화 단계, 외부 의존성 응답 시간을 남기면 503의 원인 분리가 빨라집니다.
적용 우선순위 요약
운영에서 가장 빠르게 효과를 보는 순서는 보통 아래입니다.
min-instances로 콜드 스타트 제거concurrency조정으로 과부하와 큐잉 감소- 부팅 초기화 코드 다이어트
startupProbe및 헬스 체크 정교화- CPU always 할당 검토
- 메모리 증설 및 OOM 방지
- 외부 의존성 연결 최적화
- 503을 원인별로 쪼개는 관측 체계
마무리
Cloud Run의 503과 콜드 스타트 지연은 “한 가지 설정”으로 끝나는 문제가 아니라, 스케일링 모델과 애플리케이션 초기화/의존성 설계가 맞물린 결과인 경우가 대부분입니다. 위 8가지를 순서대로 적용하면서, 변경 전후의 p95, p99 지연과 503 발생률을 함께 비교하면 원인과 효과가 선명해집니다.
만약 지연이 수 분 단위로 커지거나 504가 동반된다면, 네트워크 경로, VPC 커넥터, 이미지 풀, 리비전 설정 등 다른 축의 점검이 필요할 수 있습니다. 그 경우는 아래 글의 체크리스트가 더 직접적으로 도움이 됩니다.