Автомасштабирование в Kubernetes без приземления на реальные условия выглядит просто: раскатываете приложение, а ваш проект автоматически подстраивается под рост и спад нагрузки с помощью «магии Kubernetes». Но на деле «из коробки» это не работает. Кластер поддерживает ровно то количество реплик, которое указано в манифестах приложения, поэтому агрессивное масштабирование приложений без масштабирования кластера рано или поздно упрется в потолок. А если ваша инфраструктура не облачная, а локальная и состоит из множества разрозненных платформ виртуализации, то ресурс инженеров будет уходить на ручные операции. 

Привет, Хабр! В этой статье мы поделимся опытом инженерной команды практики виртуализации, контейнеризации и частных облаков К2Тех и поговорим о том, какие решения позволяют автоматизировать ручной труд по масштабированию в Kubernetes. Начнём рассматривать инструменты с нижних уровней, постепенно продвигаясь вверх: от уровня подов до уровня всего кластера. В итоге придем к централизованному управлению всеми рабочими нагрузками в парке кластеров, без привязки к конкретному облаку или провайдеру инфраструктуры.

С чего всё обычно начинается

Представим наш небольшой Kubernetes кластер. В нём — всего три рабочих узла в виде виртуальных машин на платформе виртуализации и несколько крайне полезных приложений, которые поддерживает наша отважная команда. Пока нагрузка была умеренной, всё работало стабильно, но когда активность работы с приложениями возросла — время ответа резко увеличилось, начались задержки и таймауты.

Надо больше реплик

Решать проблему начали с самого очевидного сценария: изменили количество реплик приложения → применили изменения → увеличили количество реплик → перераспределили нагрузку по репликам → приложение стало снова подавать признаки жизни.

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

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

Надо больше узлов

Тогда запустили действия по расширению кластера вручную: подготовили виртуальную машину → применили конфигурацию cloud-init → добавили новый узел в кластер → реплики смогли разместиться → и приложение стало снова подавать признаки жизни.

Нагрузка начала спадать, но бдительно следить за кластером и реагировать на любые всплески нагрузки довольно утомительно, а решать проблему постоянным поддержанием — неоправданно дорого. Поэтому, чтобы не тратить нервы и не заниматься оркестрацией и ресурсной эвристикой самостоятельно — настало время добавить в Kubernetes «щепотку магии».

Уровни масштабирования

В Kubernetes можно разделить масштабирование на два глобальных уровня:

  1. уровень приложений;

  2. уровень кластера.

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

Вертикальное автомасштабирование приложений

Запросы ресурсов для рабочих нагрузок по историческим причинам могут задаваться «на глаз» как с большим запасом, так и по минимуму, только с учетом своего запуска. В результате часть подов может простаивать, а другие — страдать от нехватки ресурсов. Поэтому на старте стоит разобраться, какие ресурсы необходимы приложению, и уже автоматически  настраивать их под нагрузки. И в этом может помочь Vertical Pod Autoscaler (VPA).

VPA в Kubernetes позволяет вертикально масштабировать рабочие нагрузки, увеличивая или уменьшая количество ресурсов: CPU и оперативную память.

VPA состоит из трёх компонентов:

  • Recommender — анализирует историческое потребление ресурсов на основе метрик от Metrics Server и формирует рекомендации.

  • Updater — периодически проверяет рекомендации и при необходимости пересоздаёт поды с новыми запросами ресурсов.

  • Admission Controller — перехватывает запросы на создание подов и модифицирует их спецификацию, подставляя рекомендованные значения.

VPA поддерживает несколько режимов работы:

  • Off — только формирует рекомендации, не применяя их. С него начинается безопасный путь внедрения VPA;

  • Initial — применяет рекомендации только для запускаемых подов;

  • Auto — устарел и больше не должен использоваться, заменён на Recreate;

  • Recreate — пересоздаёт поды для применения рекомендаций;

  • InPlaceOrRecreate — позволяет изменять ресурсы без пересоздания пода, если это поддерживается (в Kubernetes v1.33 вышел из Alpha в Beta, а с v1.35 считается Stable).

