Skip to content
Published on

Kubernetes HPA·VPA·KEDA 오토스케일링 전략과 실전 튜닝 가이드

Authors
  • Name
    Twitter
Kubernetes HPA VPA KEDA Autoscaling

들어가며

Kubernetes 워크로드를 운영하다 보면 가장 까다로운 문제 중 하나가 리소스 스케일링입니다. 트래픽이 급증하면 Pod가 부족해 장애가 발생하고, 트래픽이 줄면 과잉 프로비저닝으로 비용이 낭비됩니다. Kubernetes는 이 문제를 해결하기 위해 세 가지 핵심 오토스케일러를 제공합니다.

  • HPA (Horizontal Pod Autoscaler): Pod 수를 수평으로 조절
  • VPA (Vertical Pod Autoscaler): 개별 Pod의 CPU/메모리 요청량을 수직으로 조절
  • KEDA (Kubernetes Event-Driven Autoscaling): 외부 이벤트 소스 기반의 확장된 오토스케일링

이 글에서는 각 오토스케일러의 동작 원리, 설정 방법, 그리고 실전에서 겪는 트러블슈팅 사례까지 종합적으로 다룹니다.

HPA v2 동작 원리와 알고리즘

스케일링 알고리즘

HPA는 autoscaling/v2 API를 사용하며, 다음 공식으로 원하는 레플리카 수를 계산합니다:

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

예를 들어 현재 4개의 Pod가 평균 CPU 사용률 80%이고 목표가 50%라면, ceil(4 × (80/50)) = ceil(6.4) = 7개의 Pod로 스케일 아웃됩니다.

HPA 컨트롤러는 기본적으로 15초 주기로 메트릭을 수집하며, --horizontal-pod-autoscaler-sync-period 플래그로 조절할 수 있습니다.

기본 HPA 설정 - CPU/메모리 기반

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
        - type: Pods
          value: 4
          periodSeconds: 15
      selectPolicy: Max

이 설정에서 핵심은 behavior 블록입니다. 스케일 업은 즉시(stabilizationWindowSeconds: 0) 수행되지만, 스케일 다운은 5분 동안 안정화 기간을 거치며 60초마다 최대 10%씩만 줄입니다. 이를 통해 급격한 축소로 인한 서비스 영향을 방지합니다.

커스텀 메트릭 기반 HPA

실제 운영에서는 CPU/메모리보다 애플리케이션 메트릭(RPS, 큐 깊이, 활성 커넥션 등)이 더 정확한 스케일링 지표입니다. Prometheus Adapter를 활용한 커스텀 메트릭 HPA를 설정해봅니다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 30
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: '100'
    - type: Object
      object:
        metric:
          name: rabbitmq_queue_messages
        describedObject:
          apiVersion: v1
          kind: Service
          name: rabbitmq
        target:
          type: Value
          value: '500'

이 설정은 두 가지 커스텀 메트릭을 사용합니다. Pod당 초당 HTTP 요청이 100을 넘거나, RabbitMQ 큐에 대기 중인 메시지가 500개를 넘으면 스케일 아웃합니다. 복수 메트릭이 설정되면 HPA는 각 메트릭별 원하는 레플리카 수를 계산한 뒤 최대값을 선택합니다.

Prometheus Adapter 설정은 다음과 같습니다:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-adapter-config
  namespace: monitoring
data:
  config.yaml: |
    rules:
      - seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
        resources:
          overrides:
            namespace: {resource: "namespace"}
            pod: {resource: "pod"}
        name:
          matches: "^(.*)_total$"
          as: "${1}_per_second"
        metricsQuery: 'rate(<<.Series>>{<<.LabelMatchers>>}[2m])'
      - seriesQuery: 'rabbitmq_queue_messages{namespace!=""}'
        resources:
          overrides:
            namespace: {resource: "namespace"}
        name:
          matches: "^(.*)$"
          as: "$1"
        metricsQuery: '<<.Series>>{<<.LabelMatchers>>}'

VPA 모드별 차이와 제한사항

VPA 아키텍처

VPA는 세 가지 컴포넌트로 구성됩니다:

  • Recommender: 현재 및 과거 리소스 사용량을 분석하여 최적 요청량 추천
  • Updater: 잘못된 리소스 요청을 가진 Pod를 축출(Evict)
  • Admission Controller: 새로 생성되는 Pod에 올바른 리소스 요청 주입

VPA 모드

모드동작Pod 재시작사용 시나리오
Off추천만 제공, 자동 적용 없음없음리소스 분석, 초기 도입 단계
InitialPod 생성 시에만 추천값 적용없음 (신규 Pod만)안정성 우선 워크로드
Auto추천값을 기존 Pod에도 적용 (재시작)있음비상태 워크로드, 개발 환경
RecreateAuto와 동일하나 재시작 보장있음명시적 재시작 필요 시

