для on-premise кластера с частичным cloud, техники, команды, роли
// 01
Самоуправляемый Kubernetes — это принципиально другая экономика по сравнению с managed-сервисами (EKS/GKE/AKS). Деньги здесь другие: не счёт cloud-провайдера, а утилизация железа, лицензии, человеко-часы и облачные ВМ поверх этого.
Капитальные затраты уже оплачены. Основной cost — утилизация. Если узлы простаивают, деньги просто не работают. Цель: плотность и утилизация 60–80%.
Счёт идёт по часам. Каждый незакрытый idle-узел — реальные деньги. Приоритет: rightsizing, spot/preemptible, schedule-based scale-down.
Разработчики ставят requests «с запасом», лимиты неадекватно высокие. Кластер тратит 60% ресурсов на «просто в случае чего».
On-prem — для стабильных baseline-нагрузок. Cloud ВМ — для пиков и dev/staging. Cluster federation или node pool разделение.
On-prem экономия — это не снижение счёта, а увеличение полезной нагрузки на то же железо. Метрика успеха: сколько workloads умещается в кластер без деградации производительности.
// 02
Нельзя оптимизировать то, что не измерено. Первый шаг всегда — понять, где вы сейчас. Без baseline данных любые действия — угадывание.
# 1. Утилизация ресурсов по узлам kubectl top nodes # 2. Запросы vs лимиты по namespace kubectl describe nodes | grep -A6 "Allocated resources" # 3. Поды без requests/limits (КРИТИЧНО) kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].resources.requests == null) | "\(.metadata.namespace)/\(.metadata.name)"' # 4. Idle pods (CPU < 10m за 30 мин) kubectl top pods -A --sort-by=cpu | head -50 # 5. Orphaned PVC (не привязанные к активным подам) kubectl get pvc -A | grep -v Bound # 6. Namespace resource summary kubectl get resourcequota -A
# Установить kube-capacity (лучший инструмент для baseline) kubectl krew install resource-capacity # По узлам — requests vs actual utilization kubectl resource-capacity --util --pods # По namespace с CPU/Memory details kubectl resource-capacity --util --namespace-labels
Pods запрашивают в 3× больше, чем реально используют. Главная причина «жирных» узлов.
Scheduler не может нормально разместить. Узлы перегружаются без предупреждения. OOM kills.
Любая команда может захватить всё железо. Один раннавей-под гладит весь кластер.
Пустые узлы (особенно cloud ВМ) — прямые расходы. В on-prem — замороженный ресурс.
// 03
Overprovisioning — причина №1 неэффективности K8s-кластеров. Типичный кластер без оптимизации использует 30–40% запрошенных ресурсов. Rightsizing — это выравнивание requests по реальному потреблению.
Ключевое правило: request = что scheduler резервирует на узле. Limit = максимум, который pod может использовать. Если requests завышены — scheduler создаёт «дыры» на узлах, которые никогда не используются.
Собрать Prometheus-данные за 7–14 дней, взять P95 как базу для requests, P99 или max — для limits.
resources: requests: cpu: "100m" # P95 реального использования memory: "128Mi" # P95 реального использования limits: cpu: "500m" # Позволяем burst, CPU throttle некритичен memory: "256Mi" # P99 + буфер; OOM kill нежелателен
| QoS класс | Условие | Поведение при нехватке | Рекомендация |
|---|---|---|---|
| Guaranteed | requests == limits для CPU и memory | Убивается последним. Предсказуемо. | prod / critical |
| Burstable | requests < limits, заданы оба | Убивается при нехватке памяти, если превысил request | default для большинства |
| BestEffort | Нет ни requests ни limits | Первым убивается при нехватке. Scheduler игнорирует при размещении. | НИКОГДА в prod |
apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: name: my-app-vpa spec: targetRef: apiVersion: "apps/v1" kind: Deployment name: my-app updatePolicy: updateMode: "Off" # Только рекомендации, без изменений resourcePolicy: containerPolicies: - containerName: "*" minAllowed: cpu: 50m memory: 64Mi maxAllowed: cpu: "4" memory: 4Gi
kubectl describe vpa my-app-vpa -n production # Смотрим: status.recommendation.containerRecommendations # lowerBound = минимально безопасные значения # target = рекомендуемые # upperBound = максимальный потолок
Не запускайте VPA в updateMode: "Auto" на одном deployment вместе с HPA по CPU/Memory — они будут конфликтовать. Безопасный паттерн: HPA по CPU → VPA только для Memory. Или VPA в Off mode как советник.
// 04
Autoscaling — ключевой механизм cost optimization: незанятые ресурсы не потребляются. На on-prem это плотность пода на узел, в cloud — количество активных ВМ.
| Инструмент | Для чего | Ограничения | Приоритет |
|---|---|---|---|
| HPA | Горизонтальный scale по CPU/memory/custom metrics. Stateless workloads, API-сервисы. | Реактивен. Lag 30–60 сек. Плохо для spike-трафика. | Всегда |
| VPA | Вертикальный rightsizing. Memory-bound: Java, ML, Spark. Stateful, batch. | В Auto-режиме перезапускает поды. Конфликт с HPA по одной метрике. | Off/Recommend |
| KEDA | Scale on event-driven metrics: очереди RabbitMQ/Kafka, cron, latency. | Требует настройки triggers. Зависимость от внешних систем. | Async / batch |
| Cluster Autoscaler | Добавляет/удаляет узлы при нехватке/избытке. Критично для cloud ВМ. | Медленно (1–3 мин на узел). На on-prem требует интеграции с hypervisor API. | Cloud ВМ |
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-service minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65 # Не 80% — оставляем буфер behavior: scaleDown: stabilizationWindowSeconds: 300 # 5 мин стабилизация вниз policies: - type: Pods value: 1 periodSeconds: 60 # Удалять не более 1 пода в минуту scaleUp: stabilizationWindowSeconds: 0 # Scale up немедленно
apiVersion: keda.sh/v1alpha1 kind: ScaledObject spec: minReplicaCount: 0 # ← scale to zero! maxReplicaCount: 5 triggers: - type: cron metadata: timezone: "Europe/Moscow" start: "0 9 * * 1-5" # Старт в 9:00 пн-пт end: "0 20 * * 1-5" # Стоп в 20:00 desiredReplicas: "3" # Рабочее количество
Scale-to-zero для dev/staging окружений через KEDA cron trigger — один из самых быстрых wins. Dev-среды простаивают 60–70% времени. Для cloud ВМ это прямая экономия.
// 05
Без ResourceQuota одна команда или один раннавей-деплой может забрать все ресурсы кластера. Quota — это граница, LimitRange — это умолчание. Нужны оба.
ResourceQuota ограничивает суммарное потребление namespace. LimitRange задаёт дефолты для подов без явных requests/limits — без этого quota не работает корректно.
--- apiVersion: v1 kind: ResourceQuota metadata: name: team-quota namespace: team-backend spec: hard: # Compute requests.cpu: "20" requests.memory: 40Gi limits.cpu: "40" limits.memory: 80Gi # Object count (namespace sprawl protection) pods: "100" persistentvolumeclaims: "20" requests.storage: 200Gi --- apiVersion: v1 kind: LimitRange metadata: name: default-limits namespace: team-backend spec: limits: - type: Container default: # Limits если не указаны cpu: 500m memory: 512Mi defaultRequest: # Requests если не указаны cpu: 100m memory: 128Mi max: # Потолок на контейнер cpu: "4" memory: 8Gi min: cpu: 50m memory: 64Mi maxLimitRequestRatio: # Limit не может превышать request в N раз cpu: 10 memory: 4
Квоты с хорошим запасом. LimitRange жёсткий. QoS: Guaranteed для критичных, Burstable для остальных.
50% от production квоты. Scale-to-zero вечером. Spot-нода если в cloud.
Минимальные квоты. TTL на namespace (kube-janitor). Scale-to-zero aggressively. Нет persistent storage по умолчанию.
apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-resource-requests spec: rules: - name: check-resource-requests match: resources: kinds: [Pod] validate: message: "CPU и memory requests обязательны" pattern: spec: containers: - resources: requests: memory: "?*" cpu: "?*"
// 06
Orphaned (осиротевшие) ресурсы — тихие пожиратели. PVC продолжает занимать storage, Services держат load balancer open, ConfigMaps и Secrets раздувают etcd. Cascading orphans (цепочки брошенных ресурсов) дают 2–5× больше waste, чем прямые сироты.
После удаления pod/deployment PVC остаётся. В cloud — продолжает тарифицироваться. На on-prem — блокирует дисковое место.
CI/CD создаёт namespace для ветки, не удаляет после merge. Весь Helm-release остаётся жить.
Deployments обновились, старые ConfigMaps остались. Раздувают etcd, замедляют API-server.
По умолчанию K8s не удаляет завершённые Jobs. Накапливаются сотнями, засоряют API.
# 1. Найти orphaned PVC (не привязаны к запущенным подам) kubectl get pvc -A -o json | jq '.items[] | select(.status.phase != "Bound") | "\(.metadata.namespace)/\(.metadata.name): \(.spec.resources.requests.storage)"' # 2. Найти Completed/Failed Jobs старше 24 часов kubectl get jobs -A -o json | jq '.items[] | select(.status.completionTime != null) | "\(.metadata.namespace)/\(.metadata.name)"' # 3. Инструмент kor — находит всё сразу # github.com/yonahd/kor kor all --namespace production # 4. TTL-based cleanup для Jobs автоматически # В spec Jobs добавить: # ttlSecondsAfterFinished: 3600 (удалить через 1 час после завершения) # 5. kube-janitor для namespace TTL # Аннотация на namespace: # janitor/ttl: 7d — удалить namespace через 7 дней
apiVersion: batch/v1 kind: Job spec: ttlSecondsAfterFinished: 3600 # Автоудаление backoffLimit: 3 template: spec: restartPolicy: Never containers: - name: worker resources: # Обязательно для batch! requests: cpu: 200m memory: 256Mi limits: cpu: "2" memory: 1Gi
helm uninstall — он удаляет все ресурсы чартаprune: true — GitOps автоматически удаляет ресурсы, которых нет в gitowner: team-name на всех ресурсах — Kyverno политика на admissionkor с отчётом в Slack// 07
Cloud-узлы — самый быстрый источник savings. Каждый idle узел = реальные деньги по счёту. Три рычага: правильный размер, правильная цена (spot/reserved), правильное время работы.
Экономия 60–80% от On-Demand цены. Для stateless, batch, dev/staging. Требует PodDisruptionBudget и tolerations.
1–3 года резерв для baseline-нагрузки. Экономия 30–40% vs On-Demand. Только для стабильного baseline.
Memory-optimized для Java/ML, Compute-optimized для CPU-bound. Неправильный тип = 30%+ переплаты.
Dev/staging узлы выключены с 20:00 до 8:00 и в выходные. 65 часов из 168 в неделю — экономия 38%.
spec: tolerations: - key: "kubernetes.io/spot" operator: "Equal" value: "true" effect: "NoSchedule" affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: "node.kubernetes.io/lifecycle" operator: "In" values: ["spot"] topologySpreadConstraints: # Разнести по AZ для надёжности - maxSkew: 1 topologyKey: "topology.kubernetes.io/zone" whenUnsatisfiable: "DoNotSchedule"
Spot ВМ могут быть прерваны в любой момент. Обязателен: PodDisruptionBudget, graceful shutdown (terminationGracePeriodSeconds), и distributed architecture — не запускать stateful workload только на spot без on-demand fallback.
// 08
Техника без процесса не работает. Экономия дрейфует обратно через 2–3 месяца без постоянного цикла мониторинга и корректировки.
Устанавливает цели экономии (target % utilization). Защищает время на оптимизацию в спринтах. Reviewing cost dashboard раз в месяц. Принимает решения о квотах для команд.
Настройка VPA, HPA, quotas, autoscaler. Еженедельный аудит orphaned resources. Инструменты мониторинга (Prometheus, Kubecost/OpenCost). Внедрение policy-as-code.
Выставление корректных requests/limits в манифестах. Graceful shutdown реализация. Не создавать BestEffort поды. Очищать dev-ресурсы после работы.
Алерты на: узлы с утилизацией <20%, pods с OOM, PVC без owner, jobs в failed state. Никакого ручного труда — всё через Alertmanager.
Запуск kor для поиска orphaned ресурсов. Просмотр VPA recommendations — обновить requests если рекомендация стабильна 7+ дней. Slack digest: top-5 самых «тяжёлых» workloads.
Dashboard review: utilization по namespace, top spenders, waste ratio. Решения по квотам (повысить или понизить). Проверка cloud ВМ: есть ли idle узлы, нужно ли rightsizing VM типов.
Пересмотр на/off-prem баланса. Reserved Instances — сколько зарезервировать. Нужен ли Cluster Autoscaler upgrade. Есть ли смысл в node pool реструктуризации.
// 09
| Метрика | Цель / норма | Тревожный сигнал | Хорошо |
|---|---|---|---|
| CPU request utilization | ≥ 60% | < 30% — сильное overprovisioning | 60–75% — здоровый диапазон |
| Memory request utilization | ≥ 55% | < 25% — requests завышены | 55–70% |
| Node utilization (cloud) | ≥ 50% | < 20% — idle узел кандидат на удаление | 50–75% |
| Pods без requests/limits | 0 | Любое значение > 0 в prod | 0 в production |
| OOMKill rate | ≈ 0 | Рост — memory limits занижены | 0 в течение недели |
| Orphaned PVC count | 0 | > 5 не обработанных 7 дней | 0 или с owner label |
| Unallocated spend | < 5% | > 15% — ресурсы без владельца | < 5% |
| Waste ratio (reserved–used) | < 20% | > 40% — пересмотр requests | < 20% |
# CPU utilization vs requests по namespace sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace) / sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace) # Pods без resource requests (алерт = любое значение > 0) count(kube_pod_container_info) unless count(kube_pod_container_resource_requests{resource="cpu"}) # Idle nodes (utilization < 20%) sum(kube_node_status_allocatable{resource="cpu"}) by (node) - sum(kube_pod_container_resource_requests{resource="cpu"}) by (node) # OOMKill за последние 24h increase(kube_pod_container_status_last_terminated_reason{ reason="OOMKilled"}[24h])
// 10
| Инструмент | Категория | Для чего | On-prem / Cloud |
|---|---|---|---|
| OpenCost | Cost Visibility | Open-source cost allocation по namespace, workload, label. Self-hosted, нет cloud зависимости | Оба |
| Kubecost | Cost Visibility | Более полный UI, showback/chargeback, рекомендации. Free tier до 1 кластера | Оба |
| VPA (autoscaling.k8s.io) | Rightsizing | Рекомендации и автоматическое изменение requests/limits | Оба |
| KEDA | Autoscaling | Event-driven autoscaling, scale-to-zero, интеграция с очередями | Оба |
| Kyverno | Governance | Policy-as-code: enforce resources, labels, naming. Admission controller | Оба |
| kor | Orphan Cleanup | Сканирование orphaned PVC, ConfigMaps, Services, Deployments | Оба |
| kube-janitor | TTL Cleanup | TTL аннотации на namespace и resources. Автоудаление по расписанию | Оба |
| kube-capacity | Observability | kubectl plugin для быстрого overview requests vs utilization по нодам | Оба |
| Cluster Autoscaler | Node Scaling | Автоматическое добавление/удаление cloud ВМ-нод. Требует cloud API | Cloud only |
// 11
Самые быстрые wins: (1) rightsizing requests на основе VPA рекомендаций — 2 недели данных и одно применение; (2) scale-to-zero для dev/staging через KEDA cron; (3) удаление orphaned PVC. Эти три действия обычно дают 30–40% improvement без архитектурных изменений.