Published on

EKS ALB Ingress 504(5xx) 간헐 발생 원인·해결

Authors

EKS에서 ALB Ingress(= AWS Load Balancer Controller가 만드는 ALB)를 쓰다 보면 “평소엔 잘 되는데” 특정 시간대나 특정 Pod로 라우팅될 때만 504(또는 502/503) 같은 5xx가 간헐적으로 튀는 문제가 자주 나옵니다. 이 유형은 단일 설정 오타보다 타임아웃/헬스체크/드레인/네트워크/DNS/애플리케이션 리소스가 결합된 경우가 많아서, 원인별로 신호를 분리해 빠르게 좁혀야 합니다.

이 글은 “ALB가 504를 냈다”를 출발점으로, 어떤 로그/메트릭을 먼저 보고, 어떤 설정을 어떻게 바꾸면 재발을 막을 수 있는지 실전 관점으로 정리합니다.

1) 504/5xx의 의미를 먼저 분해하기

ALB Ingress에서의 5xx는 크게 두 갈래입니다.

  • ALB가 클라이언트에게 5xx를 응답: 대상(Target)과의 통신/응답이 문제
  • 백엔드가 5xx를 응답했고 ALB는 전달만: 애플리케이션 에러

특히 504는 “대상으로부터 제때 응답을 못 받음” 쪽이 대부분입니다. 하지만 “제때”의 기준이 여러 층에 존재합니다.

  • (A) ALB Idle timeout (기본 60s)
  • (B) 애플리케이션/프레임워크 타임아웃 (예: Uvicorn/Gunicorn, Node/Java 서버, DB 클라이언트)
  • (C) Kubernetes readiness/liveness, terminationGracePeriod, preStop
  • (D) Target Group health check (경로, 성공 코드, 타임아웃, interval)
  • (E) 네트워크 레벨: Security Group/NACL/노드/ENI/CNI 이슈
  • (F) DNS/서비스 디스커버리: CoreDNS 간헐 실패로 upstream 호출이 느려짐

“간헐적”이라는 단서는 보통 (C)(D)(F) 또는 특정 Pod/노드에만 발생하는 (E)(리소스)에서 많이 나옵니다.

2) 가장 먼저 확인할 것: ALB Access Log로 504의 주체 판단

가능하면 ALB Access Log를 S3로 켜고, 504가 발생한 요청의 필드를 봅니다.

  • elb_status_code: ALB가 클라이언트에 준 코드
  • target_status_code: 대상이 준 코드(대상이 응답을 못 하면 -)
  • request_processing_time, target_processing_time, response_processing_time

판단 규칙(실무에서 유용한 패턴):

  • elb_status_code=504 + target_status_code=-대상과 연결/응답 자체가 안 됨(드레인, 네트워크, 타임아웃)
  • elb_status_code=502/503 + target_status_code=-연결 실패/대상 없음/헬스체크 실패
  • elb_status_code=200인데 사용자 체감이 끊김 → 클라이언트/중간 프록시 타임아웃 가능

ALB 로그를 못 켠 상태라면, 최소한 CloudWatch의 ALB 메트릭을 봅니다.

  • HTTPCode_Target_5XX_Count
  • HTTPCode_ELB_5XX_Count
  • TargetResponseTime
  • HealthyHostCount

ELB_5XX가 오르면 로드밸런서 레벨, Target_5XX가 오르면 애플리케이션 레벨 가능성이 큽니다.

3) 원인 1: ALB Idle timeout(60s) vs 앱 처리시간 불일치

가장 흔한 504는 단순합니다.

  • 요청이 60초(기본 idle timeout) 를 넘는 순간
  • ALB가 연결을 끊고 504를 반환

특히 다음 케이스에서 자주 터집니다.

  • LLM/리포트/배치성 API처럼 처리 시간이 들쭉날쭉
  • SSE/스트리밍/롱폴링에서 버퍼링/프록시 설정이 맞지 않음

스트리밍/프록시 환경에서 끊김을 다룬 체크리스트는 아래 글도 함께 보면 좋습니다.

해결: ALB idle_timeout 조정(ingress annotation)

AWS Load Balancer Controller에서 ALB 속성은 annotation으로 설정합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=180
spec:
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-svc
                port:
                  number: 80

