Чому ваш кластер спалює гроші
Брудний секрет економіки Kubernetes у тому, що більшість кластерів працюють на реальній утилізації 15–25%, а оплачуються так, ніби вони в piku. За минулий рік ми аудитували десятки продакшн-кластерів у клієнтів, і патерн ідентичний: requests, виставлені у два-три рази більше, ніж под коли-небудь реально використовує, флот великих on-demand нод і рахунок, що зростає швидше за headcount. Типовий проєкт скорочує витрати на 35–45% у перший місяць, не змінюючи жодного рядка коду застосунку.
Ця стаття — playbook, який ми використовуємо. Вона впорядкована так, як ви маєте її виконувати — від змін, що дають найбільші виграші, до оптимізацій з довгого хвоста, які мають значення, коли ви вже зібрали низько висячі фрукти.
Вимірюйте, перш ніж різати
Ви не можете оптимізувати те, що не можете атрибутувати. Перш ніж торкатися жодного request, встановіть щось, що дає вам видимість вартості per-namespace і per-workload. У 2026 році дві опції, що мають значення:
- OpenCost — CNCF-проєкт, що виріс з Kubecost. Open source, vendor neutral, інтегрується з Prometheus.
- Kubecost — комерційна збірка тієї самої кодової бази з hosted-опціями, алертами та командними фічами.
Обидва розгортаються приблизно за годину через Helm:
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm upgrade --install opencost opencost/opencost \
--namespace opencost --create-namespace \
--set opencost.exporter.defaultClusterId=prod-eu-west-1 \
--set opencost.prometheus.internal.enabled=true
Націльте його на ваш Prometheus, зачекайте 24 години на дані, а потім збудуйте Grafana-дашборд з вартістю per namespace, idle cost та ефективністю подів (actual / requested). Якщо namespace менше ніж на 30% ефективний — це мішень.
Крок 1: Right-Size requests та limits
Надмірно запитані CPU і memory — це найбільший драйвер вартості, який ми бачимо. Інженери копіпастять requests зі старого чарту, множать на safety factor "про всяк випадок" і відправляють. Кластер слухняно провіжить ноди, щоб задовольнити ці requests.
Використовуйте VPA у recommendation mode
Не вмикайте VPA у Auto mode на stateful-ворклоадах — він рестартуватиме поди, щоб їх переразмірити. Використовуйте Off mode, щоб отримати рекомендації без мутації:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
namespace: prod
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 4Gi
Через тиждень запитайте рекомендації VPA через kubectl get vpa api-vpa -o yaml і порівняйте з поточними requests. Дельта — це ваша економія.
Правила великого пальця
- CPU requests мають націлюватись на p95 реального використання. CPU стискається — под throttlить, а не вбивається, коли перевищує requests.
- Memory requests мають націлюватись на p99 або максимум, що спостерігається. Memory не стискається — досягнення limit означає OOMKill.
- CPU limits зазвичай мають бути не встановленими. Виставлення CPU limits нижче за capacity ноди спричиняє throttling навіть коли хост простоює. Єдиний випадок їх виставити — для ворклоадів, які ви хочете обмежити з міркувань біллінгу чи справедливості.
- Memory limits мають дорівнювати requests для передбачуваної поведінки. Це дає поду Guaranteed QoS class.
Под з розумним конфігом ресурсів
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: ghcr.io/example/api:1.12.0
resources:
requests:
cpu: 150m
memory: 256Mi
limits:
memory: 256Mi
readinessProbe:
httpGet: { path: /ready, port: 8080 }
livenessProbe:
httpGet: { path: /live, port: 8080 }
Крок 2: Замініть Cluster Autoscaler на Karpenter
Якщо ви на EKS і все ще використовуєте Cluster Autoscaler з pre-created node groups, ви залишаєте гроші на столі. Karpenter дивиться на pending поди, вибирає найдешевший інстанс-тип, що їм підходить, і провіжить його за ~30 секунд. Він також консолідує: коли нода стає недовикористаною, Karpenter переносить поди на меншу інстанцію і термінує стару.
Мінімальний NodePool із змішаними spot та on-demand, capacity-optimized allocation та увімкненою консолідацією:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
template:
spec:
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["m7i", "m7a", "c7i", "r7i"]
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: ["nano", "micro", "small"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
limits:
cpu: "2000"
memory: 4000Gi
Дві деталі, що мають значення. По-перше, дозволяючи і amd64, і arm64, ви дозволяєте Karpenter вибирати Graviton-ноди, коли ворклоад їх підтримує — Graviton зазвичай на 15–20% дешевший за vCPU. По-друге, WhenEmptyOrUnderutilized — це новий режим консолідації, що активно стискає кластер, а не лише коли нода повністю порожня.
Крок 3: Правильні ворклоади на Spot
Spot-інстанції на 60–90% дешевші за on-demand. Страх — це переривання: AWS дає вам two-minute notice і забирає ноду. Цей страх зазвичай перебільшений у 2026 році — рівень переривань на поширених родинах (m7i, c7i, r7i) часто сидить нижче 5% на місяць.
Що йде на Spot
| Тип ворклоаду | Spot-безпечний? | Нотатки |
|---|---|---|
| Stateless HTTP API | Так | Запускайте достатньо реплік і ставте PDB |
| Batch jobs | Так | Використовуйте checkpoints; рестарт на перериванні |
| CI runners | Так | Job-level retry обробляє eviction |
| Dev/staging | Так | Очевидно |
| Stateful бази даних | Ні | Використовуйте on-demand або керовані RDS/Aurora |
| Leader-elected контролери | Обережно | Переконайтесь, що кілька реплік розосереджені по зонах |
Захищайте availability через PodDisruptionBudgets
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
minAvailable: 50%
selector:
matchLabels:
app: api
Комбінуйте з topology spread, щоб ваші репліки не всі приземлились на ту саму spot-ноду:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
Крок 4: Автоскейліть ворклоади, а не лише ноди
Node autoscaling необхідний, але недостатній. Якщо Deployment має 20 реплік о 3 ночі, коли міг би мати 4, ви платите за idle поди. HPA на основі CPU — це дефолт, але справжня економія живе у custom metrics.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "200"
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
Для event-driven ворклоадів (консюмери черг, Kafka-процесори) KEDA — це правильна відповідь. Він скейлить за зовнішніми метриками на кшталт SQS depth, Kafka lag або кількості рядків Postgres — включно зі scale-to-zero.
Крок 5: Namespace quotas та LimitRanges
Дайте кожній команді бюджет. Без квот одна команда, що поводиться не так, може з'їсти весь кластер і весь ваш рахунок.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-growth-quota
namespace: team-growth
spec:
hard:
requests.cpu: "40"
requests.memory: 80Gi
limits.memory: 80Gi
pods: "100"
---
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: team-growth
spec:
limits:
- type: Container
default:
cpu: 200m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "4"
memory: 8Gi
LimitRange тут — тихий герой: він дає розумні дефолти будь-якому поду, який забув їх вказати, тож "необмежений" не прокрадається.
Крок 6: Showback та Chargeback
Останні 10% економії приходять з культурного тиску, а не з конфігурації. Щойно OpenCost тягне реальні числа, надсилайте кожній інженерній команді щотижневий лист про вартість. Не алерт, не дашборд — спокійне зведення: "ваш namespace коштував $4,213 минулого тижня, ось топ-5 найдорожчих ворклоадів". Команди оптимізують власний код, коли число видно і прив'язано до них.
Простий PromQL-запит для витягування вартості per-namespace:
sum by (namespace) (
kubecost_namespace_monthly_cost{cluster="prod-eu-west-1"}
)
Підключіть це до запланованого Grafana-звіту або невеликого Python-скрипта і надсилайте кожного понеділка.
Реальне "до/після"
Ось редаговане зведення з недавнього клієнтського проєкту — Series B SaaS на EKS, що запускає близько 400 подів через кластери prod, staging та dev.
| Метрика | До | Після | Зміна |
|---|---|---|---|
| Місячні витрати EKS | $48,200 | $28,900 | -40% |
| Кількість нод (prod) | 32 | 18 | -44% |
| Середня утилізація CPU | 14% | 46% | +3.3x |
| Середня утилізація memory | 22% | 61% | +2.8x |
| Spot ratio | 0% | 65% | - |
Увесь проєкт зайняв шість тижнів. Два з них — Karpenter rollout, два — right-sizing 40 найдорожчих ворклоадів, і два — налаштування політики квот і showback.
Короткий чекліст
Якщо ви цього кварталу не зробите нічого іншого:
- Встановіть OpenCost. Отримайте цифри per-namespace.
- Right-size топ-10 найдорожчих ворклоадів через рекомендації VPA.
- Розгорніть Karpenter з увімкненою консолідацією.
- Перенесіть stateless та batch ворклоади на spot.
- Встановіть ResourceQuotas та LimitRanges на кожен namespace.
- Налаштуйте щотижневі листи про вартість до team leads.
Наступні кроки
Оптимізація вартості — це не разовий проєкт — cluster drift реальний, і чистий кластер знову стає брудним приблизно за квартал, якщо ви його не підтримуєте чесним. Команди, що залишаються ефективними, ставляться до цього як до security: автоматизовані перевірки в CI, регулярні аудити, іменований owner. Якщо хочете допомоги з впровадженням цього playbook на вашому кластері, напишіть нам.