Kubernetes: вертикальний скейлінг подів з Vertical Pod Autoscaler

Автор |  29/04/2023
 

Окрім Horizontal Pod Autoscaler (HPA), який створює додаткові поди якщо наявні починають використовувати більше CPU/Memory, ніж налаштовано у лімітах HPA, існує і Vertical Pod Autoscaler (VPA), який працює за іншою схемою: замість горизонтального масштабування, тобто збільшення кількості подів, він змінює resources.requests поду, що призводить до того, что Kubernetes Scheduler “переселяє” цей под на іншу WorkerNode, якщо на поточній не вистачає ресурсів.

Тобто, VPA постійно моніторить споживання ресурсів контейнерами у подах, і змінює значення відповідно до актуального споживання ресурсів, і може як збільшувати значення реквестів, так і зменшувати його, таким чином автоматично налаштовуючи потреби пода, щоб уникнути нераціонального використання ресурсів інстансів Kubernetes-кластеру та забезпечити сам под достатнім CPU time і пам’ятю.

Компоненти Vertical Pod Autoscaler

Після деплою VPA, він створює три поди для своєї роботи:

  • recommender: займається моніторингом використання ресурсів подами, і видає свої рекомендації по значенню cpu/mem requests, які треба встановити подам
  • updater: моніторить поди та їхні поточні значення cpu/mem requests, і якщо ці значення не збігаються зі значеннями від recommender – то “вбиває” їх (EvictedByVPA Kubernetes event), щоб контролери Kubernetes перестворили їх з потрібними значеннями
  • admission-plugin: займається власне тим, що встановлює значення реквестів для нових подів, або тих, що були перестворенні після того, як updater кільнув їх

Обмеження Vertical Pod Autoscaler

При використанні VPA, майте на увазі, що:

  • VPA не відслідковує процесс перестворення подів, тобто після того, як под був evicted – то його створення вже цілком залежить від Kubernetes. Якщо у кластері на момент перестворення подів не буде вільних WorkerNodes, то под може залишитиcь у Pending статусі, тому бажано мати Cluster Autoscaler або Karpenter, який запустить новую ноду
  • VPA не може використовуватись одночасно з HPA, якщо скейлінг налаштовано на CPU/Memory, але їх можна використовувати, якщо HPA налаштований на custom metrics
  • також майте на увазі сам факт того, що при роботі VPA перестворює поди, тобто якщо у вас немає якогось fault-tolerant у вигляді додаткових подів, які зможуть взяти на себе навантаження на час перестворення поду – то сервіс буде недоступний, допоки відповідний контроллер (ReplicaSet, StatefulSet, etc) не запустить новий інстанс поду

Див більше у Known limitations.

Запуск Vertical Pod Autoscaler

При роботі, VPA покладається на Kubernetes Metrics Server для отримання значень CPU/Mem подів, але також може використовувати Prometheus, див. How can I use Prometheus as a history provider for the VPA recommender.

Встановлення Metrics Server

Оскільки тестити VPA будемо у Minikube, то встановимо плагін Metrics Server:

[simterm]

$ minikube addons enable metrics-server

[/simterm]

Або у разі звичайного Kubernetes-кластеру – з Helm-чарту:

[simterm]

$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
$ helm -n kube-system upgrade --install metrics-server metrics-server/metrics-server

[/simterm]

І дивимось, чи працює kubectl top pod, який бере дані саме з Metrics Server:

[simterm]

$ kubectl top pod --all-namespaces
NAMESPACE     NAME                               CPU(cores)   MEMORY(bytes)   
kube-system   etcd-minikube                      83m          33Mi            
kube-system   kube-apiserver-minikube            249m         254Mi           
kube-system   kube-controller-manager-minikube   54m          45Mi            
kube-system   kube-scheduler-minikube            26m          22Mi

[/simterm]

Встановлення Vertical Pod Autoscaler

Також використовуємо Helm-чарт – cowboysysop/vertical-pod-autoscaler:

[simterm]

$ helm repo add cowboysysop https://cowboysysop.github.io/charts/
$ helm -n kube-system upgrade -install vertical-pod-autoscaler cowboysysop/vertical-pod-autoscaler

[/simterm]

Перевіряємо поди:

[simterm]

