Published on

AWS ALB 502/504 10분 타임아웃 진단 가이드

Authors

서버가 느려서 생긴 504처럼 보이지만, 정확히 10분 전후로 끊기는 502/504는 “성능 문제”라기보다 어딘가에 설정된 상한(타임아웃) 또는 중간 경로의 연결 종료일 때가 많습니다. 특히 AWS ALB 뒤에 ECS/EKS/EC2 애플리케이션을 두고 장시간 요청(리포트 생성, 대용량 업로드, 배치 트리거, 스트리밍 흉내 등)을 처리할 때 자주 겪습니다.

이 글은 “10분”이라는 힌트를 중심으로, ALB 액세스 로그와 CloudWatch 지표, 애플리케이션 로그를 한 타임라인에 올려 원인을 좁히는 실전 진단 절차를 정리합니다.

1) 먼저 확인: 502와 504의 의미(ALB 관점)

ALB에서 자주 보는 두 코드는 대략 이렇게 해석하면 진단이 빨라집니다.

  • 504 (Gateway Timeout): ALB가 타겟으로부터 제때 응답을 못 받음. 대표적으로 타겟이 처리 중이거나, 응답 바이트를 보내지 못했거나, ALB의 idle timeout 등으로 연결이 끊김.
  • 502 (Bad Gateway): ALB가 타겟으로부터 유효하지 않은 응답을 받았거나 연결이 비정상 종료됨. 예: 타겟이 중간에 커넥션을 리셋, 프록시가 잘못된 헤더를 반환, TLS/HTTP 프로토콜 불일치 등.

핵심은 ALB가 어디까지 받았는지입니다. 이걸 가장 빨리 보여주는 게 ALB 액세스 로그의 필드들입니다.

2) “정확히 10분”이면 의심할 것 5가지

10분(600초) 전후로 딱 끊기면, 아래 후보를 우선순위로 봅니다.

  1. ALB idle timeout(기본 60초, 최대 4000초)

    • 10분은 기본값은 아니지만, 누군가 600초로 바꿨을 수 있습니다.
    • idle timeout은 “요청 처리 총시간”이 아니라 연결이 유휴 상태로 유지될 수 있는 시간에 가깝습니다. 즉, 서버가 10분 동안 아무 바이트도 안 보내면 끊길 수 있습니다.
  2. 클라이언트/중간 프록시(CloudFront, Nginx, API Gateway, 사내 프록시)의 타임아웃이 600초

    • ALB 앞단에 CloudFront나 사내 프록시가 있으면 “ALB가 502/504를 냈다”가 아니라 앞단이 먼저 끊고 ALB에는 다른 증상으로 남을 수 있습니다.
  3. 타겟(앱/프레임워크/웹서버) 타임아웃이 600초

    • 예: Gunicorn timeout 600, Nginx proxy_read_timeout 600, Node.js 서버 앞 reverse proxy의 read timeout 등.
  4. NAT Gateway/SNAT/네트워크 경로에서 장시간 유휴 연결을 드롭

    • 특히 EKS에서 egress 경로나 NAT를 거치는 외부 호출이 길어질 때, 중간 장비가 유휴 세션을 정리하면서 10분 근처로 끊기는 경우가 있습니다.
    • 관련해서 네트워크 추적 관점은 EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법도 함께 보면 도움이 됩니다.
  5. 애플리케이션이 10분 동안 아무 응답도 못 보내는 구조(버퍼링) + 중간 타임아웃

    • 예: 서버는 계산 중이지만 HTTP 응답을 flush하지 못하고 마지막에 한 번에 반환.

3) 재현 조건 고정: “10분”을 측정 가능한 타임라인으로 만들기

진단은 재현이 절반입니다. 최소한 아래 3가지를 고정하세요.

  • 같은 요청(동일 경로/파라미터/바디 크기)
  • 같은 클라이언트(로컬 curl 또는 고정된 테스트 러너)
  • 같은 라우팅(가능하면 특정 타겟으로 고정하거나, 타겟 그룹의 인스턴스 수를 줄여 변수를 축소)

3-1) curl로 타임라인 측정

아래처럼 전체 시간과 HTTP 코드를 찍으면 “정확히 몇 초에 끊기는지”가 명확해집니다.

