Published on

GCP Cloud Run 504와 콜드스타트 지연 해결 가이드

Authors

서버리스로 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는 대개 아래 중 하나입니다.

  1. 클라이언트/프록시(브라우저, CDN, LB) 타임아웃
  2. Cloud Load Balancing(HTTPS LB) 타임아웃
  3. Cloud Run 요청 타임아웃(서비스 설정의 request timeout)
  4. 애플리케이션 내부에서 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.latency
  • trace / 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단계 합입니다.

  1. 인스턴스 프로비저닝 + 컨테이너 시작(이미지 pull/샌드박스/네트워킹)
  2. 애플리케이션 부팅(프레임워크 초기화, DI 컨테이너, 라우팅)
  3. 외부 의존성 준비(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를 직접 구성하는 모델은 아니지만(서비스/컨테이너 헬스체크 방식은 제품/세대에 따라 다름), 핵심은 같습니다.

  • /healthzDB 연결 같은 무거운 체크를 넣지 말고 프로세스 생존/기본 라우팅만 확인
  • 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 --from=build /app/dist ./dist
COPY --from=build /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) 우선순위 높은 해결 순서

  1. 이미지/부팅 최적화(비용 증가 없이 효과)
  2. 외부 의존성 타임아웃/재시도/풀 크기 정리(504의 상당수가 여기)
  3. min instances 1 적용(SLA 필요하면 사실상 정답)
  4. concurrency/CPU 튜닝
  5. 장기 작업 비동기화(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 값을 알려주시면 “지금 설정에서 가장 가능성 높은 병목” 기준으로 더 구체적인 튜닝안을 제시하겠습니다.