Автомасштабирование узлов кластера Kubernetes и горизонтальное масштабирование подов позволяют быстро расширить ресурсы при пиковых нагрузках. Но сложные приложения могут не нагружать поды или узлы максимально, но требовать дополнительных ресурсов, например, для параллельной обработки нескольких объектов в очереди. Триггером масштабирования кластера может быть не утилизация, а события от внешних систем — например, очереди сообщений Kafka, системы мониторинга Prometheus или от платформы CI/CD.

Всем привет! Меня зовут Илья Смирнов, я архитектор решений в Cloud.ru. Расскажу, как лучше справляться с пиковыми нагрузками, если вы развернули свое приложение в кластере Kubernetes. Вместе запустим такое демо-приложение и посмотрим, как с ним работают классические подходы автомасштабирования — в этой части, а затем попробуем масштабировать кластер по событиям с помощью KEDA (Kubernetes-based Event Driven Autoscaler) — в следующей. Не пропустите!

Собирем демо-приложение

Я буду использовать сервис Managed Kubernetes облака Cloud.ru Evolution. Как его создать и работать с ним, можно посмотреть в документации а также в лабораторной работе.

На старте я создам кластер с одним мастером с самым маленьким флейвором и группой узлов, в которую будет входить только один узел с минимальным набором ресурсов.
Для начала развернем в кластере Kubernetes базу данных mongodb и deployment с веб-сервером flask. Если отправить GET-запрос на URL …/data, то веб-сервер вычитает все записи из mongodb и отдаст их в формате JSON.

Первым шагом развернем в кластере Kubernetes mongodb с помощью helm’а:

helm install mongodb oci://registry-1.docker.io/bitnamicharts/mongodb --set useStatefulSet=true --set auth.rootPassword=mongo

Теперь нам нужен код самого приложения. Все необходимые файлы можно скопировать с репозитория.

Сначала запустим приложение simple-app — это как раз веб-сервер с flask на борту. В корне репозитория есть скрипт build-images.sh. Он собирает и загружает все образы контейнеров, которые потребуются дальше в репозиторий сервиса Evolution Artifact Registry в облаке Cloud.ru Evolution. На этом этапе нужно создать приватный репозиторий и сгенерировать ключи для доступа к облаку. Когда все будет готово, откройте скрипт для редактирования и подставьте URI репозитория и ключи в переменные в начале скрипта:

REPO=<uri репозитория>
LOGIN=<Key ID>
PASSWORD=<Secret Key>
VERSION=<Можно не менять — это просто тег образа>

Сделаем скрипт исполняемым и запустим:

chmod +x build-images.sh
./build-images.sh

Скрипт соберет образы контейнеров и загрузит их в указанный ранее приватный репозиторий.
Чтобы запустить приложение, перейдите в папку deploy репозитория и применим манифест 01-simple-app.yaml:

kubectl apply -f 01-simple-app.yaml

Теперь будет хорошо создать пару тестовых записей в mongodb. Для этого мы положили в репозиторий манифест Job’а, который сделает несколько записей в mongodb. Применяем и этот манифест:

kubectl apply -f 02-record-to-mongo.yaml

Обратите внимание, что в спецификации контейнера внутри манифестов нет свойства imagePullSecret. Это специфика того, что я использую сервисы Cloud.ru Evolution. Когда вы создаете кластер Evolution Managed Kubernetes, в нем автоматически создается секрет cloud-registry-secret, который содержит учетные данные для доступа к репозиториям Evolution Artifact Registry в этом же тенанте.

Если мы загружаем в кластер Managed Kubernetes манифест, в котором явным образом не указан параметр imagePullSecret, то Admission controller по умолчанию добавляет в манифест секрет cloud-registry-secret. Если вы проводите тесты на других платформах или развернули Kubernetes локально, то вам нужно будет добавить секрет в кластер и добавить его в манифест.

