쿠버네티스 HPA/VPA 오토스케일링 실전 튜닝
쿠버네티스에서 오토스케일링은 “켜두면 알아서 잘 되는” 기능이 아닙니다. 기본값으로 HPA를 붙여놓고 트래픽 급증을 맞으면, 파드가 늦게 뜨거나 우르르 떴다가 다시 죽는 플래핑(flapping)을 겪게 됩니다. 반대로 리소스를 넉넉히 잡아두면 노드 비용이 새어나갑니다. 오토스케일링은 결국 비용과 안정성 사이의 튜닝 문제입니다.
이 글에서는 수평 확장을 담당하는 HPA와 수직 조정을 담당하는 VPA를 실전 관점에서 다룹니다. 둘의 충돌을 피하는 법, 커스텀 메트릭으로 확장하는 법, 그리고 노드 오토스케일러와 함께 엮는 법까지 정리합니다.
HPA의 동작 원리부터 정확히
HPA는 목표 사용률과 현재 사용률의 비율로 원하는 레플리카 수를 계산합니다. 공식은 단순합니다. desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric). 즉 CPU 목표를 50%로 잡았는데 현재 90%라면, 파드를 약 1.8배로 늘립니다. 이 계산이 15초마다 반복됩니다.
여기서 흔한 실수는 requests 값을 설정하지 않는 것입니다. HPA의 CPU 사용률은 실제 사용량을 requests로 나눈 비율이므로, requests가 없으면 HPA는 아예 동작하지 못합니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
여러 메트릭을 걸면 HPA는 각각으로 계산한 레플리카 수 중 가장 큰 값을 택합니다. 안전한 쪽으로 판단하는 것이죠.
플래핑을 잡는 stabilization 튜닝
기본 설정으로 HPA를 돌리면 트래픽이 출렁일 때마다 파드가 늘었다 줄었다를 반복합니다. 이 플래핑은 애플리케이션 워밍업 비용과 커넥션 끊김을 유발합니다. v2 API의 behavior 필드로 확장·축소 속도를 비대칭으로 제어하는 것이 핵심 기법입니다.
behavior:
scaleUp:
# 확장은 빠르게 - 트래픽 급증에 즉시 대응
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
scaleDown:
# 축소는 천천히 - 5분간 관찰 후 줄임
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
원칙은 확장은 공격적으로, 축소는 보수적으로입니다. 확장이 늦으면 장애로 이어지지만, 축소가 조금 늦는 건 약간의 비용일 뿐입니다. 축소 stabilization을 300초로 두면 순간적 트래픽 감소에 파드를 성급하게 죽이지 않습니다.
커스텀 메트릭과 외부 메트릭
CPU와 메모리만으로는 부족한 서비스가 많습니다. 큐 소비자라면 큐에 쌓인 메시지 수가, API 게이트웨이라면 초당 요청 수가 진짜 부하 지표입니다. Prometheus Adapter나 KEDA를 쓰면 이런 메트릭으로 스케일할 수 있습니다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaler
spec:
scaleTargetRef:
name: queue-worker
minReplicaCount: 1
maxReplicaCount: 50
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
metricName: queue_depth
threshold: "100"
query: sum(rabbitmq_queue_messages{queue="jobs"})
KEDA의 강력한 점은 minReplicaCount를 0으로 둘 수 있다는 것입니다. 큐가 비면 워커를 완전히 0으로 내려 비용을 아끼고, 메시지가 들어오면 다시 깨웁니다. 배치성 워크로드에 특히 유용합니다.
VPA로 requests를 자동 보정하기
HPA가 “몇 개”를 다룬다면 VPA는 “얼마나 큰”을 다룹니다. VPA는 파드의 실제 사용량을 관찰해 requests/limits 권장값을 계산합니다. requests를 감으로 잡던 관행을 데이터 기반으로 바꿔줍니다. 다만 프로덕션에서는 updateMode: "Off"로 두고 권장값만 참고하는 편이 안전합니다. Auto 모드는 파드를 재생성하기 때문입니다.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off" # 권장값만 산출, 자동 적용 안 함
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: 2
memory: 2Gi
# 권장값 확인
kubectl describe vpa api-vpa | grep -A8 "Recommendation"
HPA와 VPA를 함께 쓸 때의 충돌
가장 흔한 함정입니다. HPA와 VPA가 같은 메트릭(CPU/메모리)을 동시에 보면 서로 간섭합니다. VPA가 requests를 올리면 HPA가 계산하는 사용률이 낮아지고, 그러면 HPA가 파드를 줄이고, 그럼 다시 부하가 몰리는 악순환이 생깁니다.
- HPA는 CPU/메모리로, VPA는
updateMode: "Off"로 권장만. - 또는 HPA는 커스텀 메트릭(큐, RPS)으로, VPA는 CPU/메모리 requests 조정으로 역할을 분리한다.
- 같은 리소스 축을 두 컨트롤러가 능동 제어하게 두지 않는다.
노드 오토스케일러와 파드 스케줄링
파드를 늘려도 노드에 자리가 없으면 Pending에 멈춥니다. Cluster Autoscaler나 Karpenter가 노드를 추가로 띄워줘야 합니다. 여기서 중요한 건 PodDisruptionBudget입니다. 노드 스케일다운이나 롤아웃 중에도 최소 가용 파드 수를 보장해 서비스 중단을 막습니다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: api
또한 파드가 빠르게 뜨려면 노드 프로비저닝 시간이 병목이 됩니다. Karpenter는 노드 그룹 대신 필요한 스펙의 노드를 즉시 프로비저닝해 이 지연을 줄여줍니다. 확장 지연이 문제라면 과잉 프로비저닝 파드(pause pod)를 미리 띄워 여유 노드를 확보하는 기법도 있습니다.
마무리
오토스케일링은 선언 한 줄로 끝나는 기능이 아니라 지속적으로 관측하고 튜닝하는 대상입니다. HPA의 behavior로 플래핑을 잡고, 커스텀 메트릭으로 진짜 부하를 반영하며, VPA는 권장값 참고용으로 안전하게 쓰고, 노드 오토스케일러와 PDB로 스케일 이벤트 중 안정성을 지키는 것. 이 조합이 갖춰지면 트래픽이 요동쳐도 서비스는 흔들리지 않고, 청구서는 실제 부하를 따라 정직하게 움직입니다. 완벽한 설정값은 없으니, 실제 트래픽 패턴을 보며 반복적으로 다듬어 나가세요.