- Published on
Kubernetes HPA가 0으로 안 줄 때 - PDB·윈도우·종료
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스처럼 트래픽이 없으면 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>
여기서 특히 아래를 확인합니다.
Conditions에AbleToScale,ScalingActive,ScalingLimitedEvents에not 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.stabilizationWindowSecondspolicies가 지나치게 보수적인지
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로 명시 + PDBminAvailable: 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 같은 이벤트 기반까지 포함해 설계를 한 번에 정리하는 것이 운영 비용과 장애 가능성을 함께 줄이는 길입니다.