Kubernetes HPA кастомные метрики: как настроить масштабирование по Prometheus и не словить шторм подов

Коротко:

  • Стандартный автоскейлинг по 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.io API.
  • External metrics - метрики, не привязанные к объекту кластера (длина очереди во внешнем брокере). Поставляются через external.metrics.k8s.io API.

Оба нестандартных 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-namespace

ScaledObject с триггером на 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-adapterKEDA
Интеграция с HPA APIНативная, через Metrics APIСоздаёт HPA под капотом
Масштабирование до нуляНетДа
Источники метрикТолько Prometheus50+ скейлеров
Сложность конфигурацииВысокая (правила преобразования)Средняя (декларативные триггеры)
ОтладкаЧерез kubectl get --rawkubectl 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: Min

selectPolicy: 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 metricsKEDA не может достучаться до источникаСетевой доступ, аутентификация в 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 возьмёт максимум из двух расчётов. Это правильное поведение, но нужно явно понимать, какая метрика будет доминирующей при разных сценариях нагрузки.

Чеклист перед запуском в прод

  1. Метрика в PromQL возвращает стабильное значение - проверить вручную за последние 24 часа в Grafana.
  2. В конфиге prometheus-adapter или ScaledObject использован rate/avg, а не сырой счётчик.
  3. Настроен stabilizationWindowSeconds для scale-down (минимум 120-300 секунд).
  4. Ограничена скорость масштабирования вверх через scaleUp.policies.
  5. Для KEDA с minReplicaCount: 0 настроен fallback.
  6. Проверена видимость метрики через kubectl get --raw.
  7. Настроены PodDisruptionBudget для сервиса, чтобы scale-down не убивал слишком много подов одновременно.
  8. Проведена нагрузочная тест-сессия с наблюдением за событиями HPA в реальном времени.
  9. Выставлены адекватные minReplicas - не ноль для stateless HTTP-сервисов (хотя бы 2 для отказоустойчивости).
  10. Alerts на случай, если автоскейлер достиг maxReplicas - это сигнал, что потолок нужно пересмотреть.

часто задаваемые вопросы

Prometheus-adapter встраивается в стандартный Kubernetes Metrics API и позволяет использовать обычные манифесты HPA. KEDA - отдельный оператор с собственным CRD ( ScaledObject ), который умеет масштабировать до нуля и поддерживает десятки источников данных помимо Prometheus. Для простых сценариев с метриками приложения подходит адаптер; для event-driven архитектур и очередей - KEDA удобнее.

Главный инструмент - stabilizationWindowSeconds в блоке behavior.scaleDown . Установите его в 180-300 секунд: решение об уменьшении числа реплик будет приниматься только если метрика держится ниже порога всё это время. Дополнительно стоит увеличить окно в PromQL-запросе с 30 секунд до 2-5 минут.

Да. HPA v2 поддерживает массив metrics с несколькими источниками. Контроллер рассчитает желаемое число реплик по каждой метрике отдельно и возьмёт максимум. Это значит, что любая одна метрика может спровоцировать рост - нужно следить, чтобы все целевые значения были настроены согласованно.

Это нижняя граница, ниже которой KEDA считает нагрузку нулевой и не активирует скейлинг. Полезен для очередей: если в очереди 2 сообщения, возможно, это фоновый шум, а не реальная нагрузка. activationThreshold позволяет задать минимальный порог, с которого начинается работа скейлера.

Для стандартного HPA с prometheus-adapter: kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces//pods/*/" . Если ответ содержит непустой массив items с числовыми значениями - метрика доступна. Для ScaledObject в KEDA: kubectl describe scaledobject покажет текущее значение триггера и состояние.

Если у приложения долгое время запуска (больше 2-3 минут), автоскейлинг по коротким пикам будет бесполезен - поды не успеют подняться до конца спайка. В таких случаях лучше использовать predictive-подход или держать постоянный запас реплик. Также не стоит автоскейлить stateful-компоненты (базы данных, брокеры) через HPA - там нужен специализированный оператор.

В Prometheus правило будет выглядеть примерно так: kube_horizontalpodautoscaler_status_current_replicas == kube_horizontalpodautoscaler_spec_max_replicas . Когда текущее число реплик равно максимуму - автоскейлер упёрся в потолок и уже не может ответить на рост нагрузки. Это важный сигнал для пересмотра maxReplicas или оптимизации самого сервиса.

Итог

Автоскейлинг по CPU - это стартовая точка, а не финальное решение. Переход на сигналы из самого приложения - RPS, длину очереди, latency - даёт точнее реагирующую систему, но требует аккуратной настройки. Главные рычаги управления стабильностью - stabilizationWindowSeconds, политики в behavior и правильное окно усреднения в PromQL. Без них даже хорошая метрика превращается в источник флаппинга.

Выбор между prometheus-adapter и KEDA зависит от задачи: если нужны только метрики из Prometheus и масштабирование до нуля не требуется - адаптер проще в поддержке. Если архитектура event-driven или нужно реагировать на очереди - KEDA экономит время на конфигурацию и даёт больше контроля через ScaledObject.

Любой из этих инструментов стоит сначала проверить на нагрузочном стенде с наблюдением за событиями в реальном времени. Понять, как ведёт себя скейлер при спайках и после их окончания, лучше до прода - а не в три ночи с растущей очередью инцидентов.