> For the complete documentation index, see [llms.txt](https://book.konstantinsecurity.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.konstantinsecurity.com/readme/architect/ci-cd/kubernetes-highload-deploy-part-2.md).

# Kubernetes highload deploy. part 2

<https://habr.com/ru/companies/flant/articles/549464/>

![](/files/nlA9aQ8WKfxRoY3vr6Xi)

В [прошлой части](https://habr.com/ru/company/flant/blog/545204/) были рассмотрены рекомендации по множеству механизмов Kubernetes для оптимального деплоя высокодоступных приложений включая особенности работы планировщика, стратегии обновления, приоритеты, пробы и т.п. Во втором и заключительном материале поговорим о трёх важных оставшихся темах: PodDisruptionBudget, HorizontalPodAutoscaler, VerticalPodAutoscaler, — продолжив нумерацию из первой части.

### 9. PodDisruptionBudget

Механизм [PodDisruptionBudget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets) (PDB) — это must have для работы приложения в production-среде. Он позволяет контролировать, какое количество pod’ов приложения могут быть недоступны в момент времени. Читая первую часть статьи, можно было подумать, что мы уже ко всему подготовлены, если у приложения запущены несколько реплик и прописан `podAntiAffinity`, который не позволит pod’ам schedule’иться на один и тот же узел.

Однако может случиться ситуация, при которой из эксплуатации одновременно выйдет не один узел. Например, вы решили поменять инстансы на более мощные. Могут быть и другие причины, но сейчас это не так важно. Важно, что несколько узлов выведены из эксплуатации в один момент времени. «Это же Kubernetes! — скажете вы. — Тут всё эфемерно. Ну, переедут pod’ы на другие узлы — что такого?» Давайте разберёмся.

Предположим, приложение состоит из 3-х реплик. Нагрузка распределена равномерно по ним, а pod’ы — по узлам. Оно выдержит, если упадет одна из реплик. Но вот при падении двух реплик из трёх начнётся деградация сервиса: один pod просто не справится с нагрузкой, клиенты начнут получать 500-е ошибки. Ок, если мы подготовились и заранее прописали rate limit в контейнере c nginx (конечно, если у нас есть контейнер с nginx в pod’е…), то ошибки будут 429-е. Но это все равно деградация сервиса.

Тут нам на помощь и приходит PodDisruptionBudget. Взглянем на его манифест:

```
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: app
```

Конфиг довольно простой, и большая часть полей в нем скорее всего знакома (по аналогии с другими манифестами). Самое интересное — это `maxUnavailable`. Данное поле позволяет указать, сколько pod’ов (максимум) могут быть недоступны в момент времени. Указывать значение можно как в единицах pod’ов, так и в процентах.

Предположим, что для приложения настроен PDB. Что теперь произойдет, когда два или более узлов, на которые выкачены pod’ы приложения, начнут по какой-либо причине вытеснять *(evict)* pod’ы? PDB позволит вытеснить лишь один pod, а второй узел будет ждать, пока реплик не станет хотя бы две (из трёх). Только после этого еще одну из реплик можно вытеснить.

Есть также возможность определять и `minAvailable`. Например:

```
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  minAvailable: 80%
  selector:
    matchLabels:
      app: app
```

Так можно гарантировать, что кластер будет следить за тем, чтобы 80% реплик всегда были доступны, а вытеснять с узлов \[при такой необходимости] можно только оставшиеся 20%. Указывать это соотношение снова можно в процентах и единицах.

Есть и обратная сторона медали: у вас должно быть достаточное количество узлов, причем с учетом `podAntiAffinity`. Иначе может сложиться ситуация, что одну реплику вытеснили с узла, а вернуться она никуда не может. Результат: операция `drain` просто ждет вечность, а вы остаетесь с двумя репликами приложения… В `describe` pod’а, который висит в `Pending`, можно, конечно, найти информацию о том, что нет свободных узлов, и исправить ситуацию, но лучше до этого не доводить.

Итоговая рекомендация: **всегда настраивайте PDB для критичных компонентов вашей системы**.

### 10. HorizontalPodAutoscaler

Рассмотрим другую ситуацию: что происходит, если на приложение приходит незапланированная нагрузка, которая значительно выше той, что мы «привыкли» обрабатывать? Да, ничто не мешает вручную зайти в кластер и отмасштабировать pod’ы… но ради чего тогда мы тут все тогда собрались, если все делать руками?

На помощь приходит [HorizontalPodAutoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) (HPA). Этот механизм позволяет указать нужную метрику(и) настроить автоматический порог масштабирования pod’ов в зависимости от изменения её значений. Представьте, что вы спокойно спите, но внезапно, ночью, приходит небывалая нагрузка — скажем, заокеанские пользователи узнали про ваш сервис на Reddit. Нагрузка на CPU *(или показатель иной метрики)* у pod’ов вырастает, достигает порога… после чего HPA начинает доблестно масштабировать pod’ы, чтобы способствовать распределению нагрузки благодаря выделению новых ресурсов.

В итоге, все входящие запросы обработаны в нормальном режиме. Причем — и это важно! — как только нагрузка вернется в привычное русло, HPA отмасштабирует pod’ы обратно, тем самым снижая затраты на инфраструктуру. Звучит здорово, не так ли?

Разберемся, как именно HPA вычисляет, сколько реплик надо добавить. Вот формула из документации:

```
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]
```

Предположим:

* текущее количество реплик = 3;
* текущее значение метрики = 100;
* пороговое значение метрики = 60.

Получаем следующее выражение: *3 \* ( 100 / 60 )*, т.е. на выходе получаем «около» 5 (HPA округлит результат в б\_о\_льшую сторону). Таким образом, приложению будут добавлены еще две реплики. А значение будет по-прежнему вычисляться по формуле, чтобы, как только нагрузка снизится, уменьшилось и количество необходимых реплик для обработки этой нагрузки.

Здесь начинается самое интересное. Что же выбрать в качестве метрики? Первое, что приходит на ум, — это базовые показатели, такие как CPU, Memory… И такое решение действительно сработает, если у вас… нагрузка на CPU и Memory растет прямо пропорционально входящей нагрузке. Но что, если pod’ы обрабатывают разные запросы: одни могут потребовать много тактов процессора, другие — много памяти, а третьи — вообще укладываются в минимальные ресурсы?

Рассмотрим на примере с очередью на RabbitMQ и теми, кто эту очередь будет разбирать. Допустим, в очереди 10 сообщений. Мы видим (спасибо мониторингу!), что очередь разбирается довольно быстро. То есть для нас нормально, когда в среднем в очереди скапливается до 10 сообщений. Но вот пришла нагрузка — очередь сообщений выросла до 100. Однако нагрузка на CPU и Memory не изменится у worker’ов: они будут монотонно разбирать очередь, оставляя там уже около 80-90 сообщений.

А ведь если бы мы настроили HPA по нашей (кастомной) метрике, описывающей количество сообщений в очереди, то получили бы понятную такую картину:

* текущее количество реплик = 3;
* текущее значение метрики = 80;
* пороговое значение метрики = 15.

Т.е.: 3 \* ( 80 / 15 ) = 16. Тогда HPA начнет масштабировать worker’ы до 16 реплик, и они быстро разберут все сообщения в очереди (после чего HPA может масштабировать их вниз). Однако для этого важно, чтобы **мы были «инфраструктурно готовы» к тому, что дополнительно может развернуться еще столько pod’ов**. То есть они должны влезть на текущие узлы, или же новые узлы должны быть заказаны у поставщика инфраструктуры (облачного провайдера), если вы используете [Cluster Autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler). В общем, это очередная отсылка к планированию ресурсов кластера.

Теперь взглянем на несколько манифестов:

```
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50
```

Тут все просто. Как только pod достигает нагрузки по CPU в 50%, HPA начнет масштабировать максимум до 10.

А вот более интересный вариант:

```
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: worker
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 1
  maxReplicas: 10
 metrics:
  - type: External
    external:
      metric:
        name: queue_messages
      target:
        type: AverageValue
        averageValue: 15
```

Мы уже смотрим на [custom metrics](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-custom-metrics). Опираясь на значение `queue_messages`, HPA будет принимать решение о необходимости масштабирования. Учитывая, что для нас нормально, если в очереди около 10 сообщений, здесь выставлено среднее значение в 15 как пороговое. Так можно контролировать количество реплик уже более точно. Согласитесь, что и автомасштабирование будет куда лучше и точнее \[чем по условному CPU] в случае разбора очереди?

### Дополнительные фичи

Возможности настройки HPA уже весьма разнообразны. Например, можно комбинировать метрики. Вот что получится для размер очереди сообщений и CPU:

```
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: worker
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 1
  maxReplicas: 10
 metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  - type: External
    external:
      metric:
        name: queue_messages
      target:
        type: AverageValue
        averageValue: 15
```

Как будет считать HPA? У какой из метрик при подсчете получилось большее количество реплик, на тот он и будет опираться. Если из расчета потребления CPU выходит, что надо масштабировать до 5, а по расчетам на основании размерности очереди — до 3, то произойдет масштабирование до 5 pod’ов.

С релиза [Kubernetes 1.18](https://habr.com/ru/company/flant/blog/493284/) появилась возможность прописывать политики `scaleUp` и `scaleDown`. Например:

```
behavior:
  scaleDown:
    stabilizationWindowSeconds: 60
    policies:
    - type: Percent
      value: 5
      periodSeconds: 20
   - type: Pods
      value: 5
      periodSeconds: 60
    selectPolicy: Min
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
      value: 100
      periodSeconds: 10
```

Здесь заданы две секции: одна определяет параметры масштабирования вниз (`scaleDown`), вторая — вверх (`scaleUp`). В каждой из секций есть интересный параметр — `stabilizationWindowSeconds`. Он позволяет избавиться от «флаппинга», то есть ситуации, при которой HPA будет масштабировать то вверх, то вниз. Грубо говоря, это некий таймаут после последней операции изменения количества реплик.

Теперь о политиках, и начнем со `scaleDown`. Эта политика позволяет указать, какой процент pod’ов (`type: Percent`) можно масштабировать вниз за указанный период времени. Если мы понимаем, что нагрузка на приложение — волнообразная, что спадает она так же волнообразно, надо выставить процент поменьше, а период — побольше. Тогда при снижении нагрузки HPA не станет сразу убивать множество pod’ов по своей формуле, а будет вынужден делать это постепенно. Вдобавок, мы можем указать явное количество pod’ов (`type: Pods`), больше которого за период времени убивать никак нельзя.

Также стоит обратить внимание на параметр `selectPolicy: Min` — он указывает на необходимость исходить из политики минимального количества pod’ов. То есть: если 5 процентов меньше, чем 5 единиц pod’ов, будет выбрано это значение. А если наоборот, то убирать будем 5 pod’ов. Соответственно, выставление `selectPolicy: Max` даст обратный эффект.

Со `scaleUp` аналогичная ситуация. В большинстве случаев требуется, чтобы масштабирование вверх происходило с минимальной задержкой, поскольку это может повлиять на пользователей и их опыт взаимодействия с приложением. Поэтому `stabilizationWindowSeconds` здесь выставлен в 0. Зная, что нагрузка приходит волнами, мы позволяем HPA при необходимости поднять реплики до значения `maxReplicas`, которое определено в манифесте HPA. За это отвечает политика, позволяющая раз в `periodSeconds: 10`, поднимать до 100% реплик.

Наконец, для случаев, когда мы не хотим, чтобы HPA масштабировал вниз, если уже прошла нагрузка, можно указать:

```
behavior:
  scaleDown:
    selectPolicy: Disabled
```

Как правило, политики нужны тогда, когда у вас HPA работает не так, как вы на это рассчитываете. **Политики дают б*****о*****льшую гибкость, но усложняют восприятие манифеста**.

А в скором времени получится даже опираться на ресурсы конкретного *контейнера* в pod’е (представлено как alpha в [Kubernetes 1.20](https://habr.com/ru/company/flant/blog/530924/)).

### Итог по HPA

Закончим примером финального манифеста для HPA:

```
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: worker
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 1
  maxReplicas: 10
 metrics:
  - type: External
    external:
      metric:
        name: queue_messages
      target:
        type: AverageValue
        averageValue: 15
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 5
        periodSeconds: 20
     - type: Pods
        value: 5
        periodSeconds: 60
      selectPolicy: Min
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 10
```

***NB**. Этот пример подготовлен исключительно в ознакомительных целях. Помните, что его **необходимо** адаптировать под свои условия.*

Подведем итог по Horizontal Pod Autoscaler. **Использовать HPA для production-окружений всегда полезно. Но выбирать метрики, на которых он будет основываться, надо тщательно.** Неверно выбранная метрика или некорректный порог ее срабатывания будет приводить к тому, что либо получится перерасход по ресурсам (из-за лишних реплик), либо клиенты увидят деградацию сервиса (если реплик окажется недостаточно). Внимательно изучайте поведение вашего приложения и проверяйте его, чтобы достичь нужного баланса.

### 11. VerticalPodAutoscaler

[VPA](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) позволяет считать потребление ресурсов pod’ами и (в случае соответствующего режима работы) вносить изменения в ресурсы контроллеров — править их *requests* и *limits*.

Представим ситуацию, когда была выкачена новая версия приложения с новыми функциональными возможностями. Так случилось, что импортированная библиотека оказалось тяжелой или код местами недостаточно оптимальным, из-за чего приложение начало потреблять больше ресурсов. На тестовом контуре это изменение не проявилось, потому что обеспечить такой же уровень тестирования, как в production, сложно.

Конечно, актуальные для приложения (*до* этого обновления) requests и limits уже были выставлены. И вот теперь приложение достигает лимита по памяти, приходит мистер ООМ и совершает над pod’ом насильственные действия. VPA может это предотвратить! Если посмотреть на ситуацию в таком срезе, то, казалось бы, это замечательный инструмент, который надо использовать всегда и везде. Но в реальности, конечно, все не так просто и важно понимать сопутствующие нюансы.

Основная проблема, которая на текущий момент не решена, это изменение ресурсов через *перезапуск* pod’а. В каком-то ближайшем будущем VPA научится их патчить и без рестарта приложения, но сейчас не умеет. Допустим, это не страшно для хорошо написанного приложения, всегда готового к перекату: приложение разворачивается Deployment’ом с кучей реплик, настроенными PodAntiAffinity, PodDistruptionBudget, HorizontalPodAutoscaler… В таком случае, если VPA изменит ресурсы и аккуратно (по одному) перезапустит pod’ы, мы это переживем.

Но бывает всё не так гладко: приложение не очень хорошо переживает перезапуск, или мы ограничены в репликах по причине малого количества узов, или у нас вообще StatefulSet…. В худшем сценарии придет нагрузка, у pod’ов вырастет потребление ресурсов, HPA начал масштабировать, а тут VPA: «О! Надо бы поднять ресурсы!» — и начнет перезапускать pod’ы. Нагрузка начнет перераспределяться по оставшимся, из-за чего pod может просто упасть, нагрузка уйдет на еще живые и в результате произойдет каскадное падение.

Поэтому и важно понимать разные режимы работы VPA. Но начнем с рассмотрения самого простого — **Off**.

### Режим Off

Данный режим занимается только подсчетами потребления ресурсов pod’ами и выносит рекомендации. Забегая вперед, даже скажу, что в подавляющем большинстве случаев мы у себя используем именно этот режим, именно он и является основной рекомендацией. Но вернемся к этому вопросу после рассмотрения нескольких примеров.

Итак, вот простой манифест:

```
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Recreate"
    containerPolicies:
      - containerName: "*"
        minAllowed:
          cpu: 100m
          memory: 250Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]
        controlledValues: RequestsAndLimits
```

Подробно разбирать каждый параметр в манифесте не будем: у нас есть [перевод](https://habr.com/ru/company/flant/blog/541642/), в деталях рассказывающий о возможностях и особенностях VPA. Вкратце: здесь мы указываем, на какой контроллер нацелен наш VPA (`targetRef`) и какова политика обновления ресурсов, а также задаем нижнюю и верхнюю границы ресурсов, которыми VPA волен распоряжаться. Главное внимание — полю `updateMode`. В случае режима `Recreate` или `Auto` (пока не сделают упомянутую возможность патча) будет происходить пересоздание pod’а со всеми вытекающими последствиями, но мы этого не хотим и пока просто поменяем режим работы на `Off`:

```
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Off"   # !!!
  resourcePolicy:
    containerPolicies:
      - containerName: "*"
        controlledResources: ["cpu", "memory"]
```

Теперь VPA начнет собирать метрики. Если спустя несколько минут посмотреть на него через `describe`, появятся рекомендации:

```
  Recommendation:
    Container Recommendations:
      Container Name:  nginx
      Lower Bound:
        Cpu:     25m
        Memory:  52428800
      Target:
        Cpu:     25m
        Memory:  52428800
      Uncapped Target:
        Cpu:     25m
        Memory:  52428800
      Upper Bound:
        Cpu:     25m
        Memory:  52428800
```

Спустя пару дней (неделю, месяц, …) работы приложения рекомендации будут точнее. И тогда можно будет корректировать лимиты в манифесте приложения. Это позволит спастись от ООМ’ов в случае недостатка requests/limits и поможет сэкономить на инфраструктуре (если изначально был выделен слишком большой запас).

А теперь — к некоторым сложностям.

### Другие режимы VPA

Режим **Initial** выставляет ресурсы *только* при старте pod’а. Следовательно, если у вас неделю не было нагрузки, после чего случился выкат новой версии, VPA проставит низкие requests/limits. Если резко придет нагрузка, будут проблемы, поскольку запросы/лимиты установлены сильно ниже тех, что требуются при такой нагрузке. Этот режим может быть полезен, если у вас равномерная, линейно растущая нагрузка.

В режиме **Auto** будут пересозданы pod’ы, поэтому самое сложное, чтобы приложение корректно обрабатывало процедуру рестарта. Если же оно не готово к корректному завершению работы (т.е. с правильной обработкой завершения текущих соединений и т.п.), то вы наверняка поймаете ошибки (5XX) «на ровном месте». Использование режима Auto со StatefulSet вообще редко оправдано: только представьте, что у вас VPA решит добавить ресурсы PostgreSQL в production…

А вот в dev-окружении можно и поэкспериментировать, чтобы выяснить необходимые ресурсы для выставления их для production. Например,мы решили использовать VPA в режиме Initial, у нас есть Redis, а у него — параметр `maxmemory`. Скорее всего нам надо его изменить под свои нужды. Но загвоздка в том, что Redis’у нет дела до лимитов на уровне cgroups. Если случится так, что `maxmemory` будет равен 2GB, а в системе выставлен лимит в 1GB, то ничего хорошего не выйдет. Как же выставить `maxmemory` в то же значение, что и лимит? Выход есть! Можно пробросить значения, взяв их из VPA:

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:6.2.1
        ports:
        - containerPort: 6379
        resources:
           requests:
             memory: "100Mi"
             cpu: "256m"
           limits:
             memory: "100Mi"
             cpu: "256m"
        env:
          - name: MY_MEM_REQUEST
            valueFrom:
              resourceFieldRef:
                containerName: app
                resource: requests.memory
          - name: MY_MEM_LIMIT
            valueFrom:
              resourceFieldRef:
                containerName: app
                resource: limits.memory
```

Получив переменные окружения, можно извлечь нужный лимит по памяти и по-хорошему отнять от него условные 10%, оставив их на нужды приложения. А остальное выставить в качестве maxmemory\*\*.\*\* Вероятно, придется также придумывать что-то с init-контейнером, который sed’ает конфиг Redis, потому что базовый образ Redis не позволяет ENV-переменной пробросить maxmemory. Тем не менее, решение будет работать.

Напоследок, упомяну неприятный момент, связанный с тем, что VPA «выбрасывает» pod’ы DaemonSet сразу, скопом. Со своей стороны мы начали работу над [патчем](https://github.com/kubernetes/kubernetes/pull/98307), который исправляет эту ситуацию.

### Итоговые рекомендации по VPA

* Всем приложениям будет полезен режим *Off*.
* В dev можно экспериментировать с *Auto* и *Initial*.
* В production стоит применять только тогда, когда вы уже собрали/опробовали рекомендации и точно понимаете, что делаете и зачем.

А вот когда VPA научится патчить ресурсы без перезапуска… тогда заживем!

Важно также помнить, что существует ряд нюансов при совместном использовании HPA и VPA. Например, эти механизмы нельзя использовать совместно, опираясь на одни и те же базовые метрики (CPU или Memory), потому что при достижении порога по ним VPA начнет поднимать ресурсы, а HPA начнет добавлять реплики. Как результат, нагрузка резко упадет и процесс пойдет в обратную сторону — может возникнуть «flapping». Существующие ограничения более подробно описаны в [документации](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#known-limitations). Благодарю [@Rinck](https://habr.com/users/rinck) за это полезное дополнение!

### Заключение

На этом мы завершаем обзор лучших практик в контексте деплоя HA-приложений в Kubernetes. Будем рады любой обратной связи и пожеланиям на будущие подобные материалы.

## P.S.

Читайте также в нашем блоге:

* «[Автомасштабирование и управление ресурсами в Kubernetes](https://habr.com/ru/company/flant/blog/459326/)» (обзор и видео доклада);
* «[Вертикальное автомасштабирование pod'ов в Kubernetes: полное руководство](https://habr.com/ru/company/flant/blog/541642/)».
