HPA

개요

워크로드들을 자동으로 스케일링해주는 오브젝트.[1]
지정된 조건에 따라 레플리카의 개수를 늘리거나 줄이는 동작을 알아서 해준다.
물론 데몬셋 같이 애초에 스케일링 안 되는 요소에는 스케일링을 하지 않는다.
이름에서 알 수 있듯이, 복제본을 늘리거나 줄이는 작업을 한다.
한 어플리케이션 자체의 리소스를 확장해주는 Vertical Pod Autoscaler와는 다르다.

작동 원리


오토스케일러 역시 컨트롤러를 통해 동작한다.
이 컨트롤러는 주기적으로 대상이 된 오브젝트의 상태를 감시한다.
(kube-controller-manager--horizontal-pod-autoscaler-sync-period 인자를 수정하여 기간(기본 15초)을 조정할 수 있다.)
컨트롤러는 스케일링할 scaleTargetRef 필드로 건드릴 워크로드(디플로이먼트, 스테이트풀셋)를 추적한다.
그리고 해당 워크로드의 라벨 셀렉터를 통해 파드들을 찾아낸 후, 지표를 수집한다.
얻어낸 후에는? 지표 계산을 통해 필요한 레플리카 수를 구한 뒤에 해당 워크로드에 스케일링 명령을 내린다!

참고로 스케일 명령 자체는 해당 오브젝트가 /scale이라는 api에 요청을 보내는 것을 말한다.
그래서 어떤 커스텀 오브젝트라도 /scale에 대한 서브리소스만 같이 만들어둔다면 똑같이 HPA의 대상으로 삼을 수 있다.

스케일링 알고리즘

그렇다면 얼마나 스케일링 해야 하는지는 어떻게 정하는가?

방식은 간단하다.
일단 현재 메트릭과 희망하는 메트릭을 보고 얼마나의 비율로 값이 다른지를 구한다(분수).
해당 비율은 희망하는 레플리카 개수와 현제 레플리카 개수의 비율과 같으므로 현재 레플리카 개수를 곱하면 늘어나거나, 줄어들어서 맞춰져야 할 레플리카 개수가 나오게 되는 것이다.
물론 레플리카는 정수이므로 올림 처리된다

그런데 여기에서 하나의 의문이 더 발생한다.
그렇다면 현재 메트릭 값이라는 것은 무엇인가?
가령 cpu 사용률이 메트릭이라고 쳐보자.
그렇다면 각 파드의 cpu 사용량을 일단 가져와서, 이를 합하고 현재의 개수로 나눈다(평균 구하기).
그럼 그게 현재 메트릭 값이다!

이로부터 위의 저 식은 이렇게도 표현될 수 있다. 평균을 구하면서 분모로 들어가는 현재 개수라는 값을 약분해버린 것이다.
그래서 위의 예시에서, 현재 레플리카가 3개인 상황에서 메모리 사용량을 보아 레플리카가 4개로 늘어나야 한다는 것을 알 수 있다. 다만 이렇게 나온 값이 현재 레플리카 개수와 거의 차이가 없다면(0.1배 차), 조정을 하지 않도록 설정돼있다.

여기까지만 들으면 알고리즘은 자체는 굉장히 쉽다는 것을 알 수 있다.
그러나 실제 작업이 들어갈 때는 몇가지 추가 조건이 붙는다.

무시되는 파드

일단 fail 상태인 파드와 삭제되는 중인 파드는 계산에서 완전히 제외된다.

특수 고려되는 파드

메트릭이 수집되지 않고 있는 파드, 준비 상태가 아닌 파드에 대해서는 조금 특별하게 대응한다.
일단 이들을 포함하여 계산하기에 앞서 상태가 확인되는 파드들만 이용해서 계산을 진행하고, 그 다음 이 친구들을 반영한다.
아래에서 보겠지만 이는 일단 현재 레플리카를 늘려야 하는지, 줄여야 하는지를 선판단하기 위해서이다.

