Published on

Kubernetes HPA가 0으로 안 줄 때 - PDB·윈도우·종료

Authors

서버리스처럼 트래픽이 없으면 0으로 내려가길 기대했는데, Kubernetes HPA가 minReplicas: 0임에도 1개(혹은 그 이상)에서 멈추는 경우가 있습니다. 이 문제는 단순히 “HPA가 버그다”라기보다, HPA가 내리는 축소 결정이 다른 컨트롤러/정책/종료 흐름과 충돌하거나, **스케일다운이 ‘허용되지 않는 상태’**로 판단되는 경우가 대부분입니다.

이 글에서는 실제 운영에서 자주 만나는 원인을 4갈래로 나눠서 정리합니다.

  • (A) 애초에 HPA만으로는 0 스케일이 안 되는 케이스(전제 조건)
  • (B) PDB(PodDisruptionBudget)가 축소를 막는 케이스
  • (C) stabilizationWindowSeconds / behavior 정책이 축소를 지연시키는 케이스
  • (D) 종료(termination)·트래픽·프로브·PDB 외 요인이 “항상 1개를 남기는” 케이스

중간중간 kubectl로 바로 확인 가능한 커맨드와 YAML 예제를 포함합니다.

1) 먼저 확인: HPA만으로 ‘Scale to Zero’가 되는가?

가장 중요한 전제부터 짚어야 합니다.

HPA는 기본적으로 0 스케일을 “항상” 보장하지 않는다

HPA는 메트릭 기반으로 replica 수를 계산합니다. 그런데 replica가 0이 되면:

  • 파드가 없어 메트릭 수집 대상이 사라지고
  • 특히 CPU/메모리 기반 HPA는 0에서 다시 올릴 트리거가 애매해집니다

그래서 “0으로 내리는 것” 자체는 가능해도, “0에서 다시 올리는 것”은 요청 기반 이벤트가 필요합니다. 이 때문에 보통은 다음 조합으로 해결합니다.

  • KEDA(Kafka/SQS/Prometheus/HTTP 등 이벤트 기반) + HPA
  • Knative(요청 기반 scale-to-zero)
  • (또는) External metrics로 ‘요청 수/큐 길이’ 같은 지표를 넣어 0에서도 스케일업 신호를 만들기

즉, 지금 문제가 “0으로 안 내려감”이라면 아래 원인들을 의심하면 되고, 동시에 “0에서 다시 올라가야 함”까지 요구사항이라면 KEDA/Knative 검토가 사실상 필수입니다.

현재 HPA 상태부터 읽기

먼저 HPA가 무엇 때문에 스케일다운을 안 하는지 이벤트/조건부터 봅니다.

kubectl describe hpa -n <ns> <hpa-name>

여기서 특히 아래를 확인합니다.

  • ConditionsAbleToScale, ScalingActive, ScalingLimited
  • Eventsnot enough replicas, failed to get metrics, backoff, stabilized 같은 메시지

또한 대상 스케일러(보통 Deployment/ReplicaSet)도 같이 봅니다.

kubectl get deploy -n <ns> <deploy-name> -o wide
kubectl describe deploy -n <ns> <deploy-name>

2) PDB가 스케일다운을 막는 대표 패턴

PDB는 “자발적(eviction) 중단”에서 최소 가용 파드를 보장합니다. 운영자 입장에서는 안정장치지만, 설정에 따라 HPA가 줄이려는 파드를 끝까지 못 줄이는 현상이 생깁니다.

왜 PDB가 HPA 스케일다운에 영향을 주나?

엄밀히 말하면 HPA는 Deployment의 replica를 줄이고, ReplicaSet이 파드를 종료합니다. PDB는 원래 드레인/업그레이드 같은 eviction에 적용되는 것이 핵심입니다.

그런데 실전에서는 아래처럼 PDB가 “항상 1개 남기기”를 강제하는 간접 효과를 만들 수 있습니다.

  • 클러스터 오토스케일러/노드 드레인 과정에서 eviction이 필요할 때 PDB가 막아 파드가 계속 남음
  • 일부 운영 도구(롤링/리밸런싱/드레인 자동화)가 eviction 기반으로 파드 조정을 시도
  • 결과적으로 스케일다운이 진행되더라도 파드가 완전히 사라지지 않는 상태가 지속

PDB에서 가장 흔한 함정: minAvailable=1

