컴퓨터공학 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번).

확인 문제

  1. 파드 5개, 목표 CPU 60%, 현재 평균 90% 일 때 HPA 가 계산하는 원하는 레플리카 수는?
  2. 파드에 CPU requests 가 없으면 CPU 사용률 기반 HPA 는 어떻게 되는가?
  3. 스케일 다운에 안정화 창을 두는 이유는?
  4. HPA 가 파드를 늘렸는데 Pending 이다. 무엇이 필요한가?
  5. 큐 소비자 서비스를 CPU 기반으로 오토스케일하면 생길 수 있는 문제는?

풀이

  1. ceil(5 × 90 / 60) = ceil(7.5) = 8.
  2. 사용률을 계산할 수 없어 스케일링이 동작하지 않는다.
  3. 부하가 잠깐 줄었다가 다시 오를 때 줄였다 늘리는 진동을 막기 위해서다. 창 안의 최댓값 권장치를 기준으로 줄인다.
  4. 노드 자원. Cluster Autoscaler 등으로 노드를 늘리거나, 노드가 고정이면 maxReplicas·requests 를 조정한다.
  5. 대기 중인 메시지가 쌓여도 소비자가 I/O 대기 중이면 CPU 가 낮아 늘지 않는다. 큐 길이 같은 외부 지표를 써야 한다.

더 읽을거리 (References)