Чтобы не публиковать приложение в интернет, мы создадим временный под с утилитой curl на борту и попробуем обратиться к нашему приложению.

Создаем под:

kubectl run -it --rm curl-pod --image=curlimages/curl -- /bin/sh

После создания пода, терминал автоматически должен открыть командную оболочку пода. Попробуйте обратиться к приложению simple-app:

curl http://simple-app-service/data

Чтобы выйти из пода — введите команду exit. После выхода из интерактивного режима под автоматически удалится.

Нагрузим приложение

Итак, у нас есть подопытное приложение. Попробуем перегрузить его по CPU. Для этого используем инструмент k6.

Перейдем в папку loader в репозитории и создадим ConfigMap с конфигурацией k6 и манифест самого deployment’а k6:

kubectl apply -f k6-test.yaml

Традиционные подходы к автомасштабированию

Чтобы наше приложение опять могло работать с нагрузкой, его нужно горизонтально масштабировать — создать новые поды. Тогда ClusterIP начнет распределять нагрузку между несколькими подами и они «будут успевать» отвечать и на наши запросы, а не только запросы k6.

Ручные методы масштабирования, естественно, для этой задачи не подходят. Мы же не будем держать армию инженеров, чтобы отслеживать загрузку подов и масштабировать их ? Хочется, чтобы новые поды создавались автоматически и также автоматически удалялись, если нагрузка уменьшилась.

Способ 1. Horizontal Pod Autoscaler (HPA)

Попробуем использовать встроенный ресурс Kubernetes — Horizontal Pod Autoscaler (HPA). Вот манифест HPA:

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

Теперь разберемся в основных настройках.

scaleTargetRef — это ссылка на ресурс, который HPA будет масштабировать. В качестве kind можно указать Deployment, ReplicaSet, StatefulSet или какой-либо кастомный ресурс, если он реализует подресурс /scale.

Controller Manager будет запрашивать метрики для подов указанного ресурса. Сначала Controller Manager ищет ресурс, который вы указали в scaleTargetRef, затем выбирает поды на основе меток, указанных в манифесте ресурса, а затем запрашивает метрики для этих подов через resource metrics API или custom metrics API. Какие именно метрики запрашивать, нужно указать в манифесте. По умолчанию Controll Manager собирает эти метрики каждые 15 секунд, но это время можно изменить — для этого измените параметр --horizontal-pod-autoscaler-sync-period в настройках kube-controller-manager.

Horizontal Pod Autoscaler работает, если есть плагин Metrics Server. Metrics Server собирает все метрики по утилизации на каждом узле, а HPA забирает метрики подов, за которыми он следит.

minReplicas / maxReplicas — это минимальное и максимально количество подов до которого можно масштабироваться вниз и вверх соответственно.

Про метрики поговорим немного подробнее. В качестве type вы можете указать Resource или ContainerResource. Если вы укажете Resource, то Controller Manager будет собирать метрики с подов, а если укажете ContainerResource, то HPA будет отслеживать не весь под целиком, а только определенный контейнер или контейнеры.

В качестве ресурса, на базе метрик потребления которого HPA будет масштабировать ваше Deployment или другой ресурс, вы можете выбрать cpu, memory или custom. C cpu и memory, думаю, все понятно, а custom мы оставим за пределами этой статьи, т. к. настроить их достаточно проблематично.

А в качестве target вы можете взять Utilization, AverageValue или Value. Utilization предполагает среднее потребление среди всех подов, выраженное в процентах. Если вы используете Utilization, то важно понимать, что HPA для получения процентов будет делить текущее значение метрики на значение requests соответствующей метрики в манифесте масштабируемого ресурса. Если вы не указали request, то HPA не сможет посчитать утилизацию и ничего не будет масштабировать.