VPA формирует несколько базовых рекомендаций:

  • Lower bound (нижняя граница) — минимальная оценка ресурсов для контейнера, при которой приложение теоретически может работать. Такие минимальные запросы на CPU и память, скорее всего, окажут значительное влияние на производительность и доступность.

  • Upper bound (верхняя граница) — это максимальный рекомендованный объем ресурсов для контейнера, выше которого ресурсы, скорее всего, будут расходоваться впустую.

  • Target (цель) — используется для базового задания запросов на ресурсы.

  • Uncapped target (неограниченная цель) — целевая оценка без учёта ограничений, будто бы ресурсы кластера не ограничены.

Не стоит забывать, что эти границы — рекомендации, которые могут меняться с течением времени. Поэтому, чтобы разобраться, как лучше использовать VPA, достато��но запустить типовой сценарий:

  1. Включить VPA в режиме Off.

  2. Наблюдать за формируемыми рекомендациями.

  3. Скорректировать целевые запросы на рабочих нагрузках.

  4. Только после этого смотреть на режим Recreate для динамического обновления запросов приложений при постоянном изменении нагрузок.

Лучше не использовать VPA для Stateful-нагрузок в режиме Recreate — это может привести к незапланированным прерываниям работы.

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

Горизонтальное автомасштабирование приложений

Если нужно быстро увеличивать или уменьшать количество реплик, эту задачу выполняет Horizontal Pod Autoscaler (HPA). HPA не меняет запросы на ресурсы, как это делает VPA, а открывает возможности для масштабирования по горизонтали за счет изменения количества реплик.

В стандартной конфигурации HPA может ориентироваться на загрузку приложений по cpu/memory от Metric Server. Контроллер HPA периодически сравнивает текущее значение метрик с целевым и принимает решение о масштабировании. Модель простая и хорошо работает для cpu/memory bound-нагрузок, но начинает давать сбои в более сложных сценариях. Низкая загрузка этих параметров не всегда означает, что приложение не занято работой. Можно иметь низкую загрузку на процессор или оперативную память, но большую задержку из-за блокирующих асинхронных операций и ожидания ответа от внешних сервисов.

Кастомные метрики

Когда стандартные метрики не дают фактическую картину по нагрузке, выходом может стать экспорт метрик системы мониторинга Prometheus в Metric Server через prometheus-adapter (и HPA сможет их использовать). Вот некоторые примеры таких метрик:

  • RPS (количество запросов в секунду);

  • задержка ответа;

  • и любые другие метрики приложения.

Это позволяет масштабировать приложение по фактической нагрузке, а не по косвенным признакам. У HPA нет рекомендательного режима и анализа поведения метрик с течением времени. Он просто сравнивает текущее значение метрики с целевым и принимает решение только на основе этого состояния.

Чтобы подобрать целевые значения метрик и диапазон масштабирования HPA (minReplicas и maxReplicas) под реальный профиль нагрузки, можно использовать Grafana в связке с Prometheus, и, опираясь на исторические данные, определить:

  • минимальное количество реплик;

  • максимальное количество реплик;

  • целевые значения метрик;

  • параметры стабилизации масштабирования.

После этого зафиксировать параметры для HPA в своих манифестах и корректировать уже в рамках эксплуатации — со временем показатели могут меняться.

Совместное использование HPA и VPA

Совместная работа HPA и VPA позволяет гибко автоматизировать управление нагрузкой, но добавляет новый уровень сложности. 

Если HPA и VPA реагируют на одни и те же метрики, например, по потреблению CPU, возникает эффект пинг-понга: HPA увеличивает количество подов, в результате чего нагрузка на поды падает. После этого VPA снижает количество запросов, а нагрузка в процентном соотношении снова растёт.

Это не баг одного из инструментов, а проблема одновременного масштабирования разными инструментами. Чтобы избежать непредсказуемого эффекта, обычно придерживаются нескольких правил:

  • используют VPA в режимах Off или Initial;

  • разделяют метрики для HPA и VPA, например, HPA по метрикам приложения, VPA по CPU и памяти;

  • выбирают только один тип масштабирования для конкретного приложения.

Масштабирование по событиям

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

