Коротко:
- Стандартный автоскейлинг по CPU часто реагирует слишком поздно или слишком остро - бизнес-метрики из Prometheus дают более точный сигнал.
- Есть два пути подключить внешние метрики к автоскейлеру: prometheus-adapter (встраивается в Kubernetes Metrics API) и KEDA (отдельный оператор с богатым набором триггеров).
- Flapping - главная проблема нестабильных метрик: под поднялся, метрика упала, под убился, метрика выросла - и так по кругу. Лечится через
stabilizationWindowSeconds. - Overshooting случается, когда метрика резко скачет вверх, а
scaleUp.policiesне ограничивают скорость роста числа реплик. - Для масштабирования по длине очереди (RabbitMQ, Kafka, Redis) KEDA ScaledObject проще и надежнее, чем ручная настройка через prometheus-adapter.
- Перед запуском в прод - проверь, что метрика стабильна хотя бы 2-3 минуты после пикового события, иначе автоскейлер будет осциллировать.
CPU-утилизация - удобный сигнал, но ненадежный оракул. Сервис может держать 20% CPU и при этом накапливать очередь запросов быстрее, чем успевает их обработать. Или наоборот: утилизация прыгает до 90% из-за одного тяжелого cron-job, а реальная нагрузка на API нулевая. Автоскейлер по CPU в таких сценариях либо не реагирует, либо поднимает лишние поды.
Именно поэтому зрелые команды переходят к сигналам из самого приложения: длина очереди, latency 99-го перцентиля, количество активных сессий, RPS на конкретный эндпоинт. Эти метрики уже есть в Prometheus - осталось научить кластер на них масштабироваться.
Статья охватывает весь путь: от архитектурного выбора между prometheus-adapter и KEDA до конкретных манифестов и разбора типичных ловушек - flapping, overshooting и «мертвых» метрик, из-за которых поды не поднимаются вовремя.
Как работает механизм автоскейлинга изнутри
Horizontal Pod Autoscaler - это контроллер внутри kube-controller-manager. Каждые 15 секунд (по умолчанию, параметр --horizontal-pod-autoscaler-sync-period) он запрашивает значение целевой метрики, сравнивает с желаемым значением и вычисляет нужное количество реплик по формуле:
desiredReplicas = ceil(currentReplicas * (currentValue / desiredValue))
Для метрик типа CPU это работает прямолинейно. Для внешних и кастомных сигналов нужен посредник - адаптер, который умеет отвечать на запросы Kubernetes Metrics API. Без него контроллер просто не знает, где брать данные.
Существует три категории метрик, которые понимает HPA:
- Resource metrics - CPU и память (встроены, работают из коробки через metrics-server).
- Custom metrics - метрики, привязанные к конкретному Kubernetes-объекту (например, RPS на Deployment). Поставляются через
custom.metrics.k8s.ioAPI. - External metrics - метрики, не привязанные к объекту кластера (длина очереди во внешнем брокере). Поставляются через
external.metrics.k8s.ioAPI.
Оба нестандартных API нужно регистрировать явно - либо через prometheus-adapter, либо через KEDA.
Путь через prometheus-adapter: кастомные метрики прямо в HPA
Prometheus-adapter - это мост между Prometheus и Kubernetes Metrics API. После его установки можно писать обычный манифест HPA с блоком type: Pods или type: Object и ссылаться на метрику по имени.
Установка через Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
--namespace monitoring \
--set prometheus.url=http://prometheus-server.monitoring.svc \
--set prometheus.port=9090После установки адаптер регистрируется как APIService в кластере. Проверить, что всё поднялось:
kubectl get apiservice v1beta1.custom.metrics.k8s.io
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .Настройка правил преобразования метрик
Адаптер не пробрасывает все метрики из Prometheus автоматически - нужно явно описать правила в конфиге. Вот пример для метрики RPS на Pod-уровне:
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^(.*)_total$"
as: "${1}_per_second"
metricsQuery: 'rate(http_requests_total{<<.LabelMatchers>>}[2m])'Здесь <<.LabelMatchers>> - шаблонный плейсхолдер адаптера, который подставит селекторы namespace и pod из запроса HPA. Важно: метрика должна иметь лейблы namespace и pod, иначе адаптер не сможет её привязать к объекту.
Манифест HPA с кастомной метрикой
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-service-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100"Здесь цель - держать в среднем 100 RPS на каждый под. Если трафик вырастет до 300 RPS на 2 поды, контроллер рассчитает: нужно ceil(2 * (150/100)) = 3 реплики.
Путь через KEDA: ScaledObject и триггеры
KEDA (Kubernetes Event-Driven Autoscaling) - отдельный оператор с другой философией. Он не встраивается в стандартный HPA API, а создаёт поверх него свой объект - ScaledObject. Под капотом KEDA всё равно управляет HPA, но снимает с инженера большую часть сложности конфигурации адаптеров.
Главное преимущество - встроенные скейлеры для десятков источников: Prometheus, RabbitMQ, Kafka, Redis, Azure Service Bus, AWS SQS и другие. Для Kubernetes масштабирования по очереди KEDA - де-факто стандарт.
Установка KEDA
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespaceScaledObject с триггером на Prometheus
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-service-scaledobject
namespace: production
spec:
scaleTargetRef:
name: api-service
minReplicaCount: 2
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 60
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.monitoring.svc:9090
metricName: http_requests_per_second
query: rate(http_requests_total{job="api-service"}[2m])
threshold: "100"
activationThreshold: "10"Параметр activationThreshold задаёт нижнюю границу активации - пока значение метрики ниже 10, KEDA не будет пытаться масштабировать вообще. Это защищает от шума в метрике при минимальном трафике.
ScaledObject для масштабирования по очереди RabbitMQ
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaledobject
namespace: production
spec:
scaleTargetRef:
name: message-worker
minReplicaCount: 0
maxReplicaCount: 50
triggers:
- type: rabbitmq
metadata:
host: amqp://rabbitmq.production.svc:5672
queueName: order-processing
mode: QueueLength
value: "10"Здесь minReplicaCount: 0 позволяет полностью убрать воркеры, когда очередь пуста. Это особенность KEDA - стандартный HPA не умеет скейлить до нуля. Для Kubernetes масштабирования по очереди это критично: воркеры ночью просто не нужны, а платить за idle-поды нет смысла.
Сравнение двух подходов
| Критерий | prometheus-adapter | KEDA |
|---|---|---|
| Интеграция с HPA API | Нативная, через Metrics API | Создаёт HPA под капотом |
| Масштабирование до нуля | Нет | Да |
| Источники метрик | Только Prometheus | 50+ скейлеров |
| Сложность конфигурации | Высокая (правила преобразования) | Средняя (декларативные триггеры) |
| Отладка | Через kubectl get --raw | kubectl describe scaledobject |
| Когда выбирать | Уже есть Prometheus, нужны только метрики приложения | Очереди, event-driven, масштабирование до нуля |
Flapping: почему поды скачут и как это остановить
Flapping - это осцилляция: автоскейлер поднял поды, нагрузка распределилась, метрика упала ниже порога, поды убились, нагрузка снова сконцентрировалась на меньшем числе реплик - и цикл повторился. Выглядит это как постоянные события ScalingReplicaSet в describe deployment с интервалом в 1-3 минуты.
Причин несколько:
- Целевое значение метрики выставлено слишком близко к рабочему уровню нагрузки.
- Метрика нестабильна - у неё высокая дисперсия даже при стабильной нагрузке.
- Окно усреднения слишком короткое (rate за 30 секунд вместо 2-5 минут).
Решение через stabilizationWindowSeconds
Параметр behavior в манифесте HPA позволяет задать отдельные окна стабилизации для масштабирования вверх и вниз:
spec:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 4
periodSeconds: 60
selectPolicy: MaxЗдесь scale-down не начнётся, пока метрика не держится ниже порога непрерывно 5 минут. А при росте нагрузки за один период можно добавить не более 4 подов или 25% от текущего числа - берётся максимум из двух политик (selectPolicy: Max). Это классическое HPA flapping решение для нестабильных метрик.
Важно: stabilizationWindowSeconds для scale-down по умолчанию уже равен 300 секундам. Для scale-up по умолчанию - 0. Поэтому flapping чаще всего возникает при росте нагрузки, а не при её спаде.
Overshooting: когда подов становится слишком много
Overshooting - это резкий скачок числа реплик после спайка метрики. Типичный сценарий: пришёл burst-трафик, метрика за 15 секунд выросла в 10 раз, автоскейлер посчитал нужное количество реплик и сразу запросил их все. Поды поднимаются минуту-две, трафик к этому времени уже спал, но кластер уже потратил ресурсы на запуск лишних реплик.
Параметр scaleUp.policies с типом Percent ограничивает скорость роста:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 60
- type: Pods
value: 10
periodSeconds: 60
selectPolicy: MinselectPolicy: Min здесь берёт минимум из двух политик. За одну минуту число реплик не может вырасти более чем в два раза и не более чем на 10 штук - что меньше. Это даёт контролируемый рост без гонки за каждым спайком.
«Мёртвая» метрика и защита от scale-to-zero по ошибке
Если Prometheus недоступен или метрика пропала из scrape-a, prometheus-adapter вернёт ошибку. По умолчанию HPA в этом случае не меняет число реплик - это безопасное поведение. Но KEDA с minReplicaCount: 0 может в такой ситуации убить все поды, если скейлер решит, что данных нет и нагрузки тоже нет.
Для защиты в KEDA используется fallback:
spec:
fallback:
failureThreshold: 3
replicas: 5После трёх последовательных ошибок получения метрики KEDA установит 5 реплик вместо того, чтобы скейлить до нуля. Это не идеальное решение, но лучше, чем внезапный простой из-за недоступности Prometheus.
Диагностика: что смотреть, когда автоскейлер не работает
Первый шаг при любой проблеме - события объекта:
kubectl describe hpa api-service-hpa -n production
kubectl describe scaledobject api-service-scaledobject -n productionТипичные сообщения об ошибках и их причины:
| Сообщение | Причина | Что проверить |
|---|---|---|
| unable to get metric ... no metrics returned | Адаптер не находит метрику | Правила в конфиге prometheus-adapter, наличие лейблов |
| failed to get external metrics | KEDA не может достучаться до источника | Сетевой доступ, аутентификация в TriggerAuthentication |
| the HPA was unable to compute the replica count | Деление на ноль или NaN в метрике | PromQL-запрос возвращает пустой результат |
| DesiredReplicas below minimum | Расчёт даёт меньше minReplicas | Нормальное поведение, не ошибка |
Проверить, что конкретная метрика видна через API:
# Для custom metrics
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/production/pods/*/http_requests_per_second"
# Для external metrics
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/production/http_requests_per_second"Если команда возвращает пустой массив items или ошибку 404 - проблема на стороне адаптера, не HPA.
Антипаттерны, которые ломают масштабирование
Использование инстантных значений вместо rate. Метрика вида http_requests_total без функции rate - это монотонно растущий счётчик. Автоскейлер будет видеть постоянный рост и бесконечно добавлять поды. Всегда используйте rate(...[2m]) или irate для счётчиков.
Слишком короткое окно в rate. rate(...[30s]) даёт очень зашумлённый сигнал. Для автоскейлинга лучше 2-5 минут - метрика будет менее реактивной, но стабильнее.
Целевое значение без запаса. Если средний RPS на под в норме 80, а target выставить 85 - автоскейлер будет постоянно пытаться чуть-чуть скорректировать число реплик. Держите запас 20-30% между нормальной нагрузкой и целевым порогом.
Один HPA на несколько метрик с разными масштабами. Если указать и CPU (цель 70%), и RPS (цель 100), HPA возьмёт максимум из двух расчётов. Это правильное поведение, но нужно явно понимать, какая метрика будет доминирующей при разных сценариях нагрузки.
Чеклист перед запуском в прод
- Метрика в PromQL возвращает стабильное значение - проверить вручную за последние 24 часа в Grafana.
- В конфиге prometheus-adapter или ScaledObject использован rate/avg, а не сырой счётчик.
- Настроен
stabilizationWindowSecondsдля scale-down (минимум 120-300 секунд). - Ограничена скорость масштабирования вверх через
scaleUp.policies. - Для KEDA с
minReplicaCount: 0настроенfallback. - Проверена видимость метрики через
kubectl get --raw. - Настроены PodDisruptionBudget для сервиса, чтобы scale-down не убивал слишком много подов одновременно.
- Проведена нагрузочная тест-сессия с наблюдением за событиями HPA в реальном времени.
- Выставлены адекватные
minReplicas- не ноль для stateless HTTP-сервисов (хотя бы 2 для отказоустойчивости). - Alerts на случай, если автоскейлер достиг
maxReplicas- это сигнал, что потолок нужно пересмотреть.