예를 들어 아래 PDB는 “항상 최소 1개는 살아야 한다”를 강제합니다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
  namespace: prod
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: api

이 상태에서 minReplicas: 0을 기대하면 정책 충돌이 발생할 수 있습니다. 특히 노드 유지보수/리밸런싱 시나리오에서 파드가 내려가지 못하고 남는 현상이 더 자주 드러납니다.

해결 방향

  • “정말 0으로 내려도 되는 워크로드”라면 PDB를 제거하거나, 최소 가용을 0으로 설계해야 합니다.
  • 완전한 0 스케일이 목표면, PDB는 보통 적용 대상에서 제외하거나, 조건부로만 적용하는 게 안전합니다.

예: PDB를 없애는 대신, 장애 내성을 다른 방식(다중 AZ, 재시도, 큐잉)으로 보완합니다.

PDB가 실제로 영향을 주는지 확인하려면 아래를 봅니다.

kubectl get pdb -n <ns>
kubectl describe pdb -n <ns> <pdb-name>

DisruptionsAllowed: 0가 장기간 유지된다면, “무언가를 내보내지 못하는” 상태일 수 있습니다.

3) behavior와 stabilizationWindowSeconds가 스케일다운을 ‘의도적으로’ 늦춘다

HPA v2에는 behavior가 있고, 그 안에 scaleDown.stabilizationWindowSeconds가 있습니다. 이 값이 크면 “최근 N초 동안의 추천 replica 중 가장 큰 값”을 유지하려고 해서 트래픽이 이미 꺼졌는데도 한참 동안 내려가지 않는 것처럼 보일 수 있습니다.

전형적인 증상

  • 트래픽 0, CPU 0인데도 5~10분 이상 replica가 유지
  • kubectl describe hpa 이벤트에 stabilized 같은 메시지

예시: 스케일다운 안정화 창이 10분

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 0
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60

이 설정은 “다운스케일은 최대한 빨리(100%) 하되, 최근 600초 동안의 추천치를 안정화해서 갑작스런 흔들림을 막겠다”는 의미가 됩니다.

해결 방향

  • 비용 최적화가 목적이라면 stabilizationWindowSeconds를 줄이거나(예: 60~120초)
  • scaleDown 정책을 더 공격적으로 바꾸는 대신, 서비스 품질은 다른 계층에서 보호(예: 큐잉, 캐시, 서킷 브레이커)하는 편이 낫습니다.

참고로 API 호출량 급증/장애 시 재시도 폭탄을 막는 패턴은 애플리케이션 레벨에서도 중요합니다. 장애/지연 상황에서의 방어 설계는 아래 글이 함께 도움이 됩니다.

4) 종료(termination) 지연이 ‘항상 1개 남는’ 것처럼 보이게 만든다

HPA가 replica를 줄였는데도 파드가 계속 보인다면, 실제로는 파드가 Terminating 상태로 오래 머무는 것일 수 있습니다.

체크: 파드가 Terminating에 오래 머무는가?

kubectl get pod -n <ns> -l app=api -o wide
kubectl get pod -n <ns> -l app=api -w

Terminating이 길면 아래를 의심합니다.

  • terminationGracePeriodSeconds가 과도하게 큼
  • preStop 훅이 오래 걸리거나 hang
  • 애플리케이션이 SIGTERM을 제대로 처리하지 못해 종료가 지연
  • 최종적으로 kubelet이 SIGKILL까지 기다리는 시간이 길어짐

예시: preStop으로 드레이닝하는데 120초 대기

spec:
  terminationGracePeriodSeconds: 120
  containers:
  - name: api
    image: example/api:1.2.3
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 30"]

이 경우 스케일다운이 시작돼도 파드가 30초~120초 동안 남아 있으니, 관측상 “0으로 안 줄어드는” 것처럼 보입니다.

해결 방향

  • preStop을 최소화하고(가능하면 즉시 드레인 신호만 남기기)
  • readiness를 먼저 내려 트래픽을 끊은 뒤 빠르게 종료
  • 애플리케이션에서 SIGTERM 처리(서버 close, in-flight 요청 제한)를 구현

만약 L7 프록시/스트리밍 연결(SSE, gRPC 등) 때문에 연결이 길게 유지되어 종료가 늦어지는 경우도 많습니다. 이때는 프록시 타임아웃/버퍼링이 스케일다운 체감에 큰 영향을 줍니다.