AverageValue отображает среднее потребление среди всех подов в «сыром» виде. Value отображает общее потребление среди всех подов в «сыром» виде. Если разобрать на базовом уровне, как HPA считает количество подов, которые нужно создать или удалить, то контроллер HPA использует следующую формулу:

Если, например, вы указали желаемую утилизацию 50%, а текущая равна 100%, то контроллер HPA удвоит количество подов. При этом HPA учитывает только те поды, которые находятся в состоянии Ready.

Прежде чем углубиться в настройки манифеста HPA, настроим масштабирование. Вернемся в директорию deploy и применим манифест HPA для simple-app:

cd ../deploy
kubectl apply -f 03-simple-app-hpa.yaml

Теперь посмотрим на ресурсы HPA в кластере. Вы можете выполнить команду kubectl get hpa. Я для большей наглядности буду использовать k9s.

Столбец Targets отображает значения текущего потребления и желаемого потребления. Когда текущее потребление превышает желаемое, контроллер HPA создает новые поды в соответствии с алгоритмом, который мы обсуждали выше. Но опять же, если текущее потребление ниже желаемого, то HPA будет масштабировать нагрузку вниз и удалять лишние поды.

Теперь снова создадим под с curl и проверим доступность нашего мини-приложения.

Тонкая настройка HPA — блок манифеста behavior

Теперь наше приложение автоматически масштабируется при увеличении или уменьшении нагрузки. Но что, если бы мы хотели масштабировать быстрее, чем это предполагает алгоритм масштабирования? Или, наоборот, мы хотим ограничить скорость масштабирования, чтобы не создавать излишнюю нагрузку на кластер?

Возможно, мы развернули сервис, который долго устанавливает соединение и в этом случае мы не хотим при масштабировании вниз удалять слишком много подов. Можем ли мы что-то сделать в подобных ситуациях? На самом деле можно включить в манифест HPA блок behavior, где масштабирование настраивается более тонко. Можно добавить политики:

  • масштабирования вверх — scaleUp;

  • масштабирования вниз — scaleDown.

Пример политики scaleDown:

behavior:
  scaleDown:
	policies:
	- type: Pods
  	value: 4
  	periodSeconds: 60
	- type: Percent
  	value: 10
  	periodSeconds: 60

При масштабировании вниз в течении минуты эта политика разрешает:

  • удалить максимум четыре пода,

  • удалить максимум 10% от текущего количество подов.

По умолчанию из этих двух политик применится та, которая в конкретной ситуации позволит большее количество изменений.

Рекомендую покопаться в доступных настройках behavior. Помимо политик масштабирования вы также можете, например, настроить стабилизационное окно. Оно может потребоваться, если метрики часто колеблются около настроенного порога срабатывания. Пример:

behavior:
  scaleDown:
	stabilizationWindowSeconds: 300

При такой настройке HPA будет принимать решение о масштабировании вниз учитывая все желаемые состояния за пять минут и выберет состояние с максимальным количеством требуемых подов.

Способ 2. Vertical Pod Autoscaler (VPA)

Не все деплойменты можно масштабировать горизонтально (например, поды Wordpress). Для подов подобных приложений мы можем увеличить запросы и лимиты ресурсов с помощью вертикального масштабирования. При вертикальном масштабировании велика вероятность, что под не уберется на узел.

В рамках этой статьи обсуждаем подходы к горизонтальному масштабированию, поэтому погружаться в этот метод не будет. Если интересно, смотрите инструкцию, как создавать деплоймент с вертикальным масштабированием в облаке Cloud.ru Evolution.

Способ 3. Автомасштабирование рабочих узлов кластера

Представим, что нагрузка продолжает повышаться и HPA продолжает создавать новые поды. В какой-то момент ресурсы воркера, на котором создаются новые поды, закончатся. Тогда все новые поды, которые захочет создать HPA, будут просто висеть со статусом Pending. В этом случае мы можем включить опцию Автомасштабирование для Группы узлов кластера Evolution Managed Kubernetes.