일단 파드의 준비 상태에 대해 not yet ready 상태를 매길 때가 있다.
이것은 시작되지 얼마 되지 않은 파드가 아직 제대로 요청들을 수행해낼 수 없어 다른 파드들이 부하를 받고 있는 상황일 때 과도하게 파드가 늘어나게 스케일링을 하는 것을 막기 위함이다.
파드가 시작된 이후, 준비 상태가 된지 얼마 안 됐다면 해당 파드를 not yet ready로 간주한다(--horizontal-pod-autuscaler-initial-readiness-delay인자 기본 30초).
다만 파드가 준비가 아니었다가 준비상태가 된 지 얼마 안 됐더라도 파드 자체가 시작된 지는 오래 됐다면(--horizontal-pod-autoscaler-cpu-initialization-period인자 기본 5분), 이때는 바로 ready 상태로 반영한다.
그래서 이 not yet ready 파드를 어떻게 활용하는가?
이들에 대해서는 메트릭이 0퍼센트라고 가정하고 계산한다.
이를 통해 이 파드들이 현재 다른 파드들의 부하를 견딜 수 있다고 상정하며 불필요한 스케일링을 막는 것이다.
(이런 가정이 오히려 필요한 스케일링을 늦출 수도 있지 않을까?)

메트릭이 수집되고 있지 않는 파드에 대해서는 스케일링을 덜하게 만드는 방향으로 전제를 하고 계산한다.
즉 스케일 다운을 해야 하는 상황이라면 해당 파드가 100퍼센트의 자원을 쓰고 있다고 가정한다.
반대로 스케일 아웃을 해야한다면 해당 파드가 0퍼를 쓰고 있다고 가정한다.
이를 통해 스케일링이 최대한 덜 일어나도록 만들어버린다.

아무튼 이런 식으로 추가 계산을 해서 다시 필요한 레플리카 개수를 산출한다.
이로 인해 원래 늘어났어야 할 파드가 늘어나지 않는다던가 하는 상황이 충분히 나올 수 있다.
HPA는 꽤나 스케일링에 있어 보수적인 전략을 펼친다는 것을 알 수 있는데, 스케일링을 통해 늘어난 파드의 정보가 초기에 제대로 반영되지 않는 것을 상당히 신경 쓰기 위함인 것으로 해석된다.
그렇기에 HPA를 설정할 때는 가급적이면 넉넉하게 조건을 설정해주는 것이 안정적인 서비스를 구축하는데 도움을 줄 것이다.

여러 메트릭 조건이 걸렸다면

cpu도 기준이고 메모리도 기준 값으로, 복수 조건이 설정이 돼있을 수 있다.
이때는 각 값을 계산한 이후, 레플리카의 수를 가장 높게 설정하는 값을 기준으로 한다.
이미 스케일링이 필요한 상황이라면, 최대한 적극적으로 스케일링에 임한다는 것을 알 수 있다.

이중 어떤 값이 측정되지 않고 있다면 스케일 아웃을 하는 과정에서는 제외하고 계산에 임한다.
그러나 스케일 다운을 하는 경우에 측정되지 않는 값이 있다면 스케일 다운은 진행되지 않는다.
정상 서비스 운영에 대한 영향을 최소화하는 방향으로 스케일링을 진행하는 것이다.

이전 계산 고려 - 안정화 윈도우

마지막으로, 이전 계산 값도 스케일링에 고려한다!
합산을 한다던가 하는 것은 아니고, 스케일링이 들쑥날쑥 한 기준 시간마다 변동되는 것에 제약을 걸기 위함이다.
윈도우 기간(--horizontal-pod-autoscaler-downscale-stabilization인자로 기본 5분)을 두고 이 기간 내의 시간 동안 가장 큰 값으로만 스케일링하도록 돼있다.
즉, 한번 워크로드의 레플리카가 크게 늘어난 후면 대충 5분간은 다시 떨어지지 않는다는 말이다.
이는 스케일 다운이 최대한 점진적으로, 급격하게 변동하는 메트릭값으로부터 안전하도록 하기 위한 조치다.
이러한 방식을 thrashing, flapping이라고 부른다.

사용 시 유의사항

kubectl 조작


kubectl 에서 autocale을 통해 간단하게 조작할 수 있다.

아주 간단하게, cpu가 평균 80퍼가 넘어갈 때 오토스케일링 되도록 만들었다.

메트릭 수집

어디에서 메트릭을 수집해서 스케일링을 진행하는가?
기본적으로 3가지의 api로 접근 가능한 메트릭이면 된다.

