- Published on
GCP Cloud Run 504·콜드 스타트 10분 지연 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스라서 운영이 ‘없다’고 믿고 시작했다가, Cloud Run에서 504 Gateway Timeout이 터지고 첫 요청이 10분 가까이 대기하는 상황을 만나면 체감 난이도는 급상승합니다. 특히 증상이 “가끔” 발생하면 더 답답합니다. 이 글은 Cloud Run의 요청 경로(프록시/로드밸런서/컨테이너)와 제약(요청 시간 제한, 인스턴스 프로비저닝, VPC egress)을 기준으로 504와 10분 콜드 스타트를 분리해서 진단하고, 재발을 막는 설정/코드/빌드 최적화까지 정리합니다.
아래 내용은 “Cloud Run 서비스 자체가 느린 것인지”, “외부 의존성이 느린 것인지”, “네트워크 경로가 막힌 것인지”, “인스턴스가 안 떠서 기다리는 것인지”를 관측 가능한 신호로 나눠 접근합니다.
1) 먼저 결론: 504와 10분 지연은 보통 원인이 다릅니다
504 Gateway Timeout의 전형적인 원인
- 요청 처리 시간이 Cloud Run/프록시 제한을 초과(동기 HTTP로 오래 잡고 있음)
- 앱이 응답을 늦게/못함(스레드 고갈, 데드락, GC 스톨)
- 업스트림 호출이 타임아웃 없이 대기(DB/외부 API)
- VPC 커넥터/NAT/DNS 문제로 egress가 지연되어 결국 타임아웃
“콜드 스타트가 10분”의 전형적인 원인
- 실제로는 콜드 스타트가 아니라 인스턴스 생성/스케일아웃이 막혀 대기
- 동시성/최대 인스턴스 제한으로 큐잉
- 리전 자원 부족, 할당 쿼터 제한
- 컨테이너가 뜨지만 ready가 늦음(초기화 작업 과다)
- 이미지 pull이 느림(큰 이미지, 레이어 비효율, Artifact Registry 네트워크)
- VPC 커넥터 경유 시 초기 네트워크 설정/라우팅 이슈
핵심은 **HTTP 504는 “요청 단위 타임아웃”**이고, “10분 지연”은 스케일/프로비저닝/ready 지연일 가능성이 큽니다. 따라서 로그와 메트릭에서 둘을 분리해서 봐야 합니다.
2) 증상 분리 체크리스트(관측부터)
A. Cloud Run 요청 로그에서 latency와 상태코드 확인
Cloud Run의 Request log(Cloud Logging)에서 다음을 봅니다.
httpRequest.status가 504인지httpRequest.latency가 얼마나 되는지- 같은 시각의
container로그에 앱이 요청을 받았는지(access log)
포인트: 504인데 앱 access log가 없다면, 요청이 컨테이너까지 못 갔거나(프로비저닝/라우팅) 컨테이너가 준비되지 않았을 수 있습니다.
B. “인스턴스가 없어서 대기”인지 확인: Instance count / container startup
Cloud Monitoring에서 Cloud Run 메트릭을 봅니다.
- 인스턴스 수(활성/유휴)
- 요청 수, 동시 요청 수
- 컨테이너 시작 시간/재시작
C. 의존성 지연인지 확인: 업스트림 타임아웃과 DNS
10분 지연이 “첫 요청에서만”이 아니라 특정 API 호출에서 반복된다면, 아래가 흔합니다.
- DB 연결이 타임아웃 없이 무한 대기
- DNS가 느리거나, VPC 커넥터 경유로 라우팅이 꼬임
네트워크 타임아웃류는 Kubernetes에서도 504로 보이지만 원인이 다른 경우가 많습니다. 유사한 접근(요청 경로 분리, 업스트림 타임아웃 확인)은 이 글도 도움이 됩니다: EKS ALB Ingress 504인데 Pod는 정상일 때
3) Cloud Run에서 504를 만드는 대표 패턴과 해결
3.1 동기 요청으로 “긴 작업”을 처리
보고서 생성, 대용량 변환, 크롤링 등 “몇 분” 걸리는 작업을 HTTP 요청 하나로 처리하면 504가 나기 쉽습니다.
해결: 비동기 작업 큐(Cloud Tasks / Pub/Sub)로 분리
- HTTP는 작업 생성만 하고 빠르게 202 응답
- 실제 작업은 백그라운드 워커(Cloud Run job 또는 별도 서비스)가 처리
간단한 Node.js 예시(요청은 즉시 반환):
import express from "express";
import { CloudTasksClient } from "@google-cloud/tasks";
const app = express();
app.use(express.json());
const client = new CloudTasksClient();
app.post("/generate", async (req, res) => {
const payload = { userId: req.body.userId };
// TODO: queuePath, url 등 환경변수로 관리
// 실제론 Cloud Tasks에 HTTP task를 넣고 워커가 처리
res.status(202).json({ ok: true, message: "queued" });
});
app.listen(process.env.PORT || 8080);
3.2 업스트림 호출 타임아웃 미설정(무한 대기)
DB/외부 API 호출이 기본 설정으로 오래 대기하면, 결국 프록시/클라이언트 쪽에서 504로 끊깁니다.
해결: 애플리케이션 레벨 타임아웃 + 재시도 정책
Node.js(undici/fetch)에서 AbortController로 상한을 둡니다.
export async function fetchWithTimeout(url, { timeoutMs = 3000, ...opts } = {}) {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...opts, signal: controller.signal });
return res;
} finally {
clearTimeout(t);
}
}
- 타임아웃은 “전체 요청 상한”과 “연결/읽기 타임아웃”을 분리할 수 있으면 더 좋습니다.
- 재시도는 멱등성(GET/PUT 등)과 백오프를 고려합니다.
3.3 동시성 설정으로 인한 큐잉 → 결국 504
Cloud Run은 인스턴스당 동시성(concurrency)을 높이면 효율이 좋아지지만,
- CPU가 부족한 런타임(싱글 스레드, heavy CPU)에서 동시성이 과하면
- 요청이 내부 큐에 쌓여 지연되고
- 상위 레벨에서 타임아웃이 터집니다.
해결
- CPU 바운드면 concurrency를 낮추고 인스턴스를 늘리기
- 메모리/CPU를 올려 GC/스케줄링 지연을 줄이기
- 최대 인스턴스 제한(max instances)을 너무 낮게 잡지 않기
4) “콜드 스타트 10분”을 만드는 진짜 원인들
4.1 최소 인스턴스(min instances) 0 + 큰 이미지 + 무거운 초기화
콜드 스타트는 보통 수 초~수십 초인데, 10분이면 컨테이너가 정상적으로 ready 되지 않거나, 스케일아웃이 막혀 대기하는 경우가 많습니다.
해결 1: min instances로 워밍 유지
- 트래픽이 일정하거나 SLA가 필요하면
min-instances=1이상 - 비용과 지연 사이 트레이드오프
gcloud 예시:
gcloud run services update my-svc \
--region=asia-northeast3 \
--min-instances=1 \
--cpu=2 --memory=1Gi
해결 2: Startup 작업을 “요청 경로”에서 분리
문제 패턴:
- 서버 시작 시 DB 마이그레이션
- 외부 API 워밍업을 동기적으로 수행
- 대용량 모델/사전 로딩
원칙:
- 컨테이너 부팅 시점에는 필수 최소만 수행
- 나머지는 lazy load 또는 백그라운드로
Express 예시(ready를 빠르게, 비필수 워밍업은 비동기):
let warmed = false;
async function warmUp() {
// 캐시 프리로드, 외부 API 핑 등(실패해도 서비스는 뜨게)
await new Promise(r => setTimeout(r, 500));
warmed = true;
}
warmUp().catch(err => {
console.error("warmUp failed", err);
});
app.get("/healthz", (req, res) => {
// readiness를 엄격히 하고 싶으면 warmed를 반영
res.status(200).json({ ok: true, warmed });
});
4.2 이미지 pull/빌드 레이어가 비효율 → 프로비저닝이 느림
- 베이스 이미지가 무겁고 레이어가 자주 바뀜
npm install/pip install레이어가 매번 invalidate- 멀티스테이지 빌드 미사용
해결: 멀티스테이지 + 레이어 캐시 최적화
Node.js Dockerfile 예시:
# build stage
FROM node:20-bookworm 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 /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./package.json
ENV NODE_ENV=production
CMD ["dist/server.js"]
- distroless로 런타임 크기 축소
- 의존성 설치 레이어를 소스 코드 복사보다 먼저 두어 캐시 효율 상승
4.3 VPC 커넥터/NAT/DNS 이슈로 “첫 통신”이 오래 걸림
Cloud Run에서 Serverless VPC Access를 붙이고, 모든 egress를 VPC로 보내는 구성은 흔합니다. 여기서
- Cloud NAT 포트 부족/설정 문제
- 사설 DNS/Cloud DNS 정책 문제
- 방화벽/라우팅 문제 가 있으면 “외부 통신이 되는 듯 안 되는 듯” 하다가 지연이 누적될 수 있습니다.
해결 방향
- 정말 VPC egress가 필요한 트래픽만 VPC로 보낼지 재검토
- NAT/라우팅/방화벽/Cloud DNS 정책을 점검
- 앱에서 DNS/연결 타임아웃을 명시
네트워크 정책/라우팅 문제는 증상이 ‘간헐적 타임아웃’으로 나타나기 쉬운데, 원인 분리 접근은 이 글도 참고가 됩니다: EKS CoreDNS CrashLoopBackOff - upstream 타임아웃 해결
4.4 최대 인스턴스 제한으로 스케일아웃이 막힘
max-instances를 낮게 잡아두면, 트래픽 스파이크에서 새로운 인스턴스를 못 띄워 요청이 대기합니다. 대기 시간이 길어지면 결국 클라이언트/프록시가 먼저 타임아웃을 냅니다.
해결
- 피크 트래픽을 기준으로 max-instances 재설정
- 동시성(concurrency)을 현실적인 수준으로 조정
- 큐잉이 필요한 워크로드는 Cloud Tasks/PubSub로 흡수
5) 10분 문제를 재현/진단하는 실전 절차
5.1 “콜드 스타트만” 재현하기
min-instances=0으로 두고- 한동안 트래픽을 끊어 인스턴스가 0이 되게 만든 뒤
- 첫 요청을 보내고 시간 측정
curl 예시:
time curl -i https://YOUR_SERVICE_URL/healthz
5.2 로그 상관관계 만들기(요청 ID)
앱 로그에 요청 ID를 남기면 “요청이 컨테이너에 도달했는지”가 바로 보입니다.
Express 미들웨어 예시:
import crypto from "crypto";
app.use((req, res, next) => {
const rid = req.header("X-Cloud-Trace-Context") || crypto.randomUUID();
req.rid = rid;
res.setHeader("X-Request-Id", rid);
console.log(JSON.stringify({ rid, path: req.path, t: Date.now() }));
next();
});
- Cloud Run은
X-Cloud-Trace-Context가 들어오는 경우가 많아 추적에 유리합니다.
5.3 컨테이너 시작 로그 확인
컨테이너가 뜰 때 찍히는 “server listening” 로그가 늦게 찍히면
- 이미지 pull/컨테이너 부팅/초기화가 느린 것입니다. 반대로 빨리 뜨는데 첫 요청만 늦으면
- 네트워크/업스트림/동시성 큐잉을 의심합니다.
6) 운영 관점의 권장 설정 조합(현실적인 타협)
지연 민감 API(사용자-facing)
min-instances=1~N(트래픽 패턴에 따라)- concurrency 낮게(예: 10~40)에서 시작해 부하 테스트로 조정
- CPU/메모리 여유 있게(특히 Node/Python은 메모리 부족이 지연으로 직결)
- 모든 외부 호출에 타임아웃/서킷브레이커(최소 타임아웃은 필수)
배치/긴 작업
- HTTP로 오래 잡지 말고 큐 기반 비동기
- Cloud Run Jobs 또는 워커 서비스로 분리
7) 마무리: “10분 콜드 스타트”를 없애는 핵심 3가지
- 관측으로 분리: 504(요청 타임아웃) vs 프로비저닝/ready 지연(인스턴스/시작 로그)
- 초기화/이미지 최적화: 멀티스테이지 빌드, distroless, 부팅 시 필수만 수행
- 큐잉과 타임아웃 설계: 긴 작업은 비동기, 업스트림 타임아웃은 코드에서 강제
Cloud Run은 설정 몇 개로 극적으로 좋아지지만, 반대로 “조금만 복잡한 네트워크/의존성/초기화”가 섞이면 증상이 길게 늘어질 수 있습니다. 위 체크리스트대로 요청이 어디서 멈추는지만 정확히 잡으면, 10분짜리 지연도 대부분은 수십 초 이하로 줄일 수 있습니다.