Чтобы добавить событийный подход и не строить для этого новый слой абстракции для получения метрик из внешних систем, можно использовать готовое решение — Kubernetes Event-driven Autoscaling (KEDA) — компонент для событийного масштабирования, который следит за внешними источниками сигналов и на их основе управляет масштабированием.

Технически это работает так:

  • создается объект ScaledObject (ресурс KEDA), в котором указываем, какое приложение и по каким событиям  масштабировать — например, по длине очереди сообщений, HTTP-запросам или метрикам из базы данных;

  • KEDA через scaler-модули собирает сигналы событий и превращает их в метрики;

  • метрики передаются в HPA Kubernetes, который решает, сколько реплик нужно запустить;

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

Например, кластер может быть почти полностью загружен с 06:00 до 20:00 часов по рабочим дням. Потребление ресурсов растёт с утра, достигая пика примерно к 15:00, а потом постепенно снижается почти до нуля к 20:00. Поэтому команда может останавливать рабочие нагрузки в периоды простоя, а высвободившиеся ресурсы — передавать соседней команде, которая занимается, например, расчётными задачами и постоянно нуждается в дополнительных вычислительных мощностях. Так кластер становится коммунальным, а ресурсы — перераспределяемыми во времени.

При этом KEDA не заменяет HPA, а дополняет его: поддерживает десятки готовых интеграций с внешними системами, позволяет комбинировать несколько триггеров и масштабироваться до нуля (что сильно экономит ресурсы на dev окружениях).

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

Автомасштабирование кластера

Бизнесу нужно, чтобы автомасштабирование позволяло реагировать на всплески запросов от пользователей: расти только во время пика и уменьшаться в период простоя, и тем самым экономить приятную сердцу сумму в чеке на инфраструктуре облака или провайдера. Команде — чтобы платформа с автомасштабированием вела себя предсказуемо, не провоцировала ночные внештатные ситуации по спасению продакшена, и специалистам не приходилось прибегать к ручным операциям масштабирования. Но если приложение может масштабироваться, а инфраструктура под ним — нет, то рано или поздно потребуется ручное вмешательство. 

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

Значит, нужен нативно встроенный в Kubernetes механизм масштабирования на уровне узлов.

Cluster Autoscaler

Начнём с Cluster Autoscaler — компонента Kubernetes для масштабирования кластера за счет добавления или удаления узлов. Для работы используются интеграции с внушительным списком облачных инфраструктурных провайдеров, таких как AWS, Azure, OpenStack.

Принцип работы примерно следующий: когда новые поды невозможно разместить на существующих узлах, поды переходят в состояние Pending. Cluster Autoscaler отслеживает это и понимает, что в кластере не хватает ресурсов. После этого отправляется запрос в API облачного провайдера и добавляет новый узел через механизм Auto Scaling Group (ASG) на стороне провайдера.

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

Это базовый и хорошо изученный механизм, но сильно завязанный на возможности инфраструктурного провайдера Auto Scaling Group, которые есть не везде.

Karpenter

Karpenter — альтернатива Cluster Autoscaler для автомасштабирования узлов Kubernetes — решает ту же задачу, но делает это иначе. Помимо того, что делает CA, он также подбирает размер узлов под текущую нагрузку и самостоятельно создает новые группы узлов подходящего размера, а не опирается на заранее заданные группы узлов. Если в кластере появляются поды в состоянии Pending, Karpenter рассчитывает, какой узел нужен прямо сейчас, и запрашивает его у облачного провайдера. А когда нагрузка снижается, старается уплотнить размещение подов и удалить лишние узлы, но по сравнению с Cluster Autoscaler даёт больше гибкости:

  • может «заказать» узел с нужными характеристиками, даже если такого типа изначально не было ни в одной группе узлов;

  • отслеживает configuration drift (отклонения текущего состояния инфраструктуры от заданной конфигурации) и учитывает текущее состояние инфраструктуры;

  • активно использует spot-инстансы (прерываемые виртуальные машины по сниженной цене) для снижения стоимости ресурсов.

Исторически Karpenter создавался под AWS и его сервисы. Поддержка других облачных провайдеров, например Azure, появилась позже, но большая часть продвинутого функционала всё ещё лучше раскрывается именно в AWS-среде.

