[CS300 #238] 오토스케일링 — HPA 공식, 안정화 창, 노드까지 늘리기
컴퓨터공학 300 주제 시리즈의 238번째 글이다. 전체 지도는 여기.
한 줄 요약
오토스케일링은 부하 지표를 보고 용량을 자동으로 조절하는 제어 루프다. 쿠버네티스에서는 파드 수(HPA), 파드 크기(VPA), 노드 수(Cluster Autoscaler)라는 세 축이 있고, 서로 맞물려야 제대로 동작한다.
왜 필요한가
트래픽은 일정하지 않다. 점심시간, 월말, 이벤트 시작 직후에 몰리고 새벽에는 거의 없다. 최대치에 맞춰 서버를 고정해 두면 대부분의 시간에 돈을 버리고, 평균에 맞추면 몰릴 때 넘친다. 사람이 그래프를 보고 손으로 늘리는 것은 늦고, 밤에는 아무도 없다.
오토스케일링은 이 일을 자동화한다. 다만 자동화된 제어 루프는 잘못 설정하면 오히려 진동하거나(늘렸다 줄였다 반복), 늦게 반응하거나, 비용을 폭주시킨다. 동작 원리를 알아야 안전하게 쓴다.
핵심 개념
세 가지 축
| 축 | 쿠버네티스 구성 요소 | 조절 대상 |
|---|---|---|
| 수평(horizontal) | HorizontalPodAutoscaler (HPA) | 파드 개수 |
| 수직(vertical) | VerticalPodAutoscaler (VPA, 별도 설치) | 파드의 CPU·메모리 requests/limits |
| 클러스터 | Cluster Autoscaler, Karpenter 등 | 노드 개수 |
HPA 가 파드를 늘렸는데 들어갈 노드가 없으면 파드는 Pending 이 된다. Cluster Autoscaler 는 바로 이 “자원 부족으로 스케줄되지 못한 파드”를 보고 노드를 추가한다. 반대로 노드의 파드들이 다른 노드로 옮겨 갈 수 있을 만큼 한가하면 노드를 줄인다. 둘은 이렇게 연결된다.
HPA 의 공식
HPA 컨트롤러는 주기적으로(기본 15초) 지표를 읽고 다음 식으로 원하는 레플리카 수를 계산한다.
desiredReplicas = ceil( currentReplicas × currentMetricValue / desiredMetricValue )
예: 파드 4개, 목표 CPU 사용률 50%, 현재 평균 80% → ceil(4 × 80 / 50) = ceil(6.4) = 7.
몇 가지 세부 규칙이 있다.
- CPU 사용률은 requests 대비다. 파드의
resources.requests.cpu가 없으면 사용률을 계산할 수 없어 HPA 가 동작하지 않는다. - 허용 오차(tolerance): 비율(current/desired)이 1 에 충분히 가까우면(기본 0.1 이내) 아무것도 하지 않는다. 작은 흔들림에 반응하지 않기 위해서다.
- 여러 지표: 지표를 여러 개 주면 각각 계산한 값 중 가장 큰 것을 쓴다.
- 준비 안 된 파드: 막 시작해 아직 ready 가 아닌 파드나 지표가 없는 파드는 계산에서 보수적으로 다뤄진다. 스케일 아웃 직후의 일시적 지표로 과잉 반응하지 않게 하려는 것이다.
안정화 창과 behavior
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
스케일 다운의 기본 안정화 창은 300초다. HPA 는 지난 창 동안 계산된 권장값 중 가장 큰 값을 택해 줄인다. 부하가 잠깐 꺼졌다고 바로 줄였다가 다시 늘리는 진동(flapping)을 막는다. 스케일 업은 기본적으로 즉시 반응한다. 늘리는 것은 빨리, 줄이는 것은 천천히가 기본 철학이다.
지표의 종류
| 타입 | 예 |
|---|---|
| Resource | 파드 CPU·메모리 사용률 (metrics-server 필요) |
| Pods | 파드별 커스텀 지표(초당 요청 수 등) |
| Object | 다른 객체의 지표(인그레스의 초당 요청 수 등) |
| External | 클러스터 밖 지표(큐 길이 등) |
CPU 는 편하지만 늘 좋은 지표는 아니다. I/O 대기가 많은 서비스는 CPU 가 낮아도 지연이 늘고, 큐 소비자는 큐 길이가 진짜 부하다. 이벤트 기반 스케일링을 위해 KEDA 같은 프로젝트가 큐·스트림 지표를 HPA 로 연결해 준다. KEDA 는 0개까지 줄였다가 다시 늘리는 것도 지원한다.
반응 시간의 합
스케일 아웃은 즉시 효과가 나지 않는다.
부하 증가 → 지표 수집 지연 → HPA 계산 주기 → 파드 생성 → (노드가 없으면 노드 추가) → 이미지 pull → 앱 기동 → readiness 통과
노드 추가까지 필요하면 수 분이 걸릴 수 있다. 그래서 급격한 스파이크에는 여유 용량(목표 사용률을 낮게, minReplicas 를 넉넉히)이 필요하고, 예측 가능한 피크(정시 이벤트)에는 미리 늘려 두는 예약 스케일링을 함께 쓴다.
직접 해 보기
HPA 공식, 허용 오차, 스케일 다운 안정화 창을 시뮬레이션해 보자. 한 단계는 15초로 가정한다.
import math
def hpa(series, target=50, start=2, min_r=2, max_r=20, tolerance=0.1, window_steps=20):
replicas, history = start, []
for step, total_cpu in enumerate(series):
current = total_cpu / replicas # 파드당 평균 사용률(%)
ratio = current / target
if abs(ratio - 1) <= tolerance:
desired = replicas
else:
desired = math.ceil(replicas * ratio)
desired = max(min_r, min(max_r, desired))
history = (history + [desired])[-window_steps:]
if desired < replicas: # 줄일 때는 창 안의 최댓값
desired = max(history)
if desired != replicas or step % 8 == 0:
print(f"t={step * 15:4}s load={total_cpu:4}% per-pod={current:5.1f}% "
f"-> replicas {replicas} => {desired}")
replicas = desired
# 클러스터 전체 CPU 수요(파드 1개 = 100% 기준). 급증 → 유지 → 급감 → 잠깐 튐
load = [100] * 4 + [400] * 8 + [110] * 6 + [300] + [110] * 25
hpa(load)
결과는 다음과 같다.
t= 0s load= 100% per-pod= 50.0% -> replicas 2 => 2
t= 60s load= 400% per-pod=200.0% -> replicas 2 => 8
t= 120s load= 400% per-pod= 50.0% -> replicas 8 => 8
t= 240s load= 110% per-pod= 13.8% -> replicas 8 => 8
t= 360s load= 110% per-pod= 13.8% -> replicas 8 => 8
t= 465s load= 110% per-pod= 13.8% -> replicas 8 => 6
t= 480s load= 110% per-pod= 18.3% -> replicas 6 => 6
t= 570s load= 110% per-pod= 18.3% -> replicas 6 => 3
t= 600s load= 110% per-pod= 36.7% -> replicas 3 => 3
부하가 4배가 되자 한 번에 2개에서 8개로 늘었다. 부하가 다시 줄어든 뒤에도 바로 줄이지 않았다. 마지막 “8” 권장치가 안정화 창(20단계 = 300초) 밖으로 밀려난 t=465s 에야 처음 줄였다. 그런데 3이 아니라 6이다. t=270s 의 짧은 튐이 “6” 이라는 권장치를 창 안에 남겼기 때문이다. 그 권장치마저 창 밖으로 나간 t=570s 에 비로소 3이 됐다. 창이 없었다면 8 → 3 → 6 → 3 처럼 흔들렸을 것이다. 안정화 창은 이렇게 “최근에 필요했던 최대 용량”을 기억하며 계단식으로 내려간다.
현업에서는
- requests 를 현실적으로. HPA 의 CPU 사용률은 requests 대비다. requests 를 너무 낮게 잡으면 사용률이 늘 수백 % 로 보여 최대치까지 늘고, 너무 높게 잡으면 노드 자원이 낭비되고 스케일이 둔해진다. 실측 사용량을 보고 정한다.
- HPA 와 VPA 를 같은 지표로 함께 쓰지 않는다. 둘 다 CPU 를 보고 하나는 개수를, 하나는 크기를 바꾸면 서로의 입력을 흔든다. 함께 쓰려면 HPA 는 커스텀 지표로, VPA 는 자원 크기로 역할을 나눈다.
- GitOps 와의 충돌. 디플로이먼트 매니페스트에
replicas가 박혀 있으면 GitOps 도구가 HPA 가 늘린 수를 되돌린다(230번). 매니페스트에서replicas를 빼는 것이 일반적이다. - 고정 노드 클러스터. 노드를 사서 쓰는 온프레미스·홈랩에서는 노드 수 오토스케일이 없다. HPA 의
maxReplicas를 노드 자원 총량에 맞추지 않으면 늘어난 파드가 Pending 에 머무른다. 이 경우 오토스케일링은 “있는 자원을 서비스 간에 재분배하는 도구”로 쓰는 셈이다. - 비용 상한. 클라우드에서
maxReplicas와 노드 그룹 최대 크기는 비용 상한이기도 하다. 무한 루프 버그나 공격 트래픽이 오토스케일러를 통해 청구서로 바뀌지 않게 상한을 둔다(240번).
확인 문제
- 파드 5개, 목표 CPU 60%, 현재 평균 90% 일 때 HPA 가 계산하는 원하는 레플리카 수는?
- 파드에 CPU requests 가 없으면 CPU 사용률 기반 HPA 는 어떻게 되는가?
- 스케일 다운에 안정화 창을 두는 이유는?
- HPA 가 파드를 늘렸는데 Pending 이다. 무엇이 필요한가?
- 큐 소비자 서비스를 CPU 기반으로 오토스케일하면 생길 수 있는 문제는?
풀이
- ceil(5 × 90 / 60) = ceil(7.5) = 8.
- 사용률을 계산할 수 없어 스케일링이 동작하지 않는다.
- 부하가 잠깐 줄었다가 다시 오를 때 줄였다 늘리는 진동을 막기 위해서다. 창 안의 최댓값 권장치를 기준으로 줄인다.
- 노드 자원. Cluster Autoscaler 등으로 노드를 늘리거나, 노드가 고정이면 maxReplicas·requests 를 조정한다.
- 대기 중인 메시지가 쌓여도 소비자가 I/O 대기 중이면 CPU 가 낮아 늘지 않는다. 큐 길이 같은 외부 지표를 써야 한다.
더 읽을거리 (References)
- Kubernetes Docs, Horizontal Pod Autoscaling
- Kubernetes Docs, HorizontalPodAutoscaler Walkthrough
- Kubernetes Docs, Autoscaling Workloads
- Kubernetes Autoscaler, Cluster Autoscaler