$ kk -n kube-system get pod -l app.kubernetes.io/name=vertical-pod-autoscaler
NAME                                                            READY   STATUS    RESTARTS   AGE
vertical-pod-autoscaler-admission-controller-655f9b57d7-q85kc   1/1     Running   0          58s
vertical-pod-autoscaler-recommender-7d964f7894-k87hb            1/1     Running   0          58s
vertical-pod-autoscaler-updater-7ff97c4d85-vfjkj                1/1     Running   0          58s

[/simterm]

Та його CustomResourceDefinitions:

[simterm]

$ kk get crd
NAME                                                  CREATED AT
verticalpodautoscalercheckpoints.autoscaling.k8s.io   2023-04-27T08:38:16Z
verticalpodautoscalers.autoscaling.k8s.io             2023-04-27T08:38:16Z

[/simterm]

Тепер все готове, щоб починати ним користуватись.

Приклади роботи з Vertical Pod Autoscaler

В репозиторії VPA є директорія examples, яка містить приклади маніфестів, наприклад у файлі hamster.yaml є приклад налаштованого VPA та тестового Deployment.

Але давайте створимо свої маніфести, та задеплоїмо ресурси окремо.

Спочатку Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: registry.k8s.io/ubuntu-slim:0.1
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

Тут маємо створити два поди, яким задаємо requests у 100 Milicpu та 50 Megabyte memory.

Деплоймо:

[simterm]

$ kubectl apply -f hamster-deployment.yaml 
deployment.apps/hamster created

[/simterm]

За хвилину-дві перевіряємо ресурси, які реально споживаються подами:

[simterm]

$ kk top pod
NAME                       CPU(cores)   MEMORY(bytes)   
hamster-65cd4dd797-fq9lq   498m         0Mi             
hamster-65cd4dd797-lnpks   499m         0Mi

[/simterm]

Тепер додамо VPA:

apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  # recommenders field can be unset when using the default recommender.
  # When using an alternative recommender, the alternative recommender's name
  # can be specified as the following in a list.
  # recommenders: 
  #   - name: 'alternative'
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]

Деплоїмо:

[simterm]

$ kubectl apply -f hamster-vpa.yaml 
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created

[/simterm]

Перевіряємо сам VPA:

[simterm]

$ kk get vpa
NAME          MODE   CPU   MEM   PROVIDED   AGE
hamster-vpa   Auto                          14s

[/simterm]

І за хвилину-дві, коли спрацює recommender:

[simterm]

$ kk get vpa
NAME          MODE   CPU    MEM       PROVIDED   AGE
hamster-vpa   Auto   587m   262144k   True       43s

[/simterm]

І ще за хвилину – перевіряємо поди, коли спрацює Updater:

[simterm]

$ kk get pod
NAME                       READY   STATUS        RESTARTS   AGE
hamster-65cd4dd797-fq9lq   1/1     Terminating   0          3m43s
hamster-65cd4dd797-hc9cn   1/1     Running       0          13s
hamster-65cd4dd797-lnpks   1/1     Running       0          3m43s

[/simterm]

Та перевіряємо значення requests нового поду:

[simterm]

$ kubectl get pod hamster-65cd4dd797-hc9cn -o yaml | yq '.spec.containers[].resources'
{
  "requests": {
    "cpu": "587m",
    "memory": "262144k"
  }
}

[/simterm]

Тепер, як ми побачили VPA в дії, давайте трохи розберемось з його API та доступними параметрами.

Vertical Pod Autoscaler API reference та параметри

Повний опис див. у API reference, а зараз просто зробимо describe нашого існуючого VPA, щоб зрозуміти, що там взагалі є:

[simterm]

$ kubectl describe vpa/hamster-vpa
Name:         hamster-vpa
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  autoscaling.k8s.io/v1
Kind:         VerticalPodAutoscaler
Metadata:
  Creation Timestamp:  2023-04-27T09:05:41Z
  Generation:          61
  Resource Version:    7016
  UID:                 227c0ce6-7f86-4bff-b9b5-d88914f90bec
Spec:
  Resource Policy:
    Container Policies:
      Container Name:  *
      Controlled Resources:
        cpu
        memory
      Max Allowed:
        Cpu:     1
        Memory:  500Mi
      Min Allowed:
        Cpu:     100m
        Memory:  50Mi
  Target Ref:
    API Version:  apps/v1
    Kind:         Deployment
    Name:         hamster
  Update Policy:
    Update Mode:  Auto