curl -v --max-time 1200 \
  -w "\nhttp_code=%{http_code} time_total=%{time_total} time_starttransfer=%{time_starttransfer}\n" \
  https://your-domain.example.com/long-task
  • --max-time은 클라이언트가 먼저 끊지 않도록 넉넉히 잡습니다.
  • time_starttransfer가 0에 가깝고 time_total이 600초 전후면, 첫 바이트를 받기 전에 끊긴 것입니다. 이 경우 idle timeout/상위 프록시/앱 read timeout 가능성이 큽니다.

4) ALB 액세스 로그로 “누가 끊었는지” 판별하기

ALB 액세스 로그를 켜고(가능하면 S3 + Athena), 아래 필드를 중심으로 봅니다.

  • elb_status_code
  • target_status_code
  • request_processing_time
  • target_processing_time
  • response_processing_time
  • received_bytes, sent_bytes

4-1) Athena 쿼리 예시

특정 경로에서 502/504만 뽑아 타임아웃 패턴을 확인합니다.

SELECT
  from_iso8601_timestamp(time) AS ts,
  elb_status_code,
  target_status_code,
  request_processing_time,
  target_processing_time,
  response_processing_time,
  received_bytes,
  sent_bytes,
  request_url
FROM alb_logs
WHERE request_url LIKE '%/long-task%'
  AND elb_status_code IN  ('502','504')
ORDER BY ts DESC
LIMIT 200;

4-2) 해석 포인트

  • target_processing_time이 600초 근처까지 올라가다 504면
    • ALB가 타겟 응답을 기다리다 끊겼거나(대개 idle timeout), 타겟이 응답을 끝내지 못했을 수 있습니다.
  • target_status_code-로 찍히는 502/504면
    • ALB가 타겟으로부터 HTTP 응답을 아예 못 받았을 가능성이 큽니다(연결 실패/리셋/타임아웃).
  • sent_bytes가 매우 작거나 0에 가까우면
    • 응답 바이트를 거의 못 보내고 끊긴 상황입니다.

5) CloudWatch 지표로 “ALB가 바쁜가, 타겟이 죽었나” 보기

ALB 관련 CloudWatch 지표에서 아래를 같은 시간축으로 확인합니다.

  • HTTPCode_ELB_5XX_Count
  • HTTPCode_Target_5XX_Count
  • TargetResponseTime
  • TargetConnectionErrorCount
  • RejectedConnectionCount

패턴별로 의미가 갈립니다.

  • HTTPCode_ELB_5XX_Count만 증가하고 타겟 5XX는 조용함
    • ALB 레벨에서 타임아웃/연결 문제 가능성.
  • HTTPCode_Target_5XX_Count가 같이 증가
    • 애플리케이션이 실제로 5XX를 반환 중.
  • TargetConnectionErrorCount가 증가
    • 타겟 연결 자체가 불안정(보안그룹, NACL, 타겟 다운, 포트 미리스닝 등).

6) 가장 흔한 원인: ALB idle timeout(또는 앞단 프록시 timeout)

6-1) ALB idle timeout 확인/변경

콘솔에서 Load Balancer attributes의 idle_timeout.timeout_seconds를 확인합니다.

CLI로도 확인 가능합니다.

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:region:acct:loadbalancer/app/xxx \
  --query 'Attributes[?Key==`idle_timeout.timeout_seconds`].Value' \
  --output text

변경 예시는 다음과 같습니다.

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:region:acct:loadbalancer/app/xxx \
  --attributes Key=idle_timeout.timeout_seconds,Value=1200

주의할 점:

  • idle timeout을 늘리는 게 “정답”이 아니라, 장시간 요청을 HTTP로 붙잡아 두는 설계 자체를 재검토해야 하는 경우가 많습니다.
  • 그래도 늘려야 한다면, 앞단(CloudFront/Nginx/API Gateway) 타임아웃도 같이 확인해야 합니다. 한 군데만 늘리면 다른 곳에서 동일 증상이 반복됩니다.

6-2) 앱이 10분 동안 무응답이면 “주기적 바이트 전송”이 필요할 수 있음

서버가 계산 중이라도, 중간 장비는 “유휴”로 판단하면 끊습니다. 가능한 해결책:

  • SSE(Server-Sent Events)나 chunked response로 진행 상황을 주기적으로 flush
  • 비동기 작업으로 전환(아래 9절)

7) EKS/ECS에서 자주 놓치는 것: Ingress/Nginx/Envoy 타임아웃

ALB Ingress Controller(현 AWS Load Balancer Controller) 앞뒤로 Nginx/Envoy를 두는 구성에서는, 실제 600초 제한이 Ingress 컨트롤러가 아니라 Nginx/Envoy 설정인 경우가 많습니다.