주의할 점:

  • idle timeout을 늘리는 것은 “진짜로 오래 걸리는 요청”이 있을 때만. 무작정 늘리면 느린 요청이 자원을 더 오래 점유합니다.
  • 앱 서버의 keep-alive/worker timeout도 함께 정합성을 맞춰야 합니다.

4) 원인 2: Target Group health check 설정 불일치(간헐 503/504 유발)

헬스체크가 실제 앱 특성과 맞지 않으면, 특정 Pod가 헬시에서 언헬시로 흔들리며 간헐 오류가 납니다.

대표 패턴:

  • /health가 DB/외부 API까지 확인 → 순간적인 의존성 지연이 헬스체크 실패로 전이
  • 헬스체크 timeout/interval이 너무 공격적
  • 성공 코드 범위가 맞지 않음(예: 204 반환인데 200만 허용)

해결: 헬스체크는 “가볍고 결정적”이어야 함

  • readiness와 ALB 헬스체크는 가능하면 가벼운 엔드포인트(프로세스/이벤트루프 살아있음 정도)
  • 의존성(DB 등)은 별도의 /ready에서 확인하고, 트래픽 유입 여부는 readiness로 제어

Ingress 예시:

metadata:
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: "15"
    alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "5"
    alb.ingress.kubernetes.io/healthy-threshold-count: "2"
    alb.ingress.kubernetes.io/unhealthy-threshold-count: "2"
    alb.ingress.kubernetes.io/success-codes: "200-399"

추가로, HealthyHostCount가 순간적으로 0 또는 급감하는지 보면 헬스체크 흔들림을 빠르게 확인할 수 있습니다.

5) 원인 3: Pod 종료/롤링업데이트 중 드레인 미흡(간헐 502/504)

“간헐”의 강력한 단서 중 하나는 배포/스케일링/노드 교체 타이밍에만 5xx가 늘어나는 경우입니다.

문제 메커니즘:

  • Pod가 SIGTERM을 받고 종료 중인데
  • 아직 Target Group에서 완전히 빠지지 않았거나
  • 앱이 새 연결을 받지 못하는 상태에서 요청이 들어와 504/502가 발생

해결 1: terminationGracePeriod + preStop + readiness 연동

아래는 전형적인 패턴입니다.

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: app
          image: your/app:tag
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            periodSeconds: 5
            failureThreshold: 1
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 15"]

핵심은:

  • SIGTERM 직후 readiness를 실패시키거나(앱에서 구현)
  • preStop으로 타겟에서 빠질 시간을 벌어 새 요청 유입을 줄이는 것

해결 2: Target Group deregistration delay 확인

ALB Target Group에는 deregistration delay(드레인 시간)가 있습니다. 앱의 종료/요청 처리 시간과 맞춰야 합니다.

  • 긴 요청이 있으면 delay를 늘리고
  • 짧은 요청만 있으면 너무 길게 잡지 않습니다.

(콘솔 또는 IaC로 설정. Controller에서도 일부 속성은 annotation으로 제어 가능)

6) 원인 4: 노드/Pod 리소스 압박으로 “특정 Pod만” 느려짐

간헐 504는 종종 느린 Pod로 라우팅될 때만 발생합니다.

체크 포인트:

  • HPA가 늦게 반응하거나, scale-out 직후 cold start로 느림
  • CPU throttling, 메모리 압박(OOM 직전), GC stop-the-world
  • 워커 수 부족(예: Gunicorn worker 1개로 장시간 블로킹)

확인 방법

  • kubectl top pod -n <ns>로 순간 피크 확인
  • 애플리케이션 지표(P99 latency, queue length)
  • ALB TargetResponseTime P95/P99가 튀는지

해결 방향

  • requests/limits 재설계(특히 CPU limit로 인한 throttling 주의)
  • 워커/스레드/이벤트루프 모델 점검
  • HPA 스케일 정책을 “피크 전에” 반응하도록 조정

7) 원인 5: CoreDNS/DNS 간헐 실패 → upstream 호출 지연 → 504

백엔드가 내부적으로 다른 서비스(예: RDS, 외부 API, 내부 마이크로서비스)를 호출하는데, DNS가 간헐적으로 느려지면 애플리케이션은 대기하다가 결국 타임아웃에 걸립니다. 이때 ALB는 “응답이 없다”로 504를 낼 수 있습니다.

