Перейти к содержимому
Hogin Hogin
Назад

Karpenter: ноды по требованию и счёт за кластер вдвое меньше

8 мин чтения

Ночью нагрузка падает, а счёт за кластер — нет: node group осталась того же размера, что и днём. Cluster Autoscaler умеет только добавлять и убирать целые группы заранее описанных инстансов, поэтому большая часть кластера почти всегда простаивает «про запас». Karpenter решает задачу иначе: смотрит на конкретные поды, которые не может подобрать под своё место, сам выбирает подходящий по цене и размеру инстанс и запускает ноду за секунды. А как только под уезжает — сжимает кластер обратно. Разберёмся, как это устроено и почему это обычно первый рычаг, который двигают, когда нужно срезать счёт за Kubernetes.

Содержание

Открыть содержание

Почему 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 — и решает: какой конкретно инстанс из всего доступного в облаке каталога закроет эту потребность дешевле всего.

Cluster Autoscaler масштабирует node group целиком, 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, который постоянно ищет два случая:

До consolidation: поды размазаны по недогруженным нодам. После: та же нагрузка на меньшем числе плотно упакованных нод

Управляется это полем consolidationPolicy в NodePool:

Spot, on-demand и что происходит при вытеснении

Karpenter умеет смешивать capacity-типы прямо в одном NodePool через requirements на karpenter.sh/capacity-type: [spot, on-demand]. При выборе инстанса он в первую очередь ищет spot нужного профиля, а при отсутствии свободной spot-ёмкости — берёт on-demand, без ручного вмешательства.

Отдельно Karpenter следит за spot interruption notice — двухминутным предупреждением от облака о том, что конкретный spot-инстанс будет отозван. По этому сигналу он заранее начинает готовить замену и аккуратно выселяет поды, а не ждёт, пока нода исчезнет сама и поды пересоздадутся уже в Pending.

Один NodePool сам выбирает между spot и on-demand и реагирует на вытеснение заранее

Ловушки, которые портят картину

Сравнение в одной таблице

Cluster AutoscalerKarpenter
Единица масштабирования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

Три детали, которые экономят время на отладке:

Как проверить, что работает

Запустите под с заведомо небольшими 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 могла делать свою работу, а не блокировалась на первом же поде с неправильной аннотацией.


Поделиться:

Следующая статья
External Secrets Operator: секреты из Vault в Kubernetes без копипасты