예: Nginx Ingress에서 600초로 제한되는 전형적인 설정들

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
spec:
  rules:
    - host: your-domain.example.com
      http:
        paths:
          - path: /long-task
            pathType: Prefix
            backend:
              service:
                name: app
                port:
                  number: 80

Envoy/Service Mesh를 쓰면 route timeout, idle timeout이 별도로 존재합니다. “정확히 10분”은 이런 레이어에서 나오는 경우가 많습니다.

8) 애플리케이션 런타임 타임아웃(정확히 600초 패턴)

프레임워크/서버별로 대표적인 타임아웃 지점을 체크합니다.

8-1) Gunicorn(파이썬) 600초 타임아웃

# gunicorn.conf.py
timeout = 600
graceful_timeout = 30
  • 워커가 600초 동안 응답을 못 끝내면 워커가 재시작되며, ALB에서는 502/504로 보일 수 있습니다.

8-2) Node.js + reverse proxy

Node 자체는 “요청 처리 타임아웃”이 명시적으로 없더라도, 앞단 프록시/로드밸런서가 끊고 나면 서버는 ECONNRESET 같은 로그를 남길 수 있습니다.

Express 예:

app.get('/long-task', async (req, res) => {
  req.setTimeout(0); // 무제한처럼 보이지만, 앞단 타임아웃이 더 중요
  // 장시간 작업...
  res.json({ ok: true });
});

이런 코드는 “서버는 계속 일했는데 클라이언트는 이미 끊김”을 만들기 쉽습니다. 결과적으로 재시도 폭탄, 중복 작업을 유발합니다.

9) 근본 해결: 장시간 HTTP 요청을 비동기로 바꾸기

10분짜리 동기 HTTP는 운영에서 여러 문제가 생깁니다.

  • 중간 프록시/로드밸런서 타임아웃
  • 배포/스케일링 중 커넥션 끊김
  • 모바일/브라우저 네트워크 변동에 취약
  • 재시도로 중복 실행(데이터 정합성 문제)

권장 패턴:

  1. 요청은 즉시 202 Accepted로 반환
  2. 백그라운드 작업은 SQS, EventBridge, Step Functions, Celery, Sidekiq 등으로 처리
  3. 결과는 폴링 API 또는 SSE/WebSocket으로 조회

간단한 형태의 API 예시(의사코드):

# 1) 작업 생성
POST /reports
# 응답: 202 + job_id

# 2) 상태 조회
GET /reports/{job_id}
# 응답: pending|running|done|failed

이 구조로 바꾸면 ALB idle timeout을 억지로 늘릴 필요가 크게 줄어듭니다.

10) 체크리스트: 30분 안에 범위 좁히기

아래 순서대로 보면, “ALB 문제인지/타겟 문제인지/앞단 문제인지”가 빠르게 갈립니다.

  1. curltime_total이 600초 근처인지 확인(클라이언트 타임아웃 배제)
  2. ALB 액세스 로그에서 해당 요청의 elb_status_code, target_status_code, target_processing_time 확인
  3. ALB attributes의 idle_timeout.timeout_seconds 확인
  4. 앞단(CloudFront/Nginx/API Gateway) 타임아웃 설정 확인
  5. 타겟 런타임(Gunicorn/Nginx/Envoy) 600초 설정 여부 확인
  6. CloudWatch에서 같은 시각 TargetConnectionErrorCount 및 5XX 분리 확인
  7. EKS라면 egress/NAT 경로 이슈도 점검(간헐 드롭/세션 정리)

네트워크 레이어가 의심되면, EKS 환경에서의 추적 방법은 EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법 글의 접근(Flow Log, conntrack, NAT GW 지표)이 그대로 적용됩니다.

11) 마무리: “10분”은 단서이고, 답은 타임라인이다

ALB 502/504가 10분 전후로 발생하면, 대부분은 어딘가에 600초로 설정된 타임아웃이거나 유휴 연결 드롭입니다. 해결의 핵심은 설정을 무작정 늘리는 게 아니라,

  • ALB 액세스 로그의 시간 필드들로 “어디에서 멈췄는지”를 확인하고
  • 앞단 프록시와 타겟 런타임까지 타임아웃 체인을 일관되게 정리하며
  • 가능하면 장시간 작업을 비동기 아키텍처로 전환하는 것입니다.

이 과정을 따라가면 “왜 하필 10분인지”가 수치로 설명되고, 재발도 줄일 수 있습니다.