Status:
  Conditions:
    Last Transition Time:  2023-04-27T09:06:11Z
    Status:                True
    Type:                  RecommendationProvided
  Recommendation:
    Container Recommendations:
      Container Name:  hamster
      Lower Bound:
        Cpu:     569m
        Memory:  262144k
      Target:
        Cpu:     587m
        Memory:  262144k
      Uncapped Target:
        Cpu:     587m
        Memory:  262144k
      Upper Bound:
        Cpu:     1
        Memory:  262144k
Events:          <none>

[/simterm]

Або у “чистому” yaml:

[simterm]

$ kubectl get vpa/hamster-vpa -o yaml           
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
...
spec:
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      controlledResources:
      - cpu
      - memory
      maxAllowed:
        cpu: 1
        memory: 500Mi
      minAllowed:
        cpu: 100m
        memory: 50Mi
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hamster
  updatePolicy:
    updateMode: Auto
status:
  ...
  recommendation:
    containerRecommendations:
    - containerName: hamster
      lowerBound:
        cpu: 570m
        memory: 262144k
      target:
        cpu: 587m
        memory: 262144k
      uncappedTarget:
        cpu: 587m
        memory: 262144k
      upperBound:
        cpu: "1"
        memory: 262144k

[/simterm]

І тепер розлянемо параметри з нашого VPA та інші, которі можуть бути нам корисні в майбутньому:

  • spec (VerticalPodAutoscalerSpec):
    • targetRef: тип контролеру, який відповідає за поди, которі будуть скейлитись цим VPA
    • updatePolicy (PodUpdatePolicy): задає, чи будуть рекомендації застосовані при створенні поду, і чи будуть застосовуватись протягом його роботи
      • updateMode: може мати значення “Off“, “Initial“, “Recreate” та “Auto” (дефолтне значення):
        • Off: не буде застосовувати нові значення, а тільки внесе їх у поле status (див. нижче)
        • Initial: застосує значення тільки при створенні поду
        • Recreate: застосує значення при створенні поду і під час його роботи
        • Auto: на цей час виконує теж саме, що Recreate (хоча ще чотири роки тому наче казали, що планується при “Auto” міняти реквести без рестарту)
      • minReplicas: мінімальна кількість подів, які мають бути в статусі Running, щоб VPA Updater виконав Pod Eviction для застосування нових значень у requests
    • resourcePolicy (PodResourcePolicy): задає параметри того, як CPU та Memory requests будуть налаштовуватись для конкретних контейнерів, якщо не задано – то VPA застосує нові значення для всіх контейнерів в поді
      • containerPolicies (ContainerResourcePolicy): налаштування для конкретних контейнерів, або для всіх, які не мають власних параметрів, за допомогою containerName = '*'
        • containerName: ім’я контейнеру, для якого описуються параметри
        • mode: задає, чи будуть рекомендації застосовані при створенні контейнеру, і чи будуть застосовуватись протягом його роботи, може мати значення “Off” або “Auto” (дефолтне значення)
        • minAllowed та maxAllowed: задає мінімальні та максимальні значення для CPU/Memory requests
        • ControlledResources: для яких саме ресурсів застосовувати рекомендації – ResourceCPU, ResourceMemory, або обидва (дефолтне, якщо не вказано жодного)
  • status (VerticalPodAutoscalerStatus): останні рекуомендації від Recommender
    • recommendation (RecommendedPodResources): останні рекомендовані значення CPU/Memory
      • containerRecommendations (RecommendedContainerResources): рекомендації для кожного контейнеру
        • containerName: ім’я контейнеру
        • target: рекомендовані значення для контейнеру
        • lowerBound: мінімально рекомендовані значення для контейнеру
        • upperBound: максимально рекомендовані значення для контейнеру
        • uncappedTarget: останні рекомендовані значення CPU/Memory на основі реального споживання ресурсів без врахування ContainerResourcePolicy (тобто без minAllowed та maxAllowed), не враховується Recommender, відображається тільки для інформації

На цьому поки все.

З VPA бувають проблеми, але в цілому у нас в продакшені працють без нарікань, наприклад – міняються значення для подів Prometheus-серверу.