HPA는 왜 파드를 한 번에 2배로 늘릴 수 있는가 — desiredReplicas 비례 제어와 비대칭 스케일링

seonwoo_jung·4일 전

1. 도입 — "CPU 넘으면 1개씩 늘어난다"는 오해

HPA(Horizontal Pod Autoscaler)를 처음 쓸 때 나는 이렇게 알고 있었다. "CPU가 임계치를 넘으면 파드가 하나씩 늘어나고, 떨어지면 하나씩 줄어든다." 그런데 실제 클러스터에서 트래픽이 몰리자 replicas가 3에서 6으로 한 번에 뛰는 걸 보고 당황했다. 1씩 증가가 아니었다.

확인해 보니 HPA는 증분(increment) 제어기가 아니라 목표 사용률로 수렴시키는 비례(proportional) 제어기였다. 이 글은 HPA가 다음 replica 수를 결정하는 하나의 핵심 공식과, 그 결과가 진동하지 않도록 받치는 보정 장치들(tolerance, not-ready 보정, 안정화 윈도우, behavior 정책)을 Kubernetes 공식 문서의 알고리즘 절을 따라가며 정리한 것이다. HPA를 "알아서 늘려주는 마법"이 아니라 예측 가능한 계산식으로 다루는 게 목표다.

2. 핵심 공식 — 증분이 아니라 비율

Kubernetes 공식 문서(Horizontal Pod Autoscaling)에 따르면 기본 계산식은 다음 한 줄이다.

desiredReplicas = ceil[ currentReplicas × (currentMetricValue / desiredMetricValue) ]

핵심은 currentMetricValue / desiredMetricValue라는 비율이다. 현재 Ready 파드들의 평균 CPU가 200m이고 목표가 100m이면 비율은 2.0 → replica를 2배로. 평균이 50m로 떨어지면 비율 0.5 → 절반으로. 즉 HPA는 "1만큼 더/덜"이 아니라 "목표 사용률에 도달하려면 지금 몇 개가 필요한가"를 매번 통째로 다시 계산한다.

HPA는 임계치를 넘었는지를 보고 한 칸씩 움직이는 게 아니라, 목표 비율을 만족시키는 replica 수를 한 번에 산출하는 비례 제어기다.

그래서 트래픽이 급증하면 3 → 6처럼 한 번에 2배가 될 수 있고, 반대로 절반으로 줄 수도 있다. 이 변화 폭을 제어하는 건 뒤에서 볼 behavior 정책뿐이다.

3. 진동을 막는 1차 방어선 — tolerance와 not-ready 보정

비례 제어는 강력하지만, 메트릭이 목표 근처에서 미세하게 출렁이기만 해도 replica가 흔들릴 위험이 있다. HPA에는 이를 막는 장치가 여러 겹 있다.

tolerance(기본 0.1). 비율이 1.0에 충분히 가까우면 — 즉 0.9 ~ 1.1 구간이면 — 아무 동작도 하지 않는다. 목표 근처의 잔떨림을 무시하는 1차 방어선이다.

not-ready·결측 메트릭의 비대칭 보정. 여기가 HPA에서 가장 헷갈리는 디테일이다. 아직 Ready가 아니거나 메트릭이 없는 파드를 스케일 방향에 따라 다르게 가정한다.

상황스케일 판단 시스케일 다운 판단 시
메트릭 결측 파드사용률 0% 로 가정사용률 100% 로 가정
아직 Ready 아닌 파드사용률 0% 로 가정(사실상 억제)계산에서 제외

방향이 반대인 이유는 실패 비용이 비대칭이기 때문이다. 업스케일을 과하게 하면 비용 폭증으로 끝나지만, 다운스케일을 과하게 하면 가용성이 무너진다. 그래서 결측 파드를 업 계산에선 0%(더 늘릴 근거를 약화), 다운 계산에선 100%(함부로 줄이지 못하게)로 둔다. "업은 보수적으로 작게, 다운은 보수적으로 작게" — 즉 양쪽 다 성급한 변화를 누른다.

여기에 새 파드의 워밍업을 위한 유예도 있다. 공식 문서 기준 --horizontal-pod-autoscaler-initial-readiness-delay(기본 30초)와 --horizontal-pod-autoscaler-cpu-initialization-period(기본 5분) 동안의 파드는 메트릭을 액면 그대로 믿지 않는다. 방금 뜬 파드의 콜드스타트 CPU 스파이크가 곧바로 추가 업스케일을 부르는 폭주를 막기 위해서다.

4. 시간축 방어선 — 안정화 윈도우와 behavior 정책

tolerance가 값(value) 축의 방어선이라면, 안정화 윈도우(stabilization window) 는 시간(time) 축의 방어선이다. 계산된 추천값을 바로 적용하지 않고 과거 일정 구간의 추천값들을 모아 그중 하나를 고른다.

  • scaleDown 기본 300초(5분): 지난 5분간의 추천값 중 가장 큰 값을 골라 적용한다. 트래픽이 잠깐 꺼져도 5분 안에 한 번이라도 높았으면 줄이지 않는다 → 축소·재확장이 반복되는 사이클 방지.
  • scaleUp 기본 0초: 윈도우가 0이므로 업스케일 추천은 즉시 반영된다.

