HorizontalPodAutoscaler в Kubernetes: как настроить адекватный автоскейл по CPU и кастомным метрикам
HPA в k8s настраивается за пять строк YAML, и в этом проблема. Приклеил к Deployment targetCPUUtilizationPercentage: 70, поставил minReplicas: 2, maxReplicas: 10 — и спишь спокойно. Через месяц приходит инцидент: на пике трафика поды плодятся, БД захлебывается, latency растёт. Автоскейл вместо помощи становится частью проблемы.
Покажу, как настроить HPA так, чтобы он реально работал. Что важно учесть, что не учитывает дефолт, и где использовать кастомные метрики вместо CPU. Версии — k8s 1.30, autoscaling/v2 API, prometheus-adapter 0.12.
Почему дефолтный HPA не справляется
Стандартный HPA смотрит на среднее CPU utilization по подам. Если выше target — добавляет реплики. Это работает для CPU-bound сервисов с равномерной нагрузкой, но плохо подходит для:
- I/O-bound API-сервисов, где CPU 30%, а очередь запросов растёт.
- Воркеров с очередями (Kafka, RabbitMQ) — нужно скейлиться по длине очереди, а не по CPU.
- Web-приложений с переменной нагрузкой и медленным стартом — пока scale up отрабатывает, пик уже прошёл.
- Memory-bound нагрузок — CPU низкий, память жрут, OOMKilled.
HPA умеет всё это, если правильно настроить.
Базовая структура autoscaling/v2
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60Ключевая часть — behavior. Дефолтные значения в k8s — консервативные для scale up (15 секунд stabilization, медленно добавляет) и слишком агрессивные для scale down (поэтому pods «прыгают»).
scaleUp policies
В моём примере: за 30 секунд можно либо удвоить количество реплик (Percent 100%), либо добавить 4 пода (Pods 4). selectPolicy: Max — берёт более агрессивный вариант. То есть если у тебя 2 пода — за 30 секунд может стать 6. Если 10 — может стать 14.
Это нужно, потому что трафик на e-commerce может удваиваться за минуту (рассылка, рекламная кампания, флешсейл). Дефолт скейлится медленнее, чем растёт нагрузка.
scaleDown policies
Стабилизация 300 секунд (5 минут) — это окно, в течение которого HPA смотрит на максимум метрики, прежде чем уменьшить. Без него 60-секундный спад нагрузки → лишние поды убиваются → новый пик → нужно ждать scale up. Поды дёргаются туда-сюда, latency качается.
Я ставлю минимум 5 минут на scale down, иногда 10 для тяжёлых сервисов с долгим прогревом кешей.
Custom metrics: скейлим по очереди
Допустим, у тебя воркер, который обрабатывает Kafka. Чтобы скейлить по lag-у консьюмер-группы, нужны custom metrics через prometheus-adapter.
Установка prometheus-adapter
helm install prometheus-adapter \
prometheus-community/prometheus-adapter \
-n monitoring \
--set prometheus.url=http://kube-prometheus-stack-prometheus.monitoring.svc \
--set rules.default=falseДальше нужно описать, какие метрики из Prometheus превращать в k8s metrics. Конфиг в values.yaml:
rules:
custom:
- seriesQuery: 'kafka_consumergroup_lag{namespace!=""}'
resources:
overrides:
namespace:
resource: namespace
name:
as: "kafka_consumergroup_lag"
metricsQuery: 'sum(kafka_consumergroup_lag{<<.LabelMatchers>>}) by (namespace, consumergroup)'После apply prometheus-adapter регистрирует endpoint /apis/custom.metrics.k8s.io/v1beta1, и теперь HPA может по этой метрике скейлиться.
HPA по custom-метрике
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: kafka-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kafka-worker
minReplicas: 2
maxReplicas: 30
metrics:
- type: Object
object:
describedObject:
apiVersion: v1
kind: Service
name: kafka-worker
metric:
name: kafka_consumergroup_lag
target:
type: Value
value: "1000"Целевое значение — 1000 сообщений в очереди на под. Если lag растёт — добавляется поды. Когда lag разгрёбся — снижается обратно.
В реальности я обычно комбинирую: и CPU, и custom-метрика. HPA берёт максимум из вычисленных желаемых replicas.
Внешние метрики через KEDA
Если custom metrics — это «метрика, привязанная к k8s-ресурсу», то external metrics — «что-то снаружи». Например, длина очереди в SQS, количество сообщений в RabbitMQ, размер бэклога в Pub/Sub.
Можно собрать через prometheus-adapter, но проще — KEDA. Это отдельный оператор, который умеет десятки источников из коробки.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: rabbitmq-worker
spec:
scaleTargetRef:
name: rabbitmq-worker
minReplicaCount: 1
maxReplicaCount: 50
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: orders
queueLength: "10"
host: amqp-secret10 — это сколько сообщений на под мы готовы держать. KEDA сам считает желаемое replicas и обновляет HPA-объект внутри. Снаружи всё равно живёт обычный HPA.
KEDA умеет даже scale-to-zero — если очередь пустая, поды могут полностью схлопнуться. Это для batch-нагрузок и стартапов, которые экономят на staging.
Где обычно ловят грабли
1. requests не выставлены
HPA по CPU считает actual / requests. Если у пода нет resources.requests.cpu, HPA не работает. В Events будет FailedGetResourceMetric. Без requests — никакого автоскейла.
2. Слишком жёсткий target
Видел: averageUtilization: 50. Это значит, что половина CPU всегда «в резерве». На больших фермах это лишние десятки процентов оверплаты. 70-80% — нормальный baseline для веб-сервисов, 90% для воркеров.
3. minReplicas: 1 для критичных сервисов
Если minReplicas=1 и под умер — кратковременный downtime. Для критичного API минимум 2-3 пода и PodDisruptionBudget, чтобы при maintenance node не уехало всё сразу.
4. Нет coolDown между scale up
Дефолт у scale up небольшой — это плюс, но если сервис стартует медленно (Java 60 секунд), новые поды ещё не готовы, а HPA уже смотрит на ту же метрику и решает добавить ещё. Лечится stabilizationWindowSeconds побольше или startupProbe такой, чтобы новый под не считался ready, пока не прогрелся.
5. HPA + VPA на одном объекте
VPA меняет requests, HPA смотрит на utilization относительно requests. Если оба активно работают — interfering. VPA в режиме Auto + HPA по CPU = bad. Лечится: VPA в Off для рекомендаций, или VPA только для не-CPU ресурсов (memory).
Мониторинг HPA
Я смотрю в Prometheus:
kube_horizontalpodautoscaler_status_current_replicas— текущее количество реплик.kube_horizontalpodautoscaler_status_desired_replicas— целевое.kube_horizontalpodautoscaler_spec_max_replicas— потолок.
Алёрт: если current_replicas == max_replicas дольше 10 минут — ты упёрся в потолок, нужно либо повышать max, либо разбираться, почему метрика не падает с ростом подов.
- alert: HPAMaxedOut
expr: |
kube_horizontalpodautoscaler_status_current_replicas
== kube_horizontalpodautoscaler_spec_max_replicas
for: 10m
annotations:
summary: "HPA уперся в maxReplicas"Что запомнить
HPA настраивается не одной строкой, а через behavior. Scale up быстрый и агрессивный, scale down медленный и стабильный. CPU — для CPU-bound. Очереди и латентность — через custom metrics или KEDA. requests должны быть выставлены на всех контейнерах. minReplicas минимум 2 для критичного. Не миксуй HPA и VPA на одном ресурсе.
Куда копать: официальная документация HPA, KEDA для внешних триггеров, и обязательно посмотри prometheus-adapter — без него custom metrics не работают.