5) “메트릭이 0이 아니다” 혹은 “메트릭을 못 가져온다” 문제

HPA는 메트릭이 0이거나 낮으면 줄입니다. 그런데 다음 상황에선 줄지 않습니다.

(1) CPU/메모리가 실제로 0이 아니다

  • 백그라운드 워커/스케줄러가 계속 돌고 있음
  • 로그/메트릭 전송, 폴링, keep-alive 등으로 CPU가 미세하게 유지
  • JVM/런타임 GC, 사이드카(Envoy 등)가 꾸준히 사용

확인은 아래로 합니다.

kubectl top pod -n <ns> -l app=api

(2) metrics-server/adapter 문제로 “메트릭을 못 읽음”

메트릭을 못 읽으면 HPA는 보수적으로 동작하거나(조건에 따라) 스케일 결정을 못 합니다.

kubectl get apiservices | grep metrics
kubectl describe hpa -n <ns> <hpa-name> | sed -n '/Events/,$p'

이벤트에 failed to get cpu utilization 같은 메시지가 있으면 메트릭 경로부터 복구해야 합니다.

6) 실제 트러블슈팅 순서(운영 체크리스트)

현장에서 가장 빠르게 원인을 좁히는 순서입니다.

1) HPA 이벤트/조건 확인

kubectl describe hpa -n <ns> <hpa-name>
  • stabilization/backoff/failed to get metrics 여부

2) behavior 설정 확인(특히 scaleDown)

kubectl get hpa -n <ns> <hpa-name> -o yaml
  • behavior.scaleDown.stabilizationWindowSeconds
  • policies가 지나치게 보수적인지

3) PDB 존재 여부 및 minAvailable/maxUnavailable

kubectl get pdb -n <ns>
kubectl describe pdb -n <ns> <pdb-name>
  • minAvailable: 1 같은 “항상 1개” 강제 설정이 있는지

4) 파드 종료 지연(특히 Terminating) 확인

kubectl get pod -n <ns> -l app=api
kubectl describe pod -n <ns> <pod-name>
  • preStop, terminationGracePeriodSeconds, 종료 이벤트

5) 실제 리소스 사용량/사이드카 영향 확인

kubectl top pod -n <ns> -l app=api
  • CPU가 0이 아닌 이유(워커, 사이드카, 폴링)

7) 권장 설계: “0 스케일”이 목표라면 이렇게 묶어라

HPA만으로 0을 만들려다 운영 안정성이 깨지는 경우가 많습니다. 목표에 따라 조합을 정리하면 다음이 현실적입니다.

  • 비용 최적화(0까지 내림)가 최우선: KEDA/Knative 등 이벤트 기반 오토스케일 도입 + (필요 시) PDB 제거/완화 + scaleDown window 축소
  • 가용성이 최우선(항상 최소 1개): minReplicas: 1로 명시 + PDB minAvailable: 1 유지(의도 일치)
  • 스파이크 대응이 중요: scaleUp은 공격적으로, scaleDown은 완만하게(짧은 안정화 창 + 적절한 정책)

특히 “항상 1개 남는” 현상이 사실은 **설계 의도(가용성 보장)**인지, 아니면 **정책 충돌(0 스케일 목표인데 PDB가 1 강제)**인지부터 합의하는 게 중요합니다.

8) 마무리: 0으로 안 줄어드는 건 ‘대개 정상 동작’이다

정리하면, minReplicas: 0인데도 HPA가 0으로 내려가지 않는 이유는 보통 다음 중 하나입니다.

  • PDB/운영 정책이 사실상 “최소 1개”를 강제
  • stabilizationWindowSeconds가 길어 스케일다운이 늦게 적용
  • 파드 종료가 느려 Terminating이 길게 남음
  • 메트릭이 0이 아니거나, 메트릭 수집 자체가 실패
  • (더 근본적으로) HPA만으로는 0↔N 오토스케일을 안정적으로 만들기 어려워 이벤트 기반 스케일러가 필요

위 체크리스트대로 describe hpa → behavior → pdb → termination → metrics 순서로 보면 대부분 10~20분 안에 원인을 특정할 수 있습니다. 0 스케일이 진짜 목표라면, 마지막으로 KEDA/Knative 같은 이벤트 기반까지 포함해 설계를 한 번에 정리하는 것이 운영 비용과 장애 가능성을 함께 줄이는 길입니다.