Введение
Многие из нас знакомы с концепцией Requests и Limits в Kubernetes и понимают, что уменьшение завышенных resource requests позволяет сократить расточительное потребление ресурсов в кластере. В случае GKE Autopilot и EKS Fargate это действительно так: поскольку счёт выставляется именно за запрошенные ресурсы, снижение requests даёт немедленную экономию.
Однако в большинстве управляемых Kubernetes-сред (hosted Kubernetes environments) вы платите не за requests, а за инфраструктуру узлов. Предполагается, что, уменьшив неэффективные resource requests, мы тем самым «подтолкнём» автоскейлер узлов — как правило, Karpenter или Cluster Autoscaler — к масштабированию вниз. Но на практике так происходит далеко не всегда.
Не понимая принципов работы автоскейлеров узлов, вы рискуете потратить время на оптимизацию requests и limits, так и не увидев никакого уменьшения инфраструктуры — или увидев лишь минимальный эффект. В этой статье рассматриваются многочисленные сценарии, влияющие на размер вашей инфраструктуры узлов, и объясняется, как избежать блокировщиков масштабирования вниз (scale-down blockers), способных обойтись компании в миллионы.
Для целей этой статьи термин «автоскейлеры узлов» (node autoscalers) обозначает Karpenter и Cluster Autoscaler.
Блокировщики масштабирования вниз
Описанные ниже сценарии приводят к тому, что узел, на котором выполняется рабочая нагрузка, игнорируется автоскейлером при принятии решений о консолидации (node consolidation) или масштабировании вниз. Такой узел фактически «замораживается» и не может быть удалён. Если подобных узлов в кластере один-два, ущерб невелик, но когда их десятки или сотни, вы переплачиваете за огромный объём недозагруженной инфраструктуры.
Неуправляемые поды
Автоскейлеры узлов не будут пытаться масштабировать вниз узлы, на которых запущены «голые» (Naked), автономные, статические или неуправляемые поды (unmanaged pods). Примеры таких подов:
-
Поды, развёрнутые из манифеста с
Kind: Pod -
Поды, запущенные командой
kubectl run -
Поды, созданные путём помещения манифеста в папку
/etc/kubernetes/manifests/на узле
Теоретически, если в кластере есть 5 статических подов, каждый на отдельном узле, Kubernetes никогда не сможет уменьшить кластер до менее чем 5 узлов — какой бы низкой ни была их утилизация. По этой и другим причинам следует избегать развёртывания неуправляемых подов в Kubernetes.
Хранилище hostPath и emptyDir
Поды, использующие тома hostPath, оказываются «привязаны» к узлу, потому что используемые ими данные существуют только на этом узле. Автоскейлеры избегают масштабировать вниз любые узлы с такими томами, так как это, скорее всего, приведёт к потере данных.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-hostpath
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: local-data
mountPath: /usr/share/nginx/html
volumes:
- name: local-data
hostPath:
path: /data/nginx
type: DirectoryOrCreate
Аналогично, автоскейлеры, как правило, не масштабируют вниз узлы с подами, использующими хранилище emptyDir: оно тоже привязано к узлу, на котором работает под, и его остановка, вероятно, приведёт к потере данных.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-emptydir
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: scratch-space
mountPath: /tmp/cache
volumes:
- name: scratch-space
emptyDir: {}
Локальное хранилище
Если кластер использует провайдер хранилища Rancher local path (или аналогичный) для постоянного хранения данных, создаваемый PV будет привязан к узлу, на котором он был создан, через nodeAffinity. Пока поды, использующие этот PV, работают, узел, на котором был создан PV, не может быть масштабирован вниз.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-local
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: storage
mountPath: /data
volumes:
- name: storage
persistentVolumeClaim:
claimName: nginx-pvc
Аннотации «не выселять»
Если под имеет аннотацию "cluster-autoscaler.kubernetes.io/safe-to-evict": "false" (для Cluster Autoscaler) или "karpenter.sh/do-not-disrupt": "true" (для Karpenter), он никогда не будет выселен (evicted) в рамках консолидации узлов. Это не позволяет узлу, на котором он работает, когда-либо масштабироваться вниз.
apiVersion: v1
kind: Pod
metadata:
name: nginx-protected
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
karpenter.sh/do-not-disrupt: "true"
spec:
containers:
- name: nginx
image: nginx
Поды в состоянии Terminating или Unknown, зависшие финализаторы
Когда автоскейлер рассматривает узел как кандидат на масштабирование вниз, он должен убедиться, что все рабочие нагрузки можно безопасно выселить или что они уже завершились. Под, застрявший в состоянии Terminating или Unknown, будет блокировать масштабирование вниз узла, на котором он находится.
Распространённый пример такого сценария — поды с зависшими финализаторами (stuck finalizers). Под может бесконечно оставаться в состоянии Terminating, причём зачастую незаметно для вас. Это способно заблокировать узел от масштабирования вниз на неопределённый срок. Для выявления подов в таком состоянии воспользуйтесь командой:
kubectl get pods -A | grep -E 'Terminating|Unknown'
Деплойменты на пороге PDB
Представьте ситуацию: у вас есть деплоймент из 3 реплик и Pod Disruption Budget (PDB), требующий, чтобы все 3 копии рабочей нагрузки работали одновременно. Это фактически делает все поды невыселяемыми — перезапуск даже одного из них нарушит PDB. Поскольку поды нельзя выселить, узлы, на которых они работают, никогда не смогут масштабироваться вниз.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: nginx-pdb
spec:
minAvailable: 3
selector:
matchLabels:
app: nginx
Разрастание узлов
Следующие сценарии не блокируют масштабирование вниз отдельного узла напрямую, но либо увеличивают общее число узлов в кластере или группе узлов (node group), либо не дают ему опуститься ниже определённого уровня. Когда такие узлы оказываются недозагружены, это называют разрастанием узлов (node sprawl).
Нехватка дискового пространства
Когда корневая или образная файловая система узла превышает порог высокой утилизации (по умолчанию — 85%), kubelet устанавливает состояние DiskPressure. После этого узел автоматически получает taint node.kubernetes.io/disk-pressure:NoSchedule. Планировщик Kubernetes пропускает любой узел с этим taint при размещении новых подов, что нередко приводит к необходимости масштабировать новые узлы.
Превышение максимального числа подов на узле
Когда узел достигает предела максимального числа подов (Max Pods per Node), он больше не может принимать новые поды — сколько бы ресурсов на нём ни оставалось. Тогда масштабируется новый узел, а существующие оказываются недозагружены. Следующая команда выводит список всех узлов кластера с указанием Max Pods per Node:
kubectl get nodes -o custom-columns="NODE:.metadata.name,MAX_PODS:.status.allocatable.pods"
Сопоставьте результат с выводом следующей команды, показывающей количество подов на каждом узле:
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c
Если на каком-либо узле вы вплотную приближаетесь к максимуму, рассмотрите возможность увеличения Max Pods per Node на этом или на всех узлах, либо равномерно распределите небольшие поды по нескольким узлам, а не позволяйте им концентрироваться на одном.
Хотя подробный разбор этой темы выходит за рамки статьи, достижение узлом лимита Max Pods per Node является формой исчерпания IP-адресов (IP exhaustion) и может быть устранено путём увеличения размера маски Node CIDR.
Ограничения топологического распределения
Ограничения топологического распределения подов (Pod topology spread constraints) используются для снижения задержек и повышения отказоустойчивости: они вынуждают поды распределяться по нескольким физическим регионам.
Возьмём для примера манифест ниже. Он создаёт деплоймент из 3 реплик, каждая из которых будет запланирована в отдельном регионе. Это означает, что деплойменту всегда потребуется 3 узла в разных регионах — разместить все реплики на одном узле невозможно.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-topology
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/region
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: nginx
containers:
- name: nginx
image: nginx
В большинстве случаев это осознанное архитектурное решение, при котором снижение задержки и отказоустойчивость достигаются ценой дополнительных расходов.
Запросы эфемерного хранилища
Контейнеры могут запрашивать эфемерное хранилище (ephemeral storage) примерно так же, как память и CPU. Если на узле недостаточно свободного места для удовлетворения запросов на эфемерное хранилище, он будет исключён из выборки при планировании. Это может вызвать масштабирование дополнительных узлов для удовлетворения подобных запросов — даже если ресурсов CPU и памяти вполне хватает.
apiVersion: apps/v1
kind: Deployment
metadata:
name: storage-example
spec:
replicas: 1
selector:
matchLabels:
app: storage-app
template:
metadata:
labels:
app: storage-app
spec:
containers:
- name: app-container
image: nginx
resources:
requests:
ephemeral-storage: "2Gi"
limits:
ephemeral-storage: "4Gi"
Ситуация ничем принципиально не отличается от необходимости масштабировать узел из-за нехватки CPU или памяти, однако эфемерное хранилище нередко упускается из виду как конечный ресурс, которым нужно управлять.
Affinity и Anti-affinity узлов, Node Selectors
Node affinity (привязка к узлу) — это правило «притяжения»: оно говорит планировщику «размести этот под на узле, соответствующем указанным критериям». Node anti-affinity, напротив, — правило «отталкивания»: оно явно запрещает размещение пода на узле, уже удовлетворяющем этим критериям.
Ограничения могут быть мягкими (constraint начинается с preferred…) или жёсткими (constraint начинается с required…). При использовании жёстких ограничений node affinity планировщик откажется размещать под, если ни один узел не удовлетворяет условию. Вместо этого под перейдёт в статус Pending, автоскейлер обнаружит его и попытается масштабировать узел, отвечающий ограничениям планирования.
Ниже приведён пример манифеста деплоймента с жёстким node anti-affinity. Если в кластере не окажется узла без метки diskType: hdd, потребуется масштабировать новый узел.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-server
template:
metadata:
labels:
app: web-server
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: NotIn
values:
- hdd
containers:
- name: nginx
image: nginx
nodeSelector в спецификации пода похож на node affinity, но проще: он сопоставляет только метки узлов, и все ограничения всегда являются жёсткими.
Pod Anti-affinity
Правила pod anti-affinity говорят планировщику Kubernetes: «не размещай этот под на узле, где уже работают поды, соответствующие указанным критериям». Как и в случае node anti-affinity, ограничения могут быть мягкими (preferred…) или жёсткими (required…).
При использовании жёстких ограничений anti-affinity планировщик откажется размещать под, если ни один узел не удовлетворяет условию. Под перейдёт в Pending, автоскейлер обнаружит его и попытается масштабировать подходящий узел.
Ниже — пример манифеста деплоймента с 3 репликами и жёстким правилом anti-affinity. Правило означает: «не размещать ни один из подов этого деплоймента на одном узле с другим». В результате деплойменту потребуется как минимум 3 уже существующих узла (или 3 новых) для выполнения правил размещения:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web-server
template:
metadata:
labels:
app: web-server
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-server
topologyKey: "kubernetes.io/hostname"
containers:
- name: nginx
image: nginx
Pod Affinity
Правила pod affinity говорят планировщику Kubernetes: «старайся размещать эти рабочие нагрузки вместе». Хотя они редко бывают прямой причиной масштабирования новых узлов, они могут делать это косвенно и увеличивать средний размер узлов в кластере.
Как и другие виды affinity, pod affinity бывает жёстким и мягким. Поды с жёсткими правилами affinity должны размещаться вместе, а значит — только на узлах с достаточной ёмкостью для всей группы подов, а не для одного. Если таких узлов нет, потребуется масштабировать новый.
Это также означает, что если суммарные ресурсы, требуемые для группы связанных подов, велики, Karpenter при выборе формы узла предпочтёт крупные узлы небольшим.
В следующем примере:
-
Деплоймент из 3 реплик nginx с правилом anti-affinity, требующим разместить каждую реплику на отдельном узле.
-
Деплоймент из 3 реплик redis с правилом affinity, требующим размещать каждый под redis на том же узле, что и под nginx.
-
Это означает, что для размещения всех подов потребуется минимум 3 узла.
-
Поскольку суммарные требования по памяти для каждой пары nginx + redis составляют 1000 Mi, если в кластере нет как минимум 3 узлов с 1000 Mi свободной памяти, потребуется масштабирование.
---
# Deployment 1: Nginx (3 реплики)
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-web
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
affinity:
podAntiAffinity:
# Жёсткое правило: поды Nginx НЕ ДОЛЖНЫ находиться на одном узле
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: "kubernetes.io/hostname"
containers:
- name: nginx
image: nginx
resources:
requests:
memory: "300Mi"
---
# Deployment 2: Redis (3 реплики)
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
affinity:
podAffinity:
# Жёсткое правило: поды Redis ДОЛЖНЫ находиться на узле, где уже работает Nginx
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis
image: redis
resources:
requests:
memory: "700Mi"
Стратегия оценки LeastAllocated
Конфигурация kube-scheduler по умолчанию использует стратегию оценки узлов LeastAllocated («наименее загруженный»). Это означает, что при выборе узла для размещения рабочей нагрузки планировщик отдаёт предпочтение узлам с наибольшим количеством свободных ресурсов. Такой подход полезен для равномерного распределения нагрузки: сильно загруженные узлы реже получают новые рабочие нагрузки, тогда как недозагруженные — чаще.
Обратная сторона этой стратегии в том, что она серьёзно мешает алгоритмам консолидации узлов. Как только узел начинает приближаться к порогам утилизации, при которых его можно масштабировать вниз, планировщик начинает направлять на него новые рабочие нагрузки. Это приводит к тому, что узлы хронически недозагружены и провоцирует разрастание узлов.
Стратегия MostAllocated («наиболее загруженный»), напротив, заставляет планировщик всегда отдавать предпочтение самым занятым узлам — вплоть до их полного заполнения. В результате загруженные узлы используются эффективно, а недозагруженные стремятся к масштабированию вниз, что позволяет существенно сократить разрастание узлов и расходы.
В основных управляемых вариантах Kubernetes (EKS, AKS, GKE) изменить стратегию оценки у планировщика по умолчанию невозможно, однако создать кастомный планировщик с использованием стратегии MostAllocated сравнительно несложно. Тем не менее имейте в виду, что эта стратегия также несёт определённые риски. Не торопитесь переходить на MostAllocated, не взвесив все последствия — это серьёзное архитектурное решение, которое не стоит принимать необдуманно.
apiVersion: v1
kind: ConfigMap
metadata:
name: bin-packing-scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: bin-packing-scheduler
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bin-packing-scheduler
namespace: kube-system
labels:
component: scheduler
spec:
replicas: 1
selector:
matchLabels:
component: scheduler
template:
metadata:
labels:
component: scheduler
spec:
serviceAccountName: kube-scheduler
containers:
- name: kube-scheduler
image: registry.k8s.io/kube-scheduler:v1.35.0
command:
- kube-scheduler
- --config=/etc/kubernetes/scheduler-config.yaml
- --v=2
volumeMounts:
- name: config-volume
mountPath: /etc/kubernetes/scheduler-config.yaml
subPath: scheduler-config.yaml
readOnly: true
volumes:
- name: config-volume
configMap:
name: bin-packing-scheduler-config
Далее в статье будет отдельно рассмотрено, как стратегия LeastAllocated влияет на консолидацию узлов в Cluster Autoscaler и Karpenter соответственно.
Частичное планирование
С распространением высокопроизводительных распределённых фреймворков вроде PyTorch, TensorFlow или MPI поды всё чаще имеют множество взаимных зависимостей, без выполнения которых они не могут завершить работу. Если такие поды не запланированы все вместе, они способны удерживать дорогостоящее оборудование — например, GPU — в простое.
Для подобных рабочих нагрузок требуется групповое планирование (Gang Scheduling) с помощью таких решений, как NVIDIA KAI Scheduler или нативная функция Kubernetes Workload-Aware Scheduling, которая на момент написания статьи находится в статусе alpha. Хотя подробное рассмотрение gang scheduling выходит за рамки этого руководства, стоит помнить: по мере того как AI-нагрузки и задачи параллельной обработки становятся всё более популярными в Kubernetes, взаимозависимости между подами продолжают порождать расточительство, недозагруженные узлы и даже дедлоки ресурсов при использовании устаревших механизмов планирования, работающих только с отдельными подами.
DaemonSet’ы
Хотя и Karpenter, и Cluster Autoscaler игнорируют DaemonSet’ы при принятии решений о масштабировании вниз, следует помнить: именно потому, что DaemonSet развёртывается на каждом узле, нарушение любого из описанных выше правил имеет несравнимо больший эффект.
Представьте, что вы добавили аннотацию do-not-evict к DaemonSet — и группа узлов или весь кластер внезапно становится неспособным масштабироваться вниз.
Сценарии, специфичные для конкретного автоскейлера
Следующие сценарии характерны для конкретного используемого вами автоскейлера узлов — Cluster Autoscaler или Karpenter. Понимание особенностей реализации вашего автоскейлера принципиально важно для выявления блокировщиков масштабирования вниз.
Cluster Autoscaler и стратегия LeastAllocated
Напомним: стратегия оценки узлов LeastAllocated предписывает kube-scheduler размещать новые рабочие нагрузки на наименее загруженных узлах. Это отлично распределяет нагрузку равномерно по всем узлам, однако может сильно затруднить консолидацию и масштабирование вниз при использовании Cluster Autoscaler.
Cluster Autoscaler пытается масштабировать узел вниз только тогда, когда запросы CPU и памяти на нём опускаются ниже 50%. Однако при стратегии LeastAllocated планировщик и алгоритм масштабирования вниз Cluster Autoscaler начинают «бороться» друг с другом: как только узел приближается к порогу масштабирования вниз, планировщик направляет на него новые рабочие нагрузки. Это может существенно затруднить возврат к исходному размеру кластера после масштабирования вверх.
Прочие особенности Cluster Autoscaler
Если параметр minSize группы узлов больше 0, Cluster Autoscaler не опустится ниже этого значения — даже если узел пуст.
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: worker-nodes
namespace: default
annotations:
# Это параметр minSize для Cluster Autoscaler
cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "5"
spec:
replicas: 5 # Начинаем с 5
selector:
matchLabels:
cluster.x-k8s.io/cluster-name: my-cluster
template:
# ... стандартный шаблон пода для узлов ...
Кроме того, Cluster Autoscaler использует порог утилизации (по умолчанию 50% по CPU и памяти): узел не будет удалён, если запросы на ресурсы превышают этот порог. DaemonSet’ы в этот расчёт не включаются.
Наконец, аннотация cluster-autoscaler.kubernetes.io/scale-down-disabled на узлах или подах полностью запрещает уменьшение масштаба.
Настройка консолидации узлов в Karpenter
Если конфигурация консолидации узлов Karpenter слишком пассивна, масштабирование вниз может происходить крайне медленно или не происходить вовсе. В следующем манифесте NodePool Karpenter консолидация настроена очень консервативно:
-
Никогда не консолидировать с 9:00 до 21:00
-
В остальное время консолидировать только пустые узлы
-
И только после того, как узел оставался пустым в течение часа
-
Одновременно нарушать работу не более 10% ёмкости узлов
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: tokyo-nodepool
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 60m
budgets:
- nodes: 0%
schedule: "0 9 * * *"
duration: 12h
- nodes: 10%
При такой конфигурации консолидация будет продвигаться крайне медленно, особенно при наличии сотен узлов. Более разумным вариантом может быть: с 9:00 до 21:00 разрешить консолидацию по политике WhenEmpty с лимитом нарушений 10%, а в нерабочее время включить политику WhenEmptyOrUnderutilized с лимитом до 20%.
Karpenter и использование WhenEmpty
Политика консолидации WhenEmpty предписывает Karpenter масштабировать узел вниз только тогда, когда на нём не осталось ничего, кроме DaemonSet’ов. Это бывает полезно — например, в описанном выше случае, когда в часы пиковой нагрузки применяется консервативная политика нарушений, — однако может и заблокировать консолидацию.
Представьте сценарий, когда политика WhenEmpty действует круглосуточно. Масштабирование узлов вниз будет происходить крайне редко, потому что для этого узел должен полностью опустеть. Если совместить это со стратегией LeastAllocated, при которой kube-scheduler постоянно направляет новые рабочие нагрузки на узлы, приближающиеся к порогу масштабирования, масштабирование вниз может быть заблокировано полностью.
Дрейф узлов Karpenter
Если узел находится в состоянии дрейфа (drift) — например, использует неверный AMI, имеет несоответствующие taint’ы или метки, — Karpenter может оставить его как есть или заменить, вместо того чтобы уменьшить количество узлов исходя из реальной потребности.
Cluster Autoscaler против Karpenter
Cluster Autoscaler — стабильное и надёжное решение для автоматического масштабирования узлов, однако оно начинает устаревать. При принятии решений о консолидации он анализирует каждый узел по отдельности: если запросы CPU и памяти на узле превышают 50%, узел не будет удалён. Теоретически можно иметь 25 узлов с утилизацией 51%, и Cluster Autoscaler не предпримет никаких попыток вернуть избыточную ёмкость — что влечёт значительные потери.
Karpenter, напротив, оценивает совокупный спрос и совокупную ёмкость узлов, а затем строит план упаковки (bin packing) для наиболее эффективного распределения ресурсов. Например, Karpenter способен определить, что суммарный спрос снизился, и масштабировать узел вниз, перераспределив его рабочие нагрузки на два недозагруженных узла. Он также может объединить два небольших узла в один более крупный с меньшей стоимостью. Всё это делает Karpenter значительно более эффективным инструментом управления совокупной ёмкостью узлов по сравнению с Cluster Autoscaler.
Итог
Таким образом, снижение расточительных resource requests — лишь первый шаг в оптимизации ресурсов Kubernetes. Во многих случаях реальная экономия проявится только после того, как вы масштабируете вниз инфраструктуру узлов, а на это влияет множество факторов. Надеемся, что это руководство помогло вам понять и избежать скрытых ловушек при оптимизации Kubernetes-кластеров.