Kubernetes 1.32부터 InPlaceOrRecreate 모드가 추가되어, 가능한 경우 Pod를 재시작하지 않고 리소스를 변경합니다. 이 기능은 InPlacePodVerticalScaling 피처 게이트가 활성화되어야 합니다.

VPA 설정 예시

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: payment-service-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-service
  updatePolicy:
    updateMode: 'Initial'
    minReplicas: 2
  resourcePolicy:
    containerPolicies:
      - containerName: payment-app
        minAllowed:
          cpu: '100m'
          memory: '128Mi'
        maxAllowed:
          cpu: '4'
          memory: '8Gi'
        controlledResources: ['cpu', 'memory']
        controlledValues: RequestsOnly
      - containerName: sidecar-proxy
        mode: 'Off'

이 설정에서 주목할 점은 다음과 같습니다:

  • updateMode: Initial로 설정하여 기존 Pod를 재시작하지 않고 새 Pod에만 적용
  • minReplicas: 2로 최소 2개 Pod가 항상 실행 중이도록 보장
  • 사이드카 컨테이너(sidecar-proxy)는 VPA 대상에서 제외 (mode: Off)
  • controlledValues: RequestsOnly로 limit은 건드리지 않고 request만 조절

VPA 핵심 제한사항

  1. HPA와의 동시 사용 제한: VPA는 동일한 메트릭(CPU/메모리)으로 HPA와 함께 사용하면 충돌이 발생합니다. HPA가 CPU 사용률 기반으로 Pod 수를 늘리는 동시에 VPA가 CPU 요청량을 늘리면 예측 불가능한 동작이 됩니다.
  2. 최대 1,000 Pod 제한: VPA가 관리하는 Pod 수는 클러스터당 1,000개를 초과하지 않는 것이 권장됩니다.
  3. CronJob/Job 미지원: VPA는 장기 실행 워크로드(Deployment, StatefulSet)에서만 동작합니다.
  4. JVM 워크로드: JVM의 힙 메모리는 시작 시 고정되므로 VPA가 메모리를 줄여도 실제 JVM이 사용하는 메모리는 줄어들지 않습니다.

KEDA: 이벤트 드리븐 오토스케일링

KEDA 아키텍처

KEDA는 HPA를 대체하는 것이 아니라 확장합니다. KEDA는 외부 이벤트 소스(Kafka, Prometheus, Redis, AWS SQS 등)의 메트릭을 가져와 HPA에 주입하는 역할을 합니다.

핵심 CRD는 다음과 같습니다:

  • ScaledObject: Deployment/StatefulSet에 대한 스케일링 규칙 정의
  • ScaledJob: Job 기반 워크로드에 대한 스케일링 규칙 정의
  • TriggerAuthentication: 외부 시스템 인증 정보 관리
  • ClusterTriggerAuthentication: 클러스터 범위 인증 정보

Kafka 기반 KEDA 스케일링

apiVersion: v1
kind: Secret
metadata:
  name: kafka-credentials
  namespace: production
type: Opaque
data:
  sasl_username: dXNlcm5hbWU=
  sasl_password: cGFzc3dvcmQ=
  ca: LS0tLS1CRUdJTi4uLg==
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: kafka-trigger-auth
  namespace: production
spec:
  secretTargetRef:
    - parameter: sasl
      name: kafka-credentials
      key: sasl_username
    - parameter: password
      name: kafka-credentials
      key: sasl_password
    - parameter: ca
      name: kafka-credentials
      key: ca
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-consumer-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: order-consumer
  pollingInterval: 15
  cooldownPeriod: 300
  idleReplicaCount: 0
  minReplicaCount: 1
  maxReplicaCount: 50
  fallback:
    failureThreshold: 3
    replicas: 5
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: 'kafka-0.kafka:9092,kafka-1.kafka:9092,kafka-2.kafka:9092'
        consumerGroup: order-consumer-group
        topic: orders
        lagThreshold: '100'
        activationLagThreshold: '10'
        offsetResetPolicy: latest
      authenticationRef:
        name: kafka-trigger-auth

이 설정의 핵심 파라미터를 분석합니다:

  • idleReplicaCount: 0 — 이벤트가 없으면 Pod를 0으로 줄여 비용 절감 (제로 스케일링)
  • activationLagThreshold: 10 — 컨슈머 랙이 10 미만이면 활성화하지 않음
  • lagThreshold: 100 — 컨슈머 랙 100당 1개의 Pod 추가
  • fallback — KEDA가 메트릭을 가져오지 못하면(3회 실패 시) 5개의 레플리카로 폴백

