Published on

GCP Cloud Run 503/504 원인별 해결 - 타임아웃·동시성

Authors

Cloud Run을 운영하다 보면 가장 사람을 불안하게 만드는 응답이 503504 입니다. 둘 다 “서버가 요청을 처리하지 못했다”는 느낌이지만, 원인은 꽤 다릅니다. 특히 Cloud Run은 요청 타임아웃, 컨테이너 동시성, 인스턴스 스케일링, 콜드스타트, 업스트림(DB/외부 API) 병목이 얽히면서 같은 증상이 다른 원인으로 나타납니다.

이 글은 503/504 를 “에러 코드”가 아니라 “리소스/시간/동시성의 경고등”으로 보고, Cloud Run에서 자주 발생하는 원인별로 빠르게 좁혀가는 방법과 설정/코드 레벨 해결책을 정리합니다.

또한 DB 커넥션이 얽혀 있는 경우가 많아, 커넥션 고갈 진단은 아래 글과 함께 보면 원인 분리가 빨라집니다.

503 vs 504: Cloud Run에서 의미가 달라지는 지점

503 Service Unavailable가 흔히 의미하는 것

Cloud Run에서 503 은 대개 아래 중 하나입니다.

  • 트래픽을 받을 준비가 된 인스턴스가 없음
  • 인스턴스가 뜨는 중이지만 컨테이너가 리스닝 포트에 바인딩하지 못함
  • 동시성/리소스 압박으로 요청 큐가 처리되지 못하고 실패
  • 업스트림(Cloud Run 앞단 프록시)에서 백엔드로 라우팅 실패

특히 “갑자기” 503 이 늘면, 스케일링이 따라가지 못했거나(콜드스타트 포함) 컨테이너가 죽거나, 과도한 동시성으로 내부 병목이 터졌을 가능성이 큽니다.

504 Gateway Timeout가 흔히 의미하는 것

504 는 “프록시가 백엔드 응답을 기다리다 타임아웃”입니다. Cloud Run에서는 보통 다음 케이스가 많습니다.

  • Cloud Run 서비스의 요청 타임아웃에 걸림
  • 요청은 살아있지만 DB/외부 API 호출이 느려서 응답이 늦음
  • 대량 동시 요청으로 인해 애플리케이션이 큐잉되면서 응답이 늦음

504 는 “처리 중이긴 했는데 늦었다” 쪽에 가깝습니다.

1) 타임아웃이 원인인 504: 설정과 설계로 나눠 해결

Cloud Run 요청 타임아웃 확인

Cloud Run 서비스에는 요청 타임아웃이 있습니다. 긴 작업이 있을 때 기본값(또는 현재 설정)이 짧으면 504 로 보일 수 있습니다.

다음 명령으로 현재 설정을 확인합니다.

gcloud run services describe SERVICE_NAME \
  --region REGION \
  --format="value(spec.template.spec.timeoutSeconds)"

타임아웃을 늘리는 것은 응급처치로 유효하지만, 장기적으로는 “긴 작업을 HTTP 요청에 붙잡아두는 구조” 자체를 바꾸는 게 더 안전합니다.

타임아웃 늘리기(응급처치)

gcloud run services update SERVICE_NAME \
  --region REGION \
  --timeout=300
  • 타임아웃을 늘리면 504 는 줄 수 있지만
  • 동시성/인스턴스 비용이 증가할 수 있고
  • 느린 의존성 호출이 방치될 수 있습니다

긴 작업은 비동기화(정공법)

HTTP 요청은 빠르게 202 등으로 응답하고, 작업은 큐로 넘기는 패턴이 Cloud Run과 잘 맞습니다.

  • Cloud Tasks
  • Pub/Sub
  • Workflows

예: 요청은 작업 생성만 하고 즉시 반환

// express 예시
app.post('/reports', async (req, res) => {
  const jobId = await enqueueReportJob(req.body); // Cloud Tasks 또는 Pub/Sub
  res.status(202).json({ jobId });
});

이렇게 바꾸면 “타임아웃을 늘려서 버티기”가 아니라 “타임아웃이 나올 구조를 제거”할 수 있습니다.

업스트림 호출에 타임아웃을 반드시 설정

Cloud Run의 타임아웃만 믿고 외부 호출을 무한 대기시키면, 요청이 누적되어 결국 동시성 병목으로 번집니다.

Node.js fetch 예시(AbortController 사용):

const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 2500);

try {
  const r = await fetch(process.env.UPSTREAM_URL, {
    signal: controller.signal,
  });
  return await r.json();
} finally {
  clearTimeout(t);
}

