- Published on
GCP Cloud Run 504와 콜드스타트 지연 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스로 Cloud Run을 쓰다 보면 “가끔 504가 난다”, “첫 요청이 유독 느리다”가 가장 흔한 불만입니다. 특히 트래픽이 뜸한 서비스(관리자 페이지, 배치 트리거 API, B2B 백오피스)일수록 콜드스타트가 자주 발생하고, 그 지연이 곧바로 프론트/게이트웨이의 타임아웃과 결합해 504로 보이기도 합니다.
이 글은 (1) 504가 어디서 발생하는지 경로를 분리하고, (2) 콜드스타트를 구성요소별(컨테이너 시작/앱 부팅/외부 의존성)로 쪼개 관측한 뒤, (3) Cloud Run 설정 + 애플리케이션 코드로 지연을 줄이는 방법을 정리합니다.
> 참고: “간헐적 504”의 사고방식은 쿠버네티스 ALB에서도 유사합니다. 원인 분해 관점은 EKS ALB Ingress 504(5xx) 간헐 발생 원인·해결도 같이 보면 도움이 됩니다.
1) Cloud Run의 504: 진짜 504의 출처부터 분리하기
Cloud Run에서 보이는 504는 대개 아래 중 하나입니다.
- 클라이언트/프록시(브라우저, CDN, LB) 타임아웃
- Cloud Load Balancing(HTTPS LB) 타임아웃
- Cloud Run 요청 타임아웃(서비스 설정의 request timeout)
- 애플리케이션 내부에서 upstream 호출이 타임아웃(DB, 외부 API)
겉으로는 모두 504로 보여도, 해결책은 완전히 다릅니다. 먼저 “어디서 끊겼는지”를 로그로 확정해야 합니다.
1-1) Cloud Run 요청 로그에서 확인할 핵심 필드
Cloud Run(2nd gen) 요청 로그는 Cloud Logging에서 run.googleapis.com/requests 유형으로 확인합니다. 아래를 체크합니다.
httpRequest.status: 504/499/502 등latency또는httpRequest.latencytrace/spanId(Cloud Trace 연동 시)resource.labels.service_name,revision_name
패턴
- latency가 딱 timeout 값 근처에서 끊기면(예: 60s, 300s) 상위 타임아웃일 가능성이 큼
- status가 499면(클라이언트가 먼저 끊음) 브라우저/프록시 타임아웃 의심
1-2) Cloud Run의 request timeout과 상위 프록시 타임아웃 정렬
Cloud Run 서비스에는 요청 타임아웃(최대 60분)을 설정할 수 있지만, 앞단에 HTTPS Load Balancer, CDN, API Gateway/ESPv2, Cloudflare 등을 붙이면 그들의 타임아웃이 더 짧을 수 있습니다.
- Cloud Run timeout을 300초로 늘렸는데도 60초에 끊기면 → LB/게이트웨이 타임아웃이 60초일 수 있음
- 반대로 LB는 600초인데 Cloud Run이 60초면 → Cloud Run이 먼저 끊음
결론: 504 대응은 “가장 짧은 타임아웃”을 찾아 정렬하는 작업부터 시작해야 합니다.
2) 콜드스타트 지연을 3단계로 쪼개서 측정하기
콜드스타트는 하나의 현상이 아니라, 보통 다음 3단계 합입니다.
- 인스턴스 프로비저닝 + 컨테이너 시작(이미지 pull/샌드박스/네트워킹)
- 애플리케이션 부팅(프레임워크 초기화, DI 컨테이너, 라우팅)
- 외부 의존성 준비(DB 커넥션, 시크릿 로드, 외부 API 핸드셰이크)
이 중 어떤 비중이 큰지 모르고 “min instances 올리면 되겠지”로 접근하면 비용만 늘고 효과가 적을 수 있습니다.
2-1) 애플리케이션 레벨에서 부팅 시간 로그 남기기
가장 먼저 할 일은 부팅 완료 시점을 명확히 로그로 남기는 것입니다.
예시: Node.js(Express) 부팅 시간 측정
// index.js
import express from "express";
const t0 = Date.now();
const app = express();
app.get("/healthz", (req, res) => res.status(200).send("ok"));
app.get("/", (req, res) => res.send("hello"));
const port = process.env.PORT || 8080;
app.listen(port, () => {
const bootMs = Date.now() - t0;
console.log(JSON.stringify({ msg: "app_listening", port, bootMs }));
});
예시: Spring Boot 부팅 완료 이벤트
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class BootLogger {
private final long t0 = System.currentTimeMillis();
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
long bootMs = System.currentTimeMillis() - t0;
System.out.println("{\"msg\":\"app_ready\",\"bootMs\":" + bootMs + "}");
}
}
이 로그와 첫 요청의 latency를 비교하면 “컨테이너 시작 vs 앱 부팅 vs 외부 의존성” 중 어디가 병목인지 감이 잡힙니다.
2-2) 첫 요청에서만 느리면: lazy init/커넥션 초기화 의심
부팅은 빨리 끝났는데 첫 요청만 2~10초 이상 느리면 보통:
- 첫 DB 커넥션 생성/풀 워밍업
- 첫 DNS resolve, TLS handshake
- 첫 템플릿/ORM 메타데이터 로드
- JIT warm-up(특히 JVM)
이 경우는 부팅 단계에서 미리 워밍업하거나, 커넥션 풀/클라이언트 재사용으로 해결합니다.
3) Cloud Run 설정으로 콜드스타트/504를 줄이는 핵심 옵션
3-1) min instances: 가장 확실하지만 비용이 드는 해결책
콜드스타트를 “없애는” 유일한 방법은 인스턴스를 미리 켜두는 것입니다.
- 프로덕션 API:
min-instances=1~N권장 - 트래픽이 낮지만 SLA가 있으면: 최소 1은 강력 추천
다만 인스턴스가 놀아도 과금이 발생하므로, 아래 최적화(부팅/의존성)와 같이 적용하는 게 좋습니다.
3-2) concurrency와 CPU: “첫 응답”을 빠르게 하려면 CPU 정책을 보라
Cloud Run은 요청이 없을 때 CPU를 제한하는 정책이 있을 수 있습니다(설정/세대에 따라 표현이 다를 수 있음). 콜드스타트 직후나 유휴 후 첫 요청에서 CPU가 충분히 배정되지 않으면 부팅과 초기화가 늘어집니다.
- CPU를 1 이상으로 올리면 부팅/초기화가 빨라지는 경우가 많음
- 동시성(concurrency)을 너무 크게 두면, 한 인스턴스에 첫 요청이 몰릴 때 tail latency가 커질 수 있음
권장 접근:
- latency 민감: concurrency를 낮추고(예: 10~40) 인스턴스 수로 처리
- 비용 민감: concurrency를 올리되, 첫 요청 지연은 min instances로 상쇄
3-3) timeout: “늘리기”는 임시방편, “짧게 만들기”가 목표
Cloud Run timeout을 늘리면 504는 줄어들 수 있지만, 근본 원인(느린 부팅/느린 DB/느린 외부 API)이 남습니다.
- API 요청은 가능한 짧게(수 초~수십 초)
- 오래 걸리는 작업은 Pub/Sub, Cloud Tasks, Workflows로 비동기화
3-4) startup probe/health check는 ‘있는 그대로’ 드러내게
Cloud Run은 GKE처럼 livenessProbe를 직접 구성하는 모델은 아니지만(서비스/컨테이너 헬스체크 방식은 제품/세대에 따라 다름), 핵심은 같습니다.
/healthz는 DB 연결 같은 무거운 체크를 넣지 말고 프로세스 생존/기본 라우팅만 확인- readiness 성격의 검증(예: DB 필수)은 별도 엔드포인트로 분리
잘못된 헬스체크는 “살아있는데 죽었다고 판단”하거나, “죽었는데 살아있다고 판단”해서 5xx/지연을 키웁니다. (쿠버네티스에서 livenessProbe로 재시작 루프가 나는 패턴은 EKS Pod 1분마다 재시작? livenessProbe 실패 해결와 사고방식이 유사합니다.)
4) 코드/아키텍처 레벨 최적화: 콜드스타트의 대부분은 여기서 줄어든다
4-1) 이미지 크기 줄이기: pull + start 시간을 줄이는 가장 저렴한 방법
콜드스타트에서 “컨테이너 시작” 비중이 큰 경우 이미지 최적화가 효율이 좋습니다.
- 멀티스테이지 빌드 사용
- 불필요한 빌드 도구/캐시 제거
- distroless 또는 slim 베이스 고려
예시: Node.js 멀티스테이지 Dockerfile
# build
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# runtime
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
ENV NODE_ENV=production
CMD ["dist/index.js"]
4-2) 부팅 시 무거운 초기화 제거: “요청 받기 전”에 다 하지 마라
대표적인 안티패턴:
- 앱 시작 시 모든 외부 API에 ping
- 마이그레이션/스키마 체크를 요청 서버 부팅에 포함
- 큰 설정 파일/모델을 매번 로드
대신:
- 꼭 필요한 최소 초기화만 하고, 나머지는 lazy-load
- 단, 첫 요청이 느려지는 게 싫다면 백그라운드 워밍업을 둔다
4-3) DB 커넥션 풀: 서버리스에 맞게 “작게, 재사용 가능하게”
Cloud Run은 인스턴스가 스케일아웃되므로, 풀을 크게 잡으면 DB가 먼저 터집니다.
- 풀 크기: 인스턴스당 작게(예: 2~10) 시작
- 커넥션은 프로세스 전역 싱글턴으로 재사용
- Cloud SQL이면 Cloud SQL Connector/프록시 구성 확인
DB가 병목이면 “콜드스타트”가 아니라 “첫 쿼리/락/인덱스”가 원인일 수 있습니다. 인덱스/테이블 팽창으로 응답이 간헐적으로 늘어나는 케이스는 PostgreSQL 인덱스가 느릴 때 - Bloat·VACUUM·REINDEX 같은 진단이 필요합니다.
4-4) 외부 HTTP 호출: 타임아웃/재시도 정책을 명시하라
콜드스타트 상황에서는 DNS/TLS가 첫 호출에서 느려질 수 있고, 외부 장애가 나면 요청이 길게 늘어져 504로 보입니다.
- HTTP 클라이언트 타임아웃을 반드시 설정(예: 2~5초)
- 재시도는 idempotent 요청에만 제한적으로
- 커넥션 재사용(keep-alive)
예시: Node.js fetch에 AbortController로 타임아웃
import fetch from "node-fetch";
export async function fetchWithTimeout(url, ms = 3000) {
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), ms);
try {
const res = await fetch(url, { signal: ac.signal });
return res;
} finally {
clearTimeout(t);
}
}
5) 운영에서 바로 쓰는 체크리스트(504/콜드스타트)
5-1) 10분 안에 확인할 것
- Cloud Run 요청 로그에서 latency가 특정 초에 딱 맞게 끊기는지
- status 499 여부(클라이언트가 먼저 끊음)
- 같은 시각에 revision 배포/스케일 이벤트가 있었는지
- 첫 요청만 느린지, 모든 요청이 느린지
5-2) 우선순위 높은 해결 순서
- 이미지/부팅 최적화(비용 증가 없이 효과)
- 외부 의존성 타임아웃/재시도/풀 크기 정리(504의 상당수가 여기)
- min instances 1 적용(SLA 필요하면 사실상 정답)
- concurrency/CPU 튜닝
- 장기 작업 비동기화(Cloud Tasks/PubSub)
6) 권장 “기준 구성” 예시
- 서비스 성격: 사용자-facing API, P95 < 300ms 목표
- 설정 가이드(출발점)
- min instances: 1
- concurrency: 20~40
- CPU: 1 이상(부팅/GC 여유)
- timeout: 15~30s(장기 작업은 비동기)
- 코드 가이드
/healthz는 가볍게- DB 풀 작게 + 재사용
- 외부 HTTP 타임아웃 2~5초
- 부팅 완료 로그/트레이싱으로 단계별 시간 측정
마무리
Cloud Run의 504와 콜드스타트는 “서버리스니까 어쩔 수 없다”가 아니라, 타임아웃 경로를 분리하고(어디서 끊기는지), 콜드스타트를 단계별로 계측한 뒤, 설정과 코드를 맞추면 대부분 안정적으로 줄일 수 있습니다.
특히 min instances는 가장 확실한 처방이지만 비용이 따르므로, 그 전에 이미지 크기/부팅 초기화/외부 의존성 타임아웃부터 정리하면 ‘돈으로 해결’하지 않고도 체감 개선이 크게 나옵니다.
원하시면 사용 중인 스택(Node/Spring/Python), 앞단 구성(Cloudflare, HTTPS LB, API Gateway), 현재 timeout/min instances/concurrency 값을 알려주시면 “지금 설정에서 가장 가능성 높은 병목” 기준으로 더 구체적인 튜닝안을 제시하겠습니다.