Prometheus 기반 KEDA 스케일링

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: web-frontend-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: web-frontend
  pollingInterval: 30
  cooldownPeriod: 120
  minReplicaCount: 2
  maxReplicaCount: 100
  advanced:
    restoreToOriginalReplicaCount: true
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 300
          policies:
            - type: Percent
              value: 25
              periodSeconds: 60
  triggers:
    - type: prometheus
      metadata:
        serverAddress: 'http://prometheus.monitoring.svc:9090'
        query: |
          sum(rate(nginx_ingress_controller_requests{
            namespace="production",
            service="web-frontend"
          }[2m]))
        threshold: '500'
        activationThreshold: '50'
    - type: prometheus
      metadata:
        serverAddress: 'http://prometheus.monitoring.svc:9090'
        query: |
          histogram_quantile(0.95,
            sum(rate(http_request_duration_seconds_bucket{
              namespace="production",
              service="web-frontend"
            }[5m])) by (le))
        threshold: '0.5'
        activationThreshold: '0.1'

이 설정은 두 가지 Prometheus 쿼리를 사용합니다. 초당 요청 수가 500을 넘거나 P95 레이턴시가 500ms를 넘으면 스케일 아웃합니다. advanced.horizontalPodAutoscalerConfig를 통해 내부적으로 생성되는 HPA의 behavior도 제어할 수 있습니다.

HPA vs VPA vs KEDA 비교

항목HPAVPAKEDA
스케일링 방향수평 (Pod 수)수직 (리소스 크기)수평 (Pod 수) + 제로 스케일
기본 메트릭CPU, 메모리CPU, 메모리60+ 외부 이벤트 소스
커스텀 메트릭Adapter 필요미지원네이티브 지원
제로 스케일링불가 (minReplicas >= 1)해당 없음가능 (idleReplicaCount: 0)
Pod 재시작없음있음 (Auto/Recreate)없음
설정 복잡도낮음중간중간~높음
Kubernetes 내장별도 설치별도 설치
CronJob 지원제한적미지원ScaledJob으로 지원
상태 저장 워크로드주의 필요지원주의 필요
커뮤니티/생태계매우 활발활발CNCF Graduated, 매우 활발

혼합 전략: HPA + VPA + KEDA

HPA + VPA 혼합

HPA와 VPA를 함께 사용할 때는 메트릭 충돌을 반드시 피해야 합니다. 권장 패턴은 다음과 같습니다:

  • HPA: 커스텀 메트릭(RPS, 큐 깊이 등) 기반 수평 스케일링
  • VPA: Off 모드로 리소스 추천만 수행하거나, CPU/메모리 기반 수직 조절
# HPA: 커스텀 메트릭만 사용
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: '200'
---
# VPA: CPU/메모리 리소스 최적화
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: 'Auto'
  resourcePolicy:
    containerPolicies:
      - containerName: api
        controlledResources: ['cpu', 'memory']
        controlledValues: RequestsOnly

HPA + KEDA 혼합

KEDA는 내부적으로 HPA를 생성하므로 동일 Deployment에 별도의 HPA를 만들면 충돌합니다. 대신 KEDA의 ScaledObject에 여러 trigger를 추가하는 방식으로 결합합니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: hybrid-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: worker-service
  minReplicaCount: 2
  maxReplicaCount: 100
  triggers:
    # CPU 기반 (HPA 역할 대체)
    - type: cpu
      metricType: Utilization
      metadata:
        value: '70'
    # Kafka 이벤트 기반
    - type: kafka
      metadata:
        bootstrapServers: 'kafka.default:9092'
        consumerGroup: worker-group
        topic: tasks
        lagThreshold: '50'
    # Cron 기반 예약 스케일링
    - type: cron
      metadata:
        timezone: Asia/Seoul
        start: '0 9 * * 1-5'
        end: '0 18 * * 1-5'
        desiredReplicas: '10'

이 설정은 세 가지 전략을 결합합니다: 평소에는 CPU 기반, Kafka 메시지가 밀리면 이벤트 기반, 평일 업무 시간(09:00~18:00)에는 Cron 기반으로 최소 10개를 유지합니다. KEDA는 모든 트리거 중 최대값을 사용하므로 안전하게 결합할 수 있습니다.

트러블슈팅: 실패 사례와 복구

사례 1: HPA가 스케일 아웃하지 않음

증상: CPU 사용률이 90%인데 HPA가 반응하지 않음

# HPA 상태 확인
kubectl get hpa api-server-hpa -n production -o yaml

# 이벤트 확인
kubectl describe hpa api-server-hpa -n production

# metrics-server 동작 확인
kubectl top pods -n production
kubectl get apiservices | grep metrics