Java WebClient 예시:

HttpClient httpClient = HttpClient.create()
  .responseTimeout(Duration.ofSeconds(3));

WebClient client = WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

핵심은 “외부가 느리면 내 서비스도 같이 멈춘다”를 차단하는 것입니다.

2) 동시성이 원인인 503/504: Cloud Run Concurrency를 재설계

Cloud Run의 컨테이너 동시성(concurrency) 은 인스턴스 하나가 동시에 처리할 수 있는 요청 수입니다.

  • 동시성이 너무 높으면: CPU/메모리/DB 커넥션이 고갈되어 503/504 증가
  • 동시성이 너무 낮으면: 인스턴스가 과도하게 늘어 비용 증가, 콜드스타트 빈도 증가

증상으로 보는 “동시성 과다” 신호

  • 평균 지연이 아니라 p95/p99 지연이 급증
  • 에러는 없는데 응답이 늦다가 갑자기 504
  • DB 커넥션 고갈, 외부 API rate limit, 스레드풀 고갈

DB 커넥션이 의심되면 아래 글의 체크리스트가 그대로 적용됩니다.

동시성 낮추기

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

실무 팁:

  • DB를 쓰는 API 서버라면 10 이하부터 시작해 관측하며 올리는 편이 안전합니다.
  • CPU가 1 vCPU이고 동기 블로킹 작업이 많다면 동시성을 크게 주면 지연이 폭발하기 쉽습니다.

동시성과 DB 풀 사이징을 같이 맞추기

컨테이너 동시성이 C 이고, 요청 하나가 DB 커넥션을 오래 잡는다면 커넥션 풀은 최소 C 이상이 필요합니다. 하지만 풀을 무작정 키우면 DB 서버가 먼저 죽습니다.

권장 접근:

  • 동시성을 먼저 낮춰서 인스턴스당 DB 부하를 제한
  • 요청당 DB 점유 시간을 줄이기(쿼리 최적화, 트랜잭션 범위 축소)
  • 그 다음 풀을 “필요 최소”로

Spring Boot HikariCP 예시(인스턴스당 풀 크기 제한):

spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 2000

여기서 Cloud Run 동시성이 10 인데 풀도 30 이면, 트래픽 급증 시 DB가 먼저 한계에 도달할 수 있습니다.

CPU 할당 정책이 동시성에 미치는 영향

Cloud Run은 CPU를 요청 처리 중에만 주는 설정이 일반적입니다. 백그라운드 작업이 있거나, 요청 처리 중 CPU가 부족하면 지연이 늘어 504 로 이어질 수 있습니다.

  • CPU를 늘리거나
  • 동시성을 줄이거나
  • 백그라운드 작업을 분리

중 하나로 해결하는 게 일반적입니다.

3) 콜드스타트/스케일링 지연이 원인인 503: min instances와 준비성

트래픽이 갑자기 증가할 때 새 인스턴스가 뜨는 동안 요청이 밀리면 503 이 보일 수 있습니다.

min instances로 워밍 유지

gcloud run services update SERVICE_NAME \
  --region REGION \
  --min-instances=1
  • 사용자-facing API(로그인, 결제 등)는 1 이상을 권장하는 경우가 많습니다.
  • 비용과 지연 안정성을 트레이드오프로 봐야 합니다.

startup probe 관점: “리스닝 포트 바인딩”이 늦는 경우

Cloud Run은 컨테이너가 PORT 로 리스닝해야 트래픽을 붙입니다. 앱이 초기화 중에 포트를 늦게 열면 그 사이 503 이 늘 수 있습니다.

Node/Express 예시:

const express = require('express');
const app = express();

// 가능하면 서버를 먼저 열고,
// 무거운 초기화는 lazy init 또는 별도 경로로 분리
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`listening on ${port}`);
});

무거운 초기화(대형 모델 로딩, 대규모 캐시 워밍, 마이그레이션 등)는 요청 경로에서 분리하거나, 기동 단계에서 꼭 필요하지 않다면 지연 로딩으로 바꾸는 것이 좋습니다.

4) 메모리 부족/프로세스 크래시가 원인인 503: OOM과 재시작

인스턴스가 OOM으로 죽으면 순간적으로 503 이 튈 수 있습니다. 다음을 확인합니다.

  • Cloud Logging에서 컨테이너 종료 로그
  • Cloud Monitoring에서 메모리 사용량