Автомасштабирование построено на базе Cluster Autoscaler. Если рабочие узлы в кластере не могут обеспечить достаточное количество ресурсов для запуска новых подов, функция автоматического масштабирования кластера добавляет новые узлы. Количество узлов не может увеличиваться выше указанного максимального количества.

При автомасштабировании в Evolution Managed Kubernetes происходит удаление узла, если выполняются условия:

  • использование ресурсов узла ниже порогового значения в 50% в течение 10 минут;

  • все поды могут быть перемещены на другой узел;

  • не достигнут порог минимального числа узлов.

Когда традиционные подходы не работают?

Для наглядности сделаем приложение немного сложнее и разобьем его на две части. Запустим два deployment — complex-app и processor, а также установим rabbitmq.

Начнем с rabbitmq:

helm install rabbitmq oci://registry-1.docker.io/bitnamicharts/rabbitmq --set auth.username=user --set auth.password=P@ssw0rd

Теперь развернем приложение complex-app, сервис ClusterIP для него и приложение processor. Для этого перейдите в папку deploy репозитория и примените манифесты 05-complex-app.yaml и 06-processor.yaml:

kubectl apply -f 05-compex-app.yaml
kubectl apply -f 06-processor.yaml

Что теперь делает приложение?

При GET-запросе URL …/data, приложение читает все записи из mongodb и отдает их.

А вот на POST-запрос на URL send?name=<some_name>&content=<some_content>" complex-app создаст JSON, в который вставит содержимое параметров name и content. Этот JSON complex-app отправить в очередь rabbitmq. Processor слушает rabbitmq. Когда появляется новое событие в очереди — processor его скачивает и записывает в mongodb.

Таким образом мы получаем эмуляцию event-driven архитектуры. Например, мы можем представить, что у нас не приложение, написанное для мини-демо на коленке, а видео-портал. Мы загружаем видео, а внутри кластера при этом один контейнер загружает видео в бакет S3 и отбивает об этом в rabbitmq. Rabbitmq постоянно слушает обработчик видео, который должен обработать ролик в соответствии с требованиями платформы. Как только сообщение прилетает, то обработчик скачивает видео, обрабатывает его и выкладывает в другой бакет, с которого его уже будут просматривать пользователи платформы. Кстати, мы делали похожее демо на DevOpsConf. В нашем мини-приложении такую обработку эмулирует специально вставленное ожидание в 20 секунд.

И вот теперь давайте представим, что мы хотим масштабировать наше приложении в случае, если в параллель несколько пользователей отправят сообщение в complex-app. Мы хотим, чтобы на каждое сообщение запускался отдельный под processor. Но вся проблема в том, что это очень сложно сделать с HPA. Мы просто не сможем четко подобрать метрику, чтобы масштабировать processor.

Более того, если вернуться к кейсу с видео-платформой, то мы видим — если бы мы вдруг как-то и смогли подобрать такую метрику, то столкнулись бы с проблемой масштабирования вниз. Представьте, что мы загружаем огромные видео, обработка которых занимает часы. В таком случае, когда какой-то из подов закончит обработку раньше, HPA решит масштабировать deployment вниз. Но HPA не знает в каком из подов закончилась обработка видео и может «убить» какой-то из еще работающих подов.

В рамках собственного эксперимента вы можете попробовать создать HPA для processor и отправить в него подряд пять разных записей. Если после этого вы сразу сделаете запрос /data, то увидите, что еще не все записи созданы и при этом HPA не создаст новых подов, т. к. нагрузка не выросла. Processor просто обрабатывает сообщение за сообщением из очереди.

А вот как быть с масштабированием по событию мы обсудим во второй части статьи ? А пока приглашаю вас на трек «Dev Platform Services» на конференции GoCloud Tech, где я буду рассказывать про мультикластерное автомасштабирование и отказоустойчивость в Kubernetes.

Комментарии (0)