Но у этих инструментов есть общий предел. Они лучше всего работают там, где инфраструктура уже умеет автоматически создавать и удалять узлы через API облака. Cluster Autoscaler и Karpenter в той или иной форме опираются на механизмы автомасштабирования самого провайдера. Для такой модели логично сразу использовать управляемый сервис Kubernetes от облачной платформы (Kubernetes as a Service), но в реальности инфраструктура редко бывает только облачной.

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

В гибридных и локальных (on-prem) средах нужна единая модель управления узлами, которая не зависит от конкретного провайдера и позволяет управлять инфраструктурой так же гибко, как и в облаке. Эту задачу в мире Kubernetes решает ClusterAPI.

ClusterAPI

ClusterAPI переносит управление инфраструктурой в привычную для Kubernetes модель: контроллеры приводят реальную инфраструктуру к состоянию, которое описывается в ресурсах Kubernetes.

С помощью ClusterAPI можно:

  • создавать абстракцию пулов узлов и разделять их на группы по характеристикам (GPU, CPU-инструкции, тип виртуализации, физические или виртуальные машины);

  • управлять машинами и их конфигурациями декларативно через CRD (Custom Resource Definition — пользовательские типы ресурсов Kubernetes);

  • интегрировать управление узлами с Cluster Autoscaler, Karpenter и другими механизмами масштабирования;

  • расширять список поддерживаемых провайдеров, включая собственные on-prem решения и экспериментальные интеграции (PoC — proof of concept).

На практике это позволяет работать с инфраструктурой почти так же, как с приложениями в кластерах. Стек масштабирования расширяется на ещё один уровень: кластер начинает управлять не только подами и узлами, но и жизненным циклом самих машин.

Многокластерные инсталляции

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

Типовая архитектура установки выглядит так:

  • bootstrap-кластер — служебный кластер для первоначальной инициализации управляющего кластера;

  • управляющий (management) кластер — управляет жизненным циклом всех workload-кластеров, хранит конфигурации и развёртывает их с помощью GitOps (подхода к управлению через декларативные конфигурации в Git);

  • workload-кластеры — исполняют пользовательские приложения и задачи.

ClusterAPI в связке с GitOps-подходом и централизованным мониторингом позволяет управлять всей системой как единым целым: сразу настраивается наблюдение за всеми разворачиваемыми кластерами, хранилище конфигураций (например, Gitea) централизует управление изменениями, а через инструменты раскатки (Argo CD, Flux или Fleet) можно автоматизировать рутину по подготовке типовых инсталляций. Механизмы KEDA, HPA, Cluster Autoscaler и Karpenter продолжают работать внутри каждого кластера, но их конфигурация централизованно управляется через единый management-кластер.

One cluster to rule them all.

k8s-scaling.jpg

Выводы

Kubernetes позволяет строить решения, независимые от конкретной облачной платформы (cloud-agnostic), но требует дисциплины и понимания взаимодействия инструментов. Пройдя весь путь от подов до многокластерных инсталляций вне облаков, можно получить неплохие результаты:

  • снижение ручных операций;

  • своевременное высвобождение полезных ресурсов (CPU, RAM, GPU);

  • предсказуемое потребление ресурсов;

  • возможность делить один кластер между командами.

Это показывает, что автомасштабирование перестаёт быть просто экономией ресурсов, а становится управлением сложностью. С автоматизацией ручных действий исчезает целый класс операционных задач: узлы без нагрузки удаляются сами, а во время простоя высвобождаются ресурсы на другие нагрузки. 

В нашем примере более половины сервисов со временем снизили свои запросы на ресурсы на 30–40%. Дорогие узлы с GPU используются только тогда, когда они действительно нужны. Команда начала тратить меньше времени на реактивные действия по управлению инфраструктурой и смогла сфокусироваться на более интересных задачах. 

Централизованный подход упростил логику управления и позволил работать со множеством кластеров как с единой системой, которая успевает за быстрым изменением нагрузки, не ломается от ручных операций и не потребляет избыточные ресурсы при каждом новом запросе.