Ночью нагрузка падает, а счёт за кластер — нет: node group осталась того же размера, что и днём. Cluster Autoscaler умеет только добавлять и убирать целые группы заранее описанных инстансов, поэтому большая часть кластера почти всегда простаивает «про запас». Karpenter решает задачу иначе: смотрит на конкретные поды, которые не может подобрать под своё место, сам выбирает подходящий по цене и размеру инстанс и запускает ноду за секунды. А как только под уезжает — сжимает кластер обратно. Разберёмся, как это устроено и почему это обычно первый рычаг, который двигают, когда нужно срезать счёт за Kubernetes.
Содержание
Открыть содержание
- Почему Cluster Autoscaler упирается в потолок
- Почему это стало актуально именно сейчас
- Что Karpenter делает по-другому
- NodePool и NodeClass: что вы описываете
- Consolidation: как кластер сам ужимается
- Spot, on-demand и что происходит при вытеснении
- Ловушки, которые портят картину
- Сравнение в одной таблице
- Что нужно, чтобы включить это в кластере
- Как проверить, что работает
- Итог
Почему Cluster Autoscaler упирается в потолок
Cluster Autoscaler (CA) появился, когда единственным способом описать масштабирование были node groups — фиксированные наборы одинаковых инстансов, привязанные к Auto Scaling Group или её аналогу. CA умеет ровно одно: увеличивать или уменьшать размер уже существующих групп, когда поды не помещаются или ноды простаивают.
Проблема в грануляции. Если группа описана как m5.xlarge, CA добавит ещё один m5.xlarge, даже если поду, который завис в Pending, нужно 200 МБ памяти. Инженеры реагируют на это заранее заводя десяток разных групп под разные профили нагрузки — и получают вторую работу: поддерживать эти группы, следить, чтобы в них не кончились нужные зоны доступности, и вручную решать, где выгоднее взять spot, а где on-demand.
Второе слабое место — скорость реакции. CA проверяет очередь Pending-подов с интервалом (по умолчанию раз в 10 секунд), но реальное время до готовой ноды складывается из времени на масштабирование ASG, загрузки образа и старта kubelet — обычно это полторы-три минуты. Для автоскейлинга, который должен подхватывать всплеск трафика, это ощутимая задержка.
Почему это стало актуально именно сейчас
Долгое время Karpenter воспринимался как «ещё один AWS-специфичный автоскейлер» — экспериментальный проект под конкретное облако. Ситуация поменялась: проект дошёл до v1 со стабильным API (можно опираться на манифесты без страха, что поля переименуют в следующем релизе — см. karpenter.sh), а сообщество вынесло его ядро за пределы AWS — появились провайдеры для Azure и GCP. Одновременно давление FinOps на бюджеты Kubernetes только растёт: чем больше команд переезжает в managed-кластеры, тем заметнее становится разница между «оплачено» и «реально используется». Karpenter — редкий случай, когда оптимизация не требует трогать приложение вообще: меняется только то, как выделяются ноды под уже существующую нагрузку.
Что Karpenter делает по-другому
Karpenter выкидывает node groups как промежуточную абстракцию. Вместо «увеличь вот эту группу» он смотрит напрямую на под, который не может быть запланирован, — со всеми его requests, node affinity, taints и topology constraints — и решает: какой конкретно инстанс из всего доступного в облаке каталога закроет эту потребность дешевле всего.
Ключевые отличия:
- Bin-packing по факту, а не по шаблону. Karpenter перебирает десятки типов инстансов и берёт тот, что точнее всего закрывает суммарные requests зависших подов, — без ручного набора «профилей» под каждый случай.
- Без предварительно описанных групп. Не нужно заранее заводить ASG под каждую комбинацию размера, зоны и ценового класса — Karpenter выбирает из всего, что разрешено в
NodeClass. - Секунды, а не минуты. Karpenter сам вызывает cloud API для запуска инстанса, минуя автоскейлинг-группу как посредника, и параллелит выбор инстанса с провижининг — типичное время до
Ready-ноды в разы меньше, чем у CA. - Consolidation по умолчанию. CA убирает пустые ноды; Karpenter ещё и переупаковывает недогруженные ноды, замещая несколько дешёвых мест одной более компактной нодой — об этом дальше.
NodePool и NodeClass: что вы описываете
Вместо node group Karpenter вводит два ресурса, между которыми чётко разделена ответственность.
NodePool — Kubernetes-часть: какие поды этот пул обслуживает (через requirements — архитектура, capacity-type, зона), лимиты по суммарным CPU/памяти пула и политика disruption (когда и как Karpenter может трогать уже запущенные ноды).
EC2NodeClass (или аналог для другого облака) — облачная часть: AMI, subnet-и, security groups, размер диска, IAM-роль ноды. NodePool ссылается на NodeClass через nodeClassRef.
Разделение полезно практически: одну и ту же NodeClass (одинаковый AMI, диск, роль) можно переиспользовать в нескольких NodePool с разными требованиями — например, отдельный пул под GPU-нагрузку и отдельный под общие сервисы, но с общей базовой конфигурацией облачной части.
Consolidation: как кластер сам ужимается
Это то, что реально бьёт по счёту. У Karpenter есть контроллер disruption, который постоянно ищет два случая:
- Пустая нода — на ней не осталось ни одного пода (кроме DaemonSet) — удаляется почти сразу.
- Недогруженная нода — под можно переселить так, что несколько недозагруженных нод схлопнутся в меньшее число более плотно упакованных. Karpenter планирует замену, дожидается, пока новые ноды готовы и поды на них переехали (с уважением к
PodDisruptionBudget), и только потом гасит старые.
Управляется это полем consolidationPolicy в NodePool:
WhenEmptyOrUnderutilized— агрессивный режим по умолчанию: Karpenter и убирает пустые ноды, и переупаковывает недогруженные. Это и есть источник основной экономии.WhenEmpty— консервативнее: трогает только полностью пустые ноды, недогруженные оставляет в покое. Выбирают, когда частые переселения подов создают больше шума, чем экономии (например, для stateful-нагрузки с долгим прогревом).
Spot, on-demand и что происходит при вытеснении
Karpenter умеет смешивать capacity-типы прямо в одном NodePool через requirements на karpenter.sh/capacity-type: [spot, on-demand]. При выборе инстанса он в первую очередь ищет spot нужного профиля, а при отсутствии свободной spot-ёмкости — берёт on-demand, без ручного вмешательства.
Отдельно Karpenter следит за spot interruption notice — двухминутным предупреждением от облака о том, что конкретный spot-инстанс будет отозван. По этому сигналу он заранее начинает готовить замену и аккуратно выселяет поды, а не ждёт, пока нода исчезнет сама и поды пересоздадутся уже в Pending.
Ловушки, которые портят картину
- PodDisruptionBudget мешает consolidation. Если PDB описан слишком строго (например,
minAvailableравен числу реплик), Karpenter не сможет безопасно вытеснить под ни при каких обстоятельствах — недогруженные ноды перестанут схлопываться, а экономия не наступит. Проверяйте PDB на реалистичность, а не копируйте «на всякий случай». karpenter.sh/do-not-disrupt: "true"на поде — честный способ сказать «эту ноду не трогать» (для батчевых джобов, локальных PV, чувствительных к рестарту процессов). Но аннотация, развешанная по привычке на всё подряд, тихо отключает consolidation для половины кластера — и именно этим объясняется «Karpenter стоит, а счёт не падает».- Некорректные requests. Karpenter планирует по заявленным
requests, а не по фактическому потреблению. Поды без limits/requests или с заведомо завышенными requests «для запаса» ломают bin-packing на входе — Karpenter честно подбирает инстанс под то, что попросили, даже если это в разы больше реального использования.
Сравнение в одной таблице
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| Единица масштабирования | node group целиком | отдельный инстанс под конкретные поды |
| Настройка под нагрузку | вручную заводить группы под профили | requirements в NodePool, без предзаданных групп |
| Время до готовой ноды | ~1.5–3 минуты | секунды |
| Уплотнение простаивающих нод | только пустые ноды | пустые + недогруженные (consolidationPolicy) |
| Spot/on-demand fallback | отдельные группы под каждый тип | один NodePool, выбор автоматический |
| Поддержка вне AWS | зависит от облачного провайдера | AWS, Azure, GCP, bare-metal (через провайдеры) |
Что нужно, чтобы включить это в кластере
Минимум — установленный контроллер Karpenter (Helm-чарт из официального репозитория) с IAM-правами на запуск/остановку инстансов, плюс один NodeClass и один NodePool. Вот рабочий пример на AWS с лимитами и агрессивной consolidation:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
role: karpenter-node-role
subnetSelectorTerms:
- tags: { karpenter.sh/discovery: my-cluster }
securityGroupSelectorTerms:
- tags: { karpenter.sh/discovery: my-cluster }
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
limits:
cpu: "1000"
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
Три детали, которые экономят время на отладке:
limitsвNodePool— это потолок для всего пула, а не для одной ноды. Без него ошибка в манифесте пода (например, случайно завышенныеreplicas) может раскрутить пул на неожиданную сумму — ставьте лимит осознанно с первого дня.consolidateAfter— пауза перед тем, как Karpenter начнёт переупаковывать недогруженную ноду. Значение по умолчанию бережёт от «дребезга» на резко колеблющейся нагрузке; для стабильных кластеров его можно уменьшать.subnetSelectorTerms/securityGroupSelectorTermsпо тегам, а не по ID — тогдаNodeClassне придётся менять при пересоздании сети или добавлении новой зоны доступности.
Как проверить, что работает
Запустите под с заведомо небольшими requests и посмотрите, что Karpenter поднимет для него отдельную ноду за секунды:
kubectl run test-pod --image=nginx --requests='cpu=100m,memory=128Mi'
kubectl get events --field-selector reason=Nominated -w
Проверьте consolidation — после удаления нагрузки посмотрите, что число нод уменьшается без ручного вмешательства:
kubectl get nodes -l karpenter.sh/nodepool=default -w
kubectl logs -n kube-system deployment/karpenter -f | grep -i consolidat
Если ноды не схлопываются, первым делом проверьте PDB и аннотацию do-not-disrupt на подах — в девяти случаях из десяти зависшая consolidation объясняется одним из них.
Итог
Автоскейлинг на уровне отдельной ноды по факту нагрузки — самая быстрая и наименее болезненная для разработчиков FinOps-оптимизация в Kubernetes: не нужно трогать приложение, ничего не переписывать, только правильно настроенные NodePool и NodeClass. Основная экономия приходит не от того, что Karpenter быстрее добавляет ноды, а от того, что он не стесняется их убирать и переупаковывать, как только нагрузка спадает. Цена входа — разобраться с PDB и requests так, чтобы consolidation могла делать свою работу, а не блокировалась на первом же поде с неправильной аннотацией.