원인과 해결:

  1. metrics-server 미설치: kubectl get deployment metrics-server -n kube-system으로 확인
  2. 리소스 요청 미설정: Pod에 resources.requests가 없으면 Utilization 계산 불가. 반드시 request 설정 필요
  3. maxReplicas 도달: kubectl get hpa에서 MAXPODS에 도달했는지 확인
  4. 알 수 없는 메트릭: kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" 으로 커스텀 메트릭 API 확인

사례 2: VPA 추천값이 비정상적으로 높음

증상: VPA가 메모리 요청을 32Gi로 추천하는데 실제 사용량은 2Gi

원인: JVM 워크로드에서 -Xmx를 메모리 limit에 가깝게 설정한 경우, JVM이 최대 힙을 할당하므로 VPA가 높은 값을 추천합니다.

해결: maxAllowed를 적절하게 설정하고, JVM 워크로드는 VPA에서 메모리를 제외합니다.

resourcePolicy:
  containerPolicies:
    - containerName: java-app
      controlledResources: ['cpu'] # 메모리는 VPA에서 제외
      maxAllowed:
        cpu: '4'

사례 3: KEDA가 0으로 스케일 인 후 복구가 느림

증상: Kafka 컨슈머가 0으로 스케일 인 후 메시지가 들어와도 Pod 시작까지 2~3분 소요

해결방안:

  • activationLagThreshold를 낮게 설정하여 더 빨리 활성화
  • minReplicaCount: 1로 설정하여 최소 1개 Pod 유지
  • Pod에 readinessProbe 시간을 줄여 빠르게 서비스 등록
  • 컨테이너 이미지 사전 풀(pre-pull) 전략 사용

사례 4: HPA Flapping (빈번한 스케일 업/다운)

증상: Pod 수가 지속적으로 증가/감소를 반복

# HPA 이벤트 히스토리 확인
kubectl get events --field-selector involvedObject.name=api-hpa -n production --sort-by='.lastTimestamp'

해결:

behavior:
  scaleDown:
    stabilizationWindowSeconds: 600 # 10분 안정화
    policies:
      - type: Pods
        value: 1
        periodSeconds: 300 # 5분에 1개씩만 축소
  scaleUp:
    stabilizationWindowSeconds: 60
    policies:
      - type: Percent
        value: 50
        periodSeconds: 60

운영 체크리스트

운영 환경에서 오토스케일링을 적용할 때 반드시 확인해야 할 항목들입니다:

배포 전 체크리스트

  • 모든 Pod에 resources.requestsresources.limits가 설정되어 있는가
  • PodDisruptionBudget(PDB)이 설정되어 있는가 (VPA 사용 시 필수)
  • metrics-server 또는 Prometheus Adapter가 정상 동작하는가
  • HPA와 VPA가 동일 메트릭으로 충돌하지 않는가
  • maxReplicas가 클러스터 노드 오토스케일러의 최대 노드 수와 맞는가
  • KEDA 사용 시 fallback 설정이 되어 있는가

모니터링 체크리스트

  • HPA 현재 레플리카 수와 목표 레플리카 수 대시보드
  • VPA 추천값 vs 실제 사용량 추이
  • KEDA 트리거 메트릭 값과 활성화 상태
  • 스케일링 이벤트 알림 (Slack/PagerDuty 연동)
  • 노드 오토스케일러와의 연동 상태 (Pending Pod 모니터링)

비용 최적화 체크리스트

  • 개발/스테이징 환경은 KEDA로 제로 스케일링 적용
  • 업무 시간 외 Cron 트리거로 최소 레플리카 축소
  • VPA Off 모드로 추천값을 수집하고 주기적으로 request 업데이트
  • Spot/Preemptible 인스턴스와 오토스케일링 결합

마치며

Kubernetes 오토스케일링은 단일 도구로 해결되지 않습니다. 워크로드 특성에 따라 HPA, VPA, KEDA를 적절히 조합해야 합니다. CPU/메모리 기반의 단순한 스케일링에는 HPA v2로 충분하고, 리소스 최적화가 필요하면 VPA를 병행하며, 이벤트 드리븐 워크로드나 제로 스케일링이 필요하면 KEDA가 적합합니다.

특히 운영 환경에서는 behavior 설정을 통한 스케일링 속도 제어, PDB와의 연동, 그리고 fallback 전략이 중요합니다. 이 글에서 다룬 설정 예시와 트러블슈팅 사례를 참고하여 안정적이면서도 비용 효율적인 오토스케일링 전략을 구축하시기 바랍니다.

참고자료

  1. Kubernetes 공식 문서 - Horizontal Pod Autoscaling
  2. Kubernetes 공식 문서 - Vertical Pod Autoscaling
  3. KEDA 공식 문서 - ScaledObject Specification
  4. KEDA 공식 문서 - Authentication
  5. Kubernetes Autoscaler GitHub - VPA
  6. KEDA Apache Kafka Scaler
  7. Kubernetes HPA Walkthrough