IT/Kubernets

HPA 진동 잡느라 이틀 태운 이야기

gfrog 2026. 8. 26. 15:14

지난주 화요일 새벽 2시쯤이었나. 알림이 울렸다. 특정 서비스의 P99 레이턴시가 800ms를 넘긴다는 알람이었다. 노드 16대짜리 프로덕션 클러스터에서, 특정 결제 API 파드가 30초 간격으로 8개에서 22개, 다시 10개, 또 25개... 이런 식으로 계속 오르락내리락 하고 있었다.

솔직히 처음엔 트래픽 스파이크인 줄 알았다. 근데 그라파나 대시보드를 열어보니 그건 아니었다. RPS는 완만하게 오르는 중이었다. 그런데 파드 수가 미친 듯이 진동하고 있었다. 이게 바로 HPA flapping이다. 이론으로만 알던 걸 새벽 2시에 만나게 되니 정말 반가웠다(반갑지 않았다).

처음엔 임계값을 만졌다

가장 먼저 시도한 건 CPU targetAverageUtilization을 60에서 75로 올린 것이었다. "임계값이 낮아서 자꾸 스케일업 되는 거겠지"라는 순진한 생각. 결과는? 그대로였다. 여전히 진동했고, 오히려 스케일업이 늦어져서 몇몇 요청이 타임아웃을 맞았다.

두 번째 시도는 HPA의 minReplicas를 15로 올리는 거였다. 이건 사실 근본 해결이 아니라 그냥 낭비다. 새벽 4시엔 트래픽이 없는데 15개를 유지하고 있어야 한다는 얘기니까. 팀 슬랙에 "일단 진동은 멈췄는데 이건 아닌 것 같다"고 남기고 잠깐 눈을 붙였다.

다음날, behavior 필드를 다시 봤다

Kubernetes HPA v2에는 behavior 필드가 있다. 사실 이거 알고는 있었는데 프로덕션에서 제대로 튜닝해본 적은 없었다. 기본값을 그냥 믿고 썼던 거다. 근데 기본값이 우리 워크로드에는 안 맞았다.

기본 동작을 다시 정리하자면:

  • scaleDown stabilizationWindowSeconds: 300초 (5분). 스케일다운 결정을 위해 지난 5분간의 최고 추천 replica 수를 사용한다.
  • scaleUp stabilizationWindowSeconds: 0초. 즉시 스케일업.
  • scaleUp policies: 15초마다 최대 4개 파드 또는 100% 증가 중 큰 값.

여기서 우리 문제의 원인이 있었다. 우리 서비스는 파드 하나가 뜨는데 이미지 pull + JVM 워밍업 포함해서 대략 90초 정도 걸렸다. 그런데 scaleUp이 15초마다 100% 증가로 세팅되어 있으니, HPA 관점에서는 "아직 파드가 워밍업 중이라 CPU가 안 잡혀서 여전히 부족해 보인다" → 또 4개 더 띄운다 → 이미 뜨던 파드가 워밍업 완료되면 CPU가 확 떨어진다 → 초과 프로비저닝이 감지되어 스케일다운 대기.

이걸 짧게 요약하면, 파드가 워밍업 되기 전에 다음 스케일업 결정이 내려지고 있었다.

수정한 설정

결국 아래처럼 바꿨다. 특별한 마법은 없다. 우리 워크로드의 실제 워밍업 시간을 반영했을 뿐이다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-api
  minReplicas: 6
  maxReplicas: 40
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
        - type: Pods
          value: 4
          periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
        - type: Percent
          value: 20
          periodSeconds: 120
      selectPolicy: Max

핵심 변경점 세 개:

  1. scaleUp.stabilizationWindowSeconds: 60 — 워밍업 시간에 맞춰 스케일업 결정을 60초간 관찰 후 내리게 했다. 최근에 본 oneuptime 글에서도 컨테이너 시작 시간을 기준으로 이 값을 잡으라고 강조하더라.
  2. scaleUp.policiesperiodSeconds를 60으로 늘렸다. 15초는 우리 서비스에겐 너무 짧았다.
  3. scaleDown.stabilizationWindowSeconds: 600 — 스케일다운은 10분간 관찰. 트래픽이 진짜 안정적으로 낮아졌는지 확실히 보고 내리게.

결과 확인

배포하고 나서 트래픽 부하 테스트를 다시 돌렸다. k6로 30분간 램프업. 파드 수는 10 → 14 → 18 → 22 로 완만하게 올라가서 유지되었고, 부하가 빠질 때는 22 → 20 → 17 로 10분 간격으로 부드럽게 내려왔다. P99도 300ms 이하로 안정.

Prometheus로 확인한 그래프에서 kube_horizontalpodautoscaler_status_desired_replicas 지표가 예전엔 톱니 모양이었는데, 지금은 계단식으로 부드럽게 움직인다. 이 지표 대시보드에 항상 띄워둘 만하다.

배운 것들

첫째, HPA behavior의 기본값은 "짧게 자주 시작하는" 워크로드용이다. JVM처럼 워밍업 시간이 긴 앱이라면 기본값을 그대로 쓰면 안 된다. 컨테이너 시작 시간을 먼저 측정하고, 그 값을 스케일업 window에 반영해야 한다.

둘째, 임계값을 만지기 전에 behavior를 먼저 봐야 한다. targetAverageUtilization을 올리는 건 대증치료다. 진동의 원인은 대개 임계값이 아니라 스케일링 속도다.

셋째, minReplicas를 올려서 진동을 숨기지 말자. 그건 문제를 가리는 거지 해결하는 게 아니다. 새벽에 15개 파드 붙어있는 거 나중에 코스트 리포트에서 만나면 후회한다.

여담

사실 KEDA로 event-driven scaling으로 바꾸는 걸 고민 중이긴 하다. CPU 기반 스케일링은 결국 후행 지표라서, RabbitMQ queue depth 같은 선행 지표로 스케일링하면 이런 워밍업 지연 문제가 훨씬 완화된다. 다음 스프린트에 파일럿으로 한번 붙여볼까 한다. 근데 그건 그거대로 또 삽질할 거리가 있을 것 같다.

혹시 HPA 진동 다른 방식으로 잡으신 분 계시면 방법 공유 부탁드립니다.