방향별 선택 규칙도 같은 철학이다. 다운은 윈도우 내 max 추천, 업은 윈도우 내 min 추천을 쓴다. 양쪽 모두 "성급한 변화"를 누르는 쪽으로 정렬돼 있다.

마지막으로 behavior 정책(KEP-853) 이 한 주기당 변화 폭 자체를 클램프한다. 기본값은 대략 이렇다.

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    selectPolicy: Max          # 여러 정책 중 가장 큰 변화 허용
    policies:
    - type: Percent
      value: 100               # 15초마다 최대 100% 증가(2배)
      periodSeconds: 15
    - type: Pods
      value: 4                 # 또는 15초마다 최대 +4 파드
      periodSeconds: 15
  scaleDown:
    stabilizationWindowSeconds: 300
    selectPolicy: Max
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15

selectPolicy: Max는 여러 정책이 허용하는 변화량 중 가장 큰 것을 택한다(업이라면 Percent 100%와 Pods 4 중 더 큰 쪽). Disabled로 두면 그 방향의 스케일링을 아예 끈다. 정리하면 결측·미준비 가정(업 0% / 다운 100%), 안정화 윈도우(업 0초 / 다운 300초), 윈도우 내 선택(업 min / 다운 max)이 전부 "빠르게 늘리고 천천히 줄인다" 는 한 방향으로 맞춰져 있다.

5. 한 사이클을 손으로 따라가기

HPA 컨트롤러는 이벤트 기반 연속 감시가 아니라 kube-controller-manager--horizontal-pod-autoscaler-sync-period(기본 15초)마다 깨어나 도는 폴링 루프다. 한 틱의 흐름과 손계산은 이렇다.

        ┌────────────── 15s sync tick ──────────────┐
        ▼                                            │
  메트릭 집계 → ratio = cur/target                    │
   |ratio-1| ≤ 0.1 ? ──yes──► no-op ─────────────────┤
        │ no                                         │
  desired = ceil(replicas × ratio)                   │
  (not-ready/결측 보정: 업=0%, 다운=100%)             │
  안정화 윈도우(업 min / 다운 max) → behavior 클램프    │
  scale 서브리소스.replicas = desired ────────────────┘
# 시나리오 A — 업스케일: replicas=3, 목표 CPU=50%, Ready 평균=90%
ratio   = 90 / 50 = 1.8          # |1.8 - 1| = 0.8 > 0.1 → 스케일 대상
desired = ceil(3 × 1.8) = 6      # 3 → 6, 한 번에 2배
# scaleUp Percent 100%/15s 이내(+100%)라 그대로 허용 → 즉시 반영

# 시나리오 B — 같은 디플로이, 평균이 20%로 급락
ratio   = 20 / 50 = 0.4
desired = ceil(3 × 0.4) = 2      # 2가 "추천값"
# 하지만 scaleDown 안정화 300초: 지난 5분 추천 중 max(직전 6 등)를 채택
#  → 즉시 2로 줄지 않고 윈도우가 지나야 단계적으로 내려감

실제 클러스터라면 kubectl describe hpa <name>의 이벤트에서 New size: N; reason: cpu resource utilization (percentage of request) above target 같은 줄로 동일한 계산 결과를 직접 확인할 수 있다. 참고로 metrics:에 CPU·메모리·custom을 여러 개 두면 각각으로 desiredReplicas를 구한 뒤 가장 큰 값을 채택한다 — 어느 한 자원이라도 부족하면 늘리겠다는 보수적 합집합이다.

6. 정리

HPA는 임계치 기반 증분 제어가 아니라 ceil(replicas × ratio)로 목표 비율에 수렴시키는 비례 제어기이며, 그 위에 비대칭 보정을 겹쳐 "빠르게 늘리고 천천히 줄인다".

실무에서 기억할 한 가지는 15초 폴링이라는 한계다. 스파이크가 15초 안에 끝나면 HPA는 그걸 못 볼 수도 있다. 짧고 날카로운 버스트는 HPA만으로 흡수되지 않으며, 이것이 KEDA(이벤트 기반 스케일링)·VPA·request 기반 오버프로비저닝이 함께 필요한 이유다.

더 파고든다면 두 갈래가 있다. 하나는 VPA recommender가 메트릭 히스토리(decaying histogram)로 request를 추정하는 방식과 HPA 동시 사용 시의 충돌 지점. 다른 하나는 custom.metrics.k8s.io / external.metrics.k8s.io 어댑터(Prometheus Adapter, KEDA)가 HPA에 메트릭을 공급하는 파이프라인이다.

참고 자료

0개의 댓글