대응:

  • 메모리 증설
  • 이미지/라이브러리 슬림화
  • 요청당 메모리 사용량 줄이기(대용량 JSON, 파일 처리 스트리밍)

예: Node에서 대용량 응답을 한 번에 만들지 않고 스트리밍.

5) 로드밸런서/클라이언트 타임아웃이 원인인 504: 경로 전체를 점검

Cloud Run 자체 타임아웃이 충분해도, 앞단에 다음이 있으면 더 짧은 타임아웃으로 504 가 날 수 있습니다.

  • HTTP(S) Load Balancer
  • API Gateway
  • 클라이언트(브라우저, 모바일 SDK, 서버 간 호출)의 타임아웃

체크 포인트:

  • “어디에서 504 를 생성했는지”를 로그로 구분
  • traceId 를 전달해 요청 경로를 end-to-end로 추적

Node에서 상관관계 ID를 로그에 포함하는 간단 예시:

import crypto from 'crypto';

app.use((req, res, next) => {
  const id = req.header('x-request-id') || crypto.randomUUID();
  res.setHeader('x-request-id', id);
  req.requestId = id;
  next();
});

app.get('/health', (req, res) => {
  console.log({ requestId: req.requestId, path: req.path });
  res.status(200).send('ok');
});

이렇게 해두면 503/504 가 “Cloud Run 문제인지, 앞단 문제인지, 업스트림 문제인지” 분리가 빨라집니다.

6) 관측으로 원인을 확정하는 체크리스트(실전)

로그에서 먼저 볼 것

  • 503 이 특정 시점에 몰리는지(배포 직후, 트래픽 스파이크 직후)
  • 504 직전에 애플리케이션 로그가 “느리게” 이어지는지(외부 호출 대기)
  • 컨테이너 재시작/OOM 흔적이 있는지

메트릭에서 먼저 볼 것

  • 요청 지연 p95/p99
  • 인스턴스 수 증감(스케일아웃이 늦는지)
  • CPU/메모리 사용량

빠른 결론을 위한 매핑

  • 504 + 지연 p99 급증 + 외부 API 호출 느림: 업스트림 타임아웃/재시도/서킷브레이커 필요
  • 504 + CPU 100% + 동시성 높음: 동시성 낮추고 CPU 증설 검토
  • 503 + 인스턴스가 0에서 급증: min instances로 완화, 콜드스타트 최적화
  • 503 + OOM/크래시: 메모리 증설 또는 메모리 누수/대용량 처리 개선

7) 추천 운영 설정 조합(출발점)

서비스 성격에 따라 다르지만, 자주 쓰는 출발점은 다음과 같습니다.

  • 사용자-facing API

    • --min-instances=1
    • --concurrency=5 또는 10
    • 타임아웃은 60~120 초 내에서 설계(긴 작업은 비동기)
  • 배치성/웹훅 처리

    • --min-instances=0
    • 동시성은 작업 특성에 맞게(외부 API rate limit 있으면 낮게)
    • 재시도는 idempotency 보장 후 적용

8) 배포 후 검증: 재현 가능한 부하 테스트 스크립트

동시성/타임아웃 튜닝은 추측이 아니라 재현으로 해야 합니다. 간단히 hey 로 확인할 수 있습니다.

hey -n 2000 -c 50 https://YOUR_RUN_URL/api
  • -c 를 올리면서 p95/p99, 에러 비율을 같이 봅니다.
  • 이때 동시성(--concurrency)과 인스턴스 리소스(CPU/메모리)를 바꿔가며 “가장 비용 대비 안정적인 지점”을 찾습니다.

마무리: 503/504는 설정 하나가 아니라 균형 문제

Cloud Run의 503/504 는 단일 원인이라기보다 시간(타임아웃)공유 자원(동시성, CPU, DB 커넥션) 의 균형이 깨졌다는 신호인 경우가 많습니다.

  • 504 는 먼저 “긴 작업을 HTTP에 묶어두지 않았는지”, “업스트림 타임아웃이 있는지”를 확인하고
  • 503 는 “준비된 인스턴스가 없었던 이유(콜드스타트, 크래시, 스케일링 지연)”를 확인한 뒤
  • 동시성, min instances, CPU/메모리, 외부 호출 타임아웃을 함께 튜닝하면 재발이 크게 줄어듭니다.

DB 커넥션 고갈이 함께 보인다면, Cloud Run 튜닝과 별개로 애플리케이션 풀/쿼리/트랜잭션 설계를 같이 손보는 것이 가장 빠른 해결책입니다.