이 api들은 kube-apiserverAPI Aggregation Layer으로 커스텀 api를 만들어 메트릭을 노출시키는 방식이다.
그리고 이 메트릭들을 사용할 때는 [[#metrics]]에서 pods, object, external 타입을 이용하면 된다.

k get --raw /apis/metrics.k8s.io

해당 메트릭들이 잘 노출되고 있는지 확인할 때는 이렇게 직접 api 경로를 명시해서 요청을 날려보면 된다.
image.png
k top pod를 쓸 때, -v 옵션을 써서 세부 과정을 디버깅해보면 먼저 aggregation layer에서 그룹 디스커버리를 호출하고, 관련 api를 찾아서 요청이 진행되는 것을 확인할 수 있다.
image.png
위에처럼 그냥 --raw를 사용하면 이 중간과정없이 바로 해당 메트릭 api로 요청을 쏘는 것을 볼 수 있다.

커스텀 메트릭을 이용하는 방법 중 유용한 게 또 프로메테우스다.
자세한 건 Prom-Adaptor를 참조하다.

스케일링 중단시키기

스케일링을 중단하고 싶다면 그냥 HPA 오브젝트를 없애면 된다.
근데 HPA 오브제트의 대상이 되는 워크로드에서 replica를 0으로 세팅해도 HPA의 조정이 중단된다.
측정 알고리즘 상 곱셈밖에 없기 때문에, 0이 되는 순간 그냥 계산이 안 돼버리는 것이다.

파일 관리 관련

워크로드 양식파일을 따로 관리하고 있다면, 해당 파일에서 replicas 필드를 없애는 것을 추천한다.
내 파일을 적용할 때마다 HPA가 고생할 수도 있다..
근데 심지어 윈도우 안정화 이슈로 변동이 빠르게 적용되지도 않을 것이다.
기존에 있던 놈에 갑자기 replicas 필드를 없애면 레플리카가 하나로 줄어들게 될 텐데, 이건 기본값이 잠시 세팅되는 거라 걱정하지 말자.
(그게 걱정되면 처음부터 HPA한테 스케일링을 맡겨라)

k apply -f test.yaml --server-side --field-manager={hpa이름} --validate=false

이런 식으로 서버 사이드로 관리권을 넘기는 방법도 있다는데, 이건 시도 안 해봤다.

양식 작성법

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hpa-deployment
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

기본적인 양식 작성법으로, 거의 위 명령어와 동일하다.[2]
먼저 스케일할 타겟을 지정하고, 최대와 최소값을 지정한다.
그 스케일의 기준과 각 행동을 지정하면 된다.
(참고로 아래 나올 다양한 방식들을 활용하기 위해서는 꼭 apiVersion을 v2로 쓰자)

scaleTargetRef

스케일링의 대상을 지정하는 필드이다.
보통은 디플로이먼트, 스테이트풀셋 정도가 대상이 되는데, 위에서도 언급했듯이 /scale api가 구현된 어떤 오브젝트든 대상이 될 수 있다.
대표적으로는 Argo CD 롤아웃 정도가 있는 것 같다.
단 하나 유의할 만한 건 같은 네임스페이스의 오브젝트를 대상으로 하라는 것 정도.

metrics

이제 어떤 메트릭을 수집하고, 그 값의 기준을 어디에 둘 것인지를 보자.
이때 주의할 게 하나 있다.
메트릭에는 기본적으로 두 가지 타입의 값이 있다.

하나는 그냥 **원시적인 값(raw value)** 으로, 단순 메모리 사용량 같은 것이 예시이다. 두번째는 **사용률(utilization)** 로, 이것은 일단 사용중인 원시적인 값을 구하고, 파드 안의 컨테이너의 리소스 요청량을 분모로 하여 비율을 구한다. 이런 방식으로 인해 사용률을 통해 제한을 걸때는 반드시 리소스 요청 스펙도 명시해야 한다. ### resource ```yaml metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 - type: Resource resource: name: memory target: type: AverageValue averageValue: 100m ``` `metrics.k8s.io` api로 제공되는, CPU와 메모리 자원은 resource 타입이다. 위에서도 봤지만 이것은 기본적으로 파드 단위로 계산되기에, 특정 컨테이너는 높은 값을 가지면서 다른 컨테이너들에 의해 파드의 상태가 제대로 판단되지 않는 이슈가 있을 수 있다. (아니 근데 왜 헷갈리게 원시값은 AverageValue면서 사용량은 또 Util이라고만 하냐;;) ### containerResource ```yaml type: ContainerResource containerResource: name: cpu container: application target: type: Utilization averageUtilization: 60 ``` 위의 문제를 해소하고자 1.30 기준으로 stable된 방식으로 컨테이너 단위로 제한을 걸게도 할 수 있다. 이걸 활용할 때는 해당 워크로드에서 컨테이너 이름을 함부로 변경하지 않도록 주의하자.

여기에 추가적으로 세 가지 유형의 커스텀 메트릭을 사용할 수 있다.

pods

  - type: Pods
    pods:
      metric:
        name: packets-per-second
      target:
        type: AverageValue
        averageValue: 1k

파드의 다른 메트릭을 이용하고 싶다면 이렇게 Pods라고 써준다.[3]
유의할 점은 일단 해당 메트릭을 노출하는 다른 추가 세팅을 먼저 해줘야 한다는 것.
그래서 해당 세팅을 통해 metric.name 필드에 들어간 이름의 메트릭이 실제로 있어야만 한다.

그리고 여기에는 AverageValue만 들어갈 수 있다는 것.
사용률은 리소스 요청량을 분모로 하는 거니까, 어쩌면 이건 당연한데, 어쩌면 Dynamic Resource Allocation이 GA가 된다면 상황이 달라질 수도 있겠다.

object

  - type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        name: main-route
      target:
        type: Value
        value: 10k

임의의 오브젝트의 지표를 이용해 스케일링을 지정하고 싶다면 이걸 활용해야 한다.
파드가 아닌 다른 오브젝트를 사용하는 만큼 사용할 오브젝트를 describedObject로 명시한다.
autoscaling/v2 api 에서 사용가능한 방식이란 것을 참고하자.
관련 오브젝트에 대한 값이 metrics api로 노출만 되고 있고, metric 필드에 명시된 이름이 거기에 있다면 그것을 스케일의 기준으로 삼을 수 있게 된다.
이 친구는 Value, AverageValue가 가능하다.

type: Object
object:
  metric:
    name: http_requests
    selector: {matchLabels: {verb: GET}}

이런 식으로, 오브젝트로 추적할 수 없는 내용의 지표를 담을 때 라벨 셀렉터를 사용할 수도 있다.
이때의 라벨은 프로메테우스 데이터 형식이나 각종 관측 가능성 툴에서 정의하는 메트릭의 라벨을 말하는 건데, 이것들을 쿠버네티스 라벨 셀렉터 형식으로 작성하면 된다.

external

- type: External
  external:
    metric:
      name: queue_messages_ready
      selector:
        matchLabels:
          queue: "worker_tasks"
    target:
      type: AverageValue
      averageValue: 30

클러스터 내부의 오브젝트와 관련 없는 메트릭을 이용하는 방법도 존재한다.
위에까진 커스텀 메트릭이라 부르는데, 이놈은 외부 메트릭이라 부른다.
이 친구도 Value, AverageValue가 가능하다.
세팅하는 것이 복잡할 수 있고, 메트릭 세팅을 하는 것이 까다롭기에 사용하지 않는 것이 권장된다.

behavior

  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 4
        periodSeconds: 15
      selectPolicy: Max

스케일링 시 어떻게 행동할지도 정할 수 있다(세팅하지 않을 시 적용되는 값은 위의 예시).
크게는 스케일 다운과 스케일 업을 할 때의 행동을 각각 지정할 수 있다.
여기에 [[#이전 계산 고려 - 안정화 윈도우]]에서 봤던 값을 커스텀하는 것도 가능하다.
여기에서 구체적으로 지정하는 게 무어냐, 어차피 위의 계산에 따라 스케일링될 값은 정해져있는데?
여기에서 지정하는 것은 스케일링될 때 변동성을 완화하는 설정이다.

      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

이 건 15초 기준으로 스케일링될 때 100퍼센트까지만 허용한다는 것이다.
그 이상 스케일링돼야한다고 해도 15초이내로는 최대 100퍼센트까지만 가능한 것이다.
파드를 지정해서 아예 구체적인 숫자를 지정하는 것도 가능하다.

      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 4
        periodSeconds: 15
      selectPolicy: Max

복수의 정책이 들어간다면 가장 크게 변동을 허용하는 정책이 채택된다.
정책이 이렇게 세팅됐고 레플리카가 하나라 쳐보자.
근데 15개가 늘어나야 하는 상황이다.
그렇다면 일단 처음에는 아래 정책이 적용돼 4개가 늘어난다.
그러나 그 다음에는 위 기준이 적용될 때 5개가 늘어날 수 있으므로 위 정책이 적용된다.
(1 -> 1 + 4 -> 1+ 4 + 5)
최대 변동 정책으로 적용되는 게 싫다면 selectPolicy: Min으로 하자.
selectPolicy: Disabled라 하면 해당 방향의 정책들은 아예 적용되지 않는다.
그래서 스케일다운을 하기는 싫다면 이걸 설정해버리면 된다.

관련 문서

이름 noteType created
KEDA knowledge 2024-12-29
Prometheus-Adapter knowledge 2025-03-04
5W - HPA, KEDA를 활용한 파드 오토스케일링 published 2025-03-07

참고


  1. https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ ↩︎

  2. https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/ ↩︎

  3. https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/ ↩︎