들어가며
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** | 추천만 제공, 자동 적용 없음 | 없음 | 리소스 분석, 초기 도입 단계 |
| **Initial** | Pod 생성 시에만 추천값 적용 | 없음 (신규 Pod만) | 안정성 우선 워크로드 |
| **Auto** | 추천값을 기존 Pod에도 적용 (재시작) | 있음 | 비상태 워크로드, 개발 환경 |
| **Recreate** | Auto와 동일하나 재시작 보장 | 있음 | 명시적 재시작 필요 시 |
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 비교
| 항목 | HPA | VPA | KEDA |
| ---------------------- | -------------------------- | -------------------- | --------------------------- |
| **스케일링 방향** | 수평 (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.requests`와 `resources.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](https://kubernetes.io/docs/concepts/workloads/autoscaling/horizontal-pod-autoscale/)
2. [Kubernetes 공식 문서 - Vertical Pod Autoscaling](https://kubernetes.io/docs/concepts/workloads/autoscaling/vertical-pod-autoscale/)
3. [KEDA 공식 문서 - ScaledObject Specification](https://keda.sh/docs/2.18/concepts/scaling-deployments/)
4. [KEDA 공식 문서 - Authentication](https://keda.sh/docs/2.18/concepts/authentication/)
5. [Kubernetes Autoscaler GitHub - VPA](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler)
6. [KEDA Apache Kafka Scaler](https://keda.sh/docs/2.19/scalers/apache-kafka-go/)
7. [Kubernetes HPA Walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/)
현재 단락 (1/411)
Kubernetes 워크로드를 운영하다 보면 가장 까다로운 문제 중 하나가 **리소스 스케일링**입니다. 트래픽이 급증하면 Pod가 부족해 장애가 발생하고, 트래픽이 줄면 과잉 프...