[kubernetes] HPA loop 현상 해결 및 Best Practice
들어가기 앞서
현재 운영 중인 서비스에서는 일부 파드에 대해 Horizontal Pod Autoscaler(HPA)가 적용되어 있다. 그런데 파드를 재배포하거나 노드를 교체하는 과정에서, 파드가 새로운 노드로 이동하며 초기화되는 시점에 HPA가 반복적으로 트리거되는 현상이 발생하였다. 이 작업은 일반적으로 트래픽이 적은 새벽 시간대에 수행되었기 때문에, 외부 요청 증가로 인한 리소스 사용 급증이 원인일 가능성은 낮다.
더불어 문제 상황에서는 일시적으로 증가한 파드 수가 이후에도 줄어들지 않고 유지되었으며, 해당 시간대에 트래픽이 거의 없고 리소스 사용률도 낮은 상태였음에도 불구하고 scale in이 발생하지 않았다. 이에 대한 원인 분석과 해결 방안 및 모범 사례에 대해 말하고자 한다.
HPA란?
Horizontal Pod Autoscaler(HPA)는 Kubernetes에서 Pod의 수를 자동으로 증가 또는 감소시키는 기능이다. 일반적으로 CPU 사용률, 메모리 사용률, 또는 Custom Metrics에 따라 동작한다.
- 설정된 메트릭이 기준치 이상 → Pod 수 증가
- 설정된 메트릭이 기준치 이하 → Pod 수 감소
- 컨트롤러가 Metrics Server를 통해 메트릭을 수집하여 판단
아래는 CPU 사용률 기준으로 HPA를 설정하는 예시이다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: example-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: example-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- example-deployment에 대해 HPA를 적용
- 최소 2개, 최대 10개의 Pod로 자동 조절
하지만 HPA가 정상적으로 동작하기 위해서는 아래와 같은 전제조건이 필요하다.
- metrics-server가 클러스터에 설치되어 있어야 함
- Pod에 resources.requests.cpu가 설정되어 있어야 정확히 동작
minReplicas와 maxReplicas는 HPA 리소스의 YAML 파일에 명시하지만, 실제로 얼마나 많은 Pod를 생성할지는 HPA가 동적으로 판단한다. 이때 결정되는 값이 desiredReplicas이며, 아래의 공식을 따른다.
원하는 레플리카 수 = ceil[현재 레플리카 수 * ( 현재 메트릭 값 / 원하는 메트릭 값 )]
만약 Target CPU averageUtilization이 300이고, 현재 사용량이 450이라면 다음과 같이 계산된다.
desiredReplicas = ceil[현재 레플리카 수 × (450 ÷ 300)] = ceil[현재 레플리카 수 × 1.5]
즉, 현재 레플리카 수가 4개였다면
desiredReplicas = ceil[4 × 1.5] = ceil[6] = 6
이처럼 HPA는 메트릭 사용량이 목표보다 높을수록 레플리카 수를 증가시켜, 애플리케이션이 과부하 없이 안정적으로 동작할 수 있도록 자동 조정한다. 반대로 사용량이 낮아지면 레플리카 수를 줄이는 방식으로 리소스를 절약한다. 또한, 만약 CPU와 Memory를 모두 사용하여 Target 을 설정하였다면, CPU 또는 Memory 중 하나라도 목표치를 초과하면 스케일 아웃(확장)이 발생한다.
- 각 메트릭에 대해 위 공식을 적용하여 desiredReplicas를 산출한다.
- 이 중 가장 큰 desiredReplicas 값을 최종 값으로 사용한다.
단, 어떤 메트릭은 오류로 인해 desiredReplicas 계산이 불가능할 수 있다. 이 경우
- 스케일 업이 필요한 메트릭이 있다면 → 스케일 업 진행
- 스케일 인만 필요한 경우라면 → 스케일 동작 없음
즉, 스케일 업은 진행되지만 스케일 인은 보수적으로 판단하여 생략될 수 있다.
HPA가 발동하여 Scale out 하면 다시 Scale in 하기 어렵다.
desiredReplicas 공식의 영향
위에서 언급했듯이 스케일 다운 시에도 여러 메트릭이 있을 때 각 메트릭 기준으로 계산한 desiredReplicas 중 가장 큰 값을 사용한다. 그렇기 때문에 메트릭 중 한개라도 많이 사용한다면 축소될 수 없다. 또한, desiredReplicas 의 공식에 '현재 레플리카 수'가 곱해지기 때문이다. 예를 들어 만약 위와 같은 조건인 Target CPU averageUtilization이 300이라고 가정해보자. 현재 사용량이 260 이고 파드 수가 4개라면 desiredReplicas 는 4이다.
desiredReplicas = ceil(4 × (260 ÷ 300)) = ceil(3.46) = 4
사용량이 높아져 400이 되었을 때 desiredReplicas 는 6으로 확장된다.
desiredReplicas = ceil(4 × (400 ÷ 300)) = ceil(5.33) = 6
이후 트래픽이 줄어 다시 CPU 사용량이 250이 되었다고 해보자.
desiredReplicas = ceil(6 × (260 ÷ 300)) = ceil(5.2) = 6
하지만 이때 desiredReplicas 의 공식에는 Scale out 할때 곱해진 현재 레플리카 수 4가 아닌 6이 곱해진다. 즉, 사용량이 260으로 돌아왔지만 desiredReplicas 는 6으로 유지된다. 즉, 사용량이 평소보다 많이 감소해야 Scale in 이 발생한다.
보수적인 계산 방식에 의한 영향
HPA가 파드의 메트릭을 수집해 스케일 비율을 계산할 때, 일부 파드의 메트릭이 없거나 아직 준비되지 않은 경우가 있다. 이때는 안전하게 계산하기 위해 보수적인 방식으로 처리한 다음, 다시 전체 평균을 계산한다.
- 스케일 인 상황일 때: 메트릭이 없는 파드는 이미 100% 사용 중이라고 가정한다. (즉, 바쁜 파드라고 보고 함부로 줄이지 않음)
- 스케일 아웃 상황일 때: 메트릭이 없는 파드는 0% 사용 중이라고 가정한다. (즉, 아직 여유 있다고 보고 과하게 늘리지 않음)
- 준비되지 않은 파드도 마찬가지로 0% 사용 중으로 본다.
이렇게 보정된 값으로 다시 평균 사용률을 계산하고, 만약 새 계산 결과가 원래 스케일 방향을 바꾸거나, 이미 목표 사용률과 거의 같아(허용 오차 범위 안에) 스케일이 불필요하다고 판단되면, 실제 스케일 동작은 하지 않는다. HPA에 여러 메트릭이 지정된 경우,
- 각 메트릭에 대해 위 공식을 적용하여 desiredReplicas를 산출한다.
- 이 중 가장 큰 desiredReplicas 값을 최종 값으로 사용한다.
단, 어떤 메트릭은 오류로 인해 desiredReplicas 계산이 불가능할 수 있다. 이 경우
- 스케일 업이 필요한 메트릭이 있다면 → 스케일 업 진행
- 스케일 인만 필요한 경우라면 → 스케일 동작 없음
즉, 스케일 업은 진행되지만 스케일 인은 보수적으로 판단하여 생략될 수 있다.
Memory는 해제되기 어렵다
CPU는 시분할 방식으로 동작하며, 애플리케이션의 부하에 따라 필요한 만큼 동적으로 할당된다. 반면 메모리는 한 번 할당되면 애플리케이션이 명시적으로 해제하지 않는 이상 시스템 차원에서 회수되지 않으며, 정적으로 유지되는 경우가 많다. 예를 들어, 애플리케이션이 기본적으로 데이터베이스와의 커넥션을 유지하고 있다면 해당 커넥션에 할당된 메모리는 지속적으로 점유된다.
이런 특성 때문에 HPA의 기준으로 CPU를 사용하는 경우, 사용량이 감소하면 자연스럽게 scale-in이 발생할 수 있다. CPU는 유휴 상태일 때 사용률이 감소하기 때문이다.
반면, Memory를 기준으로 HPA를 구성할 경우 메모리 사용률은 쉽게 감소하지 않으며, 애플리케이션이 메모리를 적극적으로 반환하지 않는 이상 scale-in이 일어나기 어렵다. 이러한 구조는 HPA의 목적에 부합하지 않으며, 실질적으로 비효율적이고 바람직하지 않다.
HPA가 loop로 발생하는 이유와 해결 방안
HPA 구성 시에 CPU와 같은 Threshold 값을 정의하여 파드 자원이 재배포되거나 추가될 때 초기화 및 업데이트 같이 CPU 자원 사용이 많은 경우에는 다시 파드 자원이 추가되고 CPU 사용률이 늘어나는 것 같은 루프 현상이 발생할 수 있다. 하지만, Kubernetes는 이를 방지하기 위한 메커니즘을 가지고 있다.
https://kubernetes.io/ko/docs/tasks/run-application/horizontal-pod-autoscale/
기본적으로 HPA는 안정화 기간 (Stabilization period)을 가지고 있어서 급격한 스케일링을 방지하는 것은 물론 새로 생성된 파드의 메트릭이 안정화될 때까지 기다리도록 설계되어 있다. 또한 scaleUp/scaleDown.stabilizationWindowSeconds 값을 정의하여 원하는 시간 만큼 안정화에 필요한 시간을 조정할 수도 있다. (아래 포스팅의 안정화 윈도우 참고)
Horizontal Pod Autoscaling
쿠버네티스에서, HorizontalPodAutoscaler 는 워크로드 리소스(예: 디플로이먼트 또는 스테이트풀셋)를 자동으로 업데이트하며, 워크로드의 크기를 수요에 맞게 자동으로 스케일링하는 것을 목표로 한
kubernetes.io
[kubernetes] Horizontal Pod Autoscaling (HPA)
Horizontal Pod AutoscalingKubernetes에서 HorizontalPodAutoscaler(HPA)는 Deployment나 StatefulSet 같은 워크로드 리소스를 자동으로 업데이트하여, 수요에 따라 파드 수를 자동으로 조절한다. 수평 스케일링(horizontal
hyukops.tistory.com
HPA 의 Best Practice 는?
기본적으로는
우선 위에서 설명한대로 기본적으로 Memory 지표는 Target으로 적용하지 않는 것이 좋다. 만약 Istio 등을 사용하지 않고 커스텀 메트릭을 따로 사용하지 않는다면 CPU 지표를 유일한 Target으로 사용하는 것이 좋다. 하지만, 만약 갑작스럽게 트래픽이 폭주한다면, 노드의 파드의 CPU 사용량이 늘어나고 이에 반응하여 HPA가 발동하기까지 시간이 오래 걸릴 수 있다. 또한, 앞서 말한대로 파드를 재배포하는 과정을 모니터링하고 stabilizationWindowSeconds 을 설정하여 loop 현상을 방지하는 것이 좋다.
개인적인 경험상 파드 배포가 시작되고 정상적으로 RUNNING 되기 까지 시간의 3배 정도를 설정하는 것이 좋더이다..
Keda 등을 사용한다면
KEDA(Kubernetes Event-Driven Autoscaling)를 사용하는 환경에서 Redis를 세션 저장소로 활용하고 있다면, Redis의 지표를 기반으로 커스텀 메트릭을 생성하여 HPA의 타겟으로 지정하는 것이 가능하다. 예를 들어, Redis에 저장된 세션 수, 큐의 길이, 혹은 특정 키의 항목 수 등과 같은 애플리케이션 특화 지표를 Prometheus 또는 Redis Exporter를 통해 수집한 뒤, 이를 KEDA의 ScaledObject 또는 ScaledJob 리소스를 통해 autoscaling에 활용할 수 있다.
Istio와 Prometheus를 사용한다면 (가장 추천)
Istio와 Prometheus를 함께 사용하고 있다면, istio-ingressgateway의 트래픽 지표를 Prometheus를 통해 수집하고, 이를 기반으로 커스텀 메트릭을 생성하여 HPA의 스케일링 기준으로 활용하는 것이 바람직하다. 그 이유는, CPU나 메모리 사용량을 기준으로 HPA를 설정할 경우 실제 트래픽이 증가해 리소스가 소진되기까지 일정 시간이 소요되며, 이로 인해 대응이 늦어질 수 있기 때문이다. 특히 istio-ingressgateway는 클러스터 내 외부 트래픽의 진입점 역할을 하므로, 트래픽 변화에 가장 먼저 노출되는 컴포넌트이다. 따라서 요청 수(request count), 초당 요청량(RPS), 또는 큐 길이 등 트래픽 관련 지표를 기반으로 HPA를 설정하면, 리소스 사용량 급증 이전에 선제적으로 스케일 아웃을 유도할 수 있어 보다 효과적인 대응이 가능하다.