이 패턴은 로그에 흔히 이렇게 남습니다.

  • Temporary failure in name resolution
  • i/o timeout (특히 첫 연결에서)

EKS에서 CoreDNS가 “정상처럼 보이는데” DNS가 간헐 실패하는 케이스는 아래 글을 같이 참고하면 진단이 빨라집니다.

빠른 재현/검증용 명령

문제 시간대에 Pod 안에서 DNS/연결을 직접 확인합니다.

kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- sh

# Pod 내부
nslookup your-upstream.default.svc.cluster.local
wget -S -O- http://your-upstream.default.svc.cluster.local:8080/healthz

해결은 CoreDNS 리소스/노드 로컬 DNS 캐시/네트워크 경로/업스트림 리졸버 문제 등으로 갈리므로, “ALB 문제”로만 좁히지 않는 게 중요합니다.

8) 원인 6: 보안그룹/네트워크 정책/CNI 이슈로 특정 경로만 드랍

ALB → Node/Pod 경로는 보안그룹, 서브넷 라우팅, CNI(aws-vpc-cni), NetworkPolicy(사용 시) 등 여러 레이어를 통과합니다. 간헐 504는 “패킷 드랍/재전송”에서도 발생합니다.

특히 다음 상황을 의심합니다.

  • 특정 노드에만 몰릴 때 실패
  • 재시도하면 바로 성공
  • target_status_code=-가 자주 보임(연결 실패)

네트워크 관점에서 “Pod는 뜨는데 트래픽이 0/간헐”을 빠르게 진단하는 흐름은 아래 글이 도움이 됩니다.

9) 실전 트러블슈팅 순서(체크리스트)

간헐 504/5xx는 “추측”보다 “증거 기반 분기”가 훨씬 빠릅니다. 아래 순서로 보면 평균적으로 시간을 가장 아낍니다.

1) ALB Access Log/메트릭으로 범주화

  • ELB_5XX vs Target_5XX
  • target_status_code- 인지
  • TargetResponseTime이 튀는지

2) 배포/스케일 이벤트와 상관관계 확인

  • 5xx 스파이크 시점에 Deployment rollout, HPA scale, 노드 드레인 있었는지

3) 타임아웃 정합성 점검

  • ALB idle timeout
  • 앱 서버 타임아웃(keepalive, request timeout)
  • upstream 클라이언트 타임아웃(DB/HTTP)

4) 헬스체크/레디니스 설계 재검토

  • 헬스체크 엔드포인트가 무겁지 않은지
  • 성공 코드, timeout/interval 적절한지

5) 특정 Pod/노드 편향 확인

  • 에러가 특정 target에 몰리는지
  • CPU throttling/메모리 압박/GC

6) DNS/네트워크 계층 확인

  • CoreDNS 지연/실패
  • SG/NACL/NetworkPolicy/CNI

10) “해결됐는지”를 검증하는 방법

수정 후에는 단순히 에러가 안 보이는 것보다, 재발 조건에서 지표가 개선되는지 확인해야 합니다.

  • 동일 트래픽에서 TargetResponseTime P95/P99 감소
  • HealthyHostCount 안정화(흔들림 제거)
  • 롤링 업데이트 중 5xx 스파이크가 사라짐
  • 애플리케이션 로그에서 타임아웃/리졸브 에러 감소

부하 테스트(짧게라도)로 “60초 근처” 같은 경계 조건을 다시 밟아보면, idle timeout/워커/드레인 문제는 금방 재현됩니다.

마무리

EKS의 ALB Ingress 504/5xx 간헐 문제는 ALB 자체 결함이라기보다, 타임아웃 정합성 + 헬스체크 설계 + 종료/드레인 + 내부 DNS/네트워크 + 리소스가 맞물려 생기는 경우가 대부분입니다.

가장 중요한 것은 (1) ALB 로그/메트릭으로 “누가 5xx를 만들었는지”를 먼저 나누고, (2) 배포/스케일/종속성 지연과의 상관관계를 통해 원인을 좁힌 뒤, (3) 타임아웃/헬스체크/드레인 설계를 체계적으로 맞추는 것입니다.

이 흐름대로 접근하면 “가끔 난다”는 애매한 문제도 대개 1~2회 관측으로 원인을 특정할 수 있습니다.