KEDA как финансовый гардрейл: scale-to-zero, лимиты реплик и автоскейлинг по событиям в Kubernetes

от автора

Привет, Хабр.

Меня зовут Максим Качинский, я Head of Ops в Rocketdata. В этой статье разберу KEDA как практический инструмент для Kubernetes-workloads с непостоянной нагрузкой: очередями, расписаниями, HTTP-пиками и внешними метриками.

Материал не про «поставили автоскейлер и сразу сэкономили», а про production-подход: где KEDA действительно помогает, какие параметры нужно ограничивать заранее, как проверять экономику пилота и что может пойти не так при раскатке.

В сообществе «Практики FinOps» мы разбираем не только отчеты, аллокацию и бюджетные процессы, но и инженерные механики, которые помогают влиять на инфраструктурные косты до того, как они превращаются в строку в биллинге.

TL;DR

Проблема: HPA масштабирует по CPU/Memory, но не видит очереди, расписания и бизнес-метрики. Сервисы простаивают 18 часов в сутки и жгут бюджет, пока ждут редкую нагрузку.

Решение: KEDA масштабирует поды по событиям — длина очереди, cron, Prometheus-метрики, HTTP-трафик. Сервис включается под работу и выключается в простое.

FinOps-профит:

  • Scale-to-zero: 0 подов когда нагрузки нет → экономия на холостом простое

  • maxReplicaCount: финансовый предохранитель от бесконтрольного разгона

  • Экономия 40–70% на workloads с непостоянной нагрузкой (воркеры, batch-задачи, dev-окружения)


Дальше разберем KEDA именно как практический FinOps-гардрейл для Kubernetes: где HPA уже не хватает, как устроен ScaledObject, как безопасно подходить к scale-to-zero, какие ограничения ставить через maxReplicaCount и какие метрики собрать до пилота, чтобы потом доказать эффект цифрами.

В Kubernetes легко привыкнуть к мысли, что автоскейлинг уже есть, значит проблема с динамической нагрузкой решена. На практике все сложнее. Стандартный HPA хорошо работает там, где нагрузка выражается через CPU и память. Но часть сервисов живет по другой логике: ждет сообщения в очереди, включается по расписанию, получает краткие пики HTTP-трафика или обрабатывает задачи пачками.

С точки зрения FinOps это важный слой. Если сервис держит поды и ноды включенными «на всякий случай», инфраструктура оплачивается не за полезную работу, а за ожидание. KEDA помогает сдвинуть масштабирование ближе к фактической нагрузке: событиям, очередям, расписанию, HTTP-трафику и внешним метрикам.

Это не замена управлению затратами, аллокации и ownership. Но это рабочий инженерный гардрейл, который помогает не держать ресурсы включенными без необходимости и заранее ограничивать верхнюю границу масштабирования.

Где стандартного HPA уже не хватает

Horizontal Pod Autoscaler обычно воспринимается как базовый ответ на вопрос «как масштабировать поды». В типовом сценарии он принимает решения по CPU и памяти.

Но не вся нагрузка проявляется через эти метрики.

Пример: есть воркер, который разбирает сообщения из очереди. Он может почти не грузить CPU, потому что ждет новые сообщения или ответа от внешнего API. При этом очередь уже растет, время обработки увеличивается, SLA начинает проседать.

Для HPA картина может выглядеть спокойно:

CPU: 2%Memory: stableReplicas: 2

А фактическая бизнес-нагрузка выглядит так:

Queue length: 3 250 messagesOldest message age: 18 minutesSLA: at risk

Если в этот момент масштабироваться только по CPU, реакция будет запаздывать. Если держать большой резерв подов постоянно, команда платит за мощность, которая большую часть времени простаивает.

Это и есть классический over-provisioning: ресурсы заняты, счет растет, полезной работы в этот момент нет.

Что делает KEDA

KEDA, Kubernetes Event-driven Autoscaling, добавляет в Kubernetes событийное масштабирование. Важная деталь: KEDA не заменяет HPA, а расширяет его. Он позволяет масштабировать контейнеры в Kubernetes по количеству событий, которые нужно обработать. Официальная документация KEDA.

KEDA может работать с разными источниками нагрузки:

  1. очереди сообщений (RabbitMQ, Kafka, SQS, Azure Service Bus);

  2. расписания (cron);

  3. базы данных (PostgreSQL, MySQL, Redis);

  4. HTTP-трафик (через HTTP Add-on);

  5. метрики из Prometheus;

  6. внешние event source через 60+ встроенных скейлеров.

Как это работает внутри

┌─────────────┐         ┌──────────────┐│ RabbitMQ    │────────▶│ KEDA Scaler  ││ Queue: 150  │         │ polls every  │└─────────────┘         │ 30 sec       │                        └──────┬───────┘                               │                               ▼                        ┌──────────────┐                        │ External     │                        │ Metrics API  │                        └──────┬───────┘                               │                               ▼                        ┌──────────────┐                        │ HPA decides  │                        │ 150/10 = 15  │                        └──────┬───────┘                               │                               ▼                        ┌──────────────┐                        │ Deployment   │                        │ scales to 15 │                        └──────────────┘

KEDA проверяет источник событий, передает метрику в Kubernetes через External Metrics API, а HPA уже управляет количеством реплик. В документации KEDA отдельно описывает scaling для Deployments, StatefulSets и Custom Resources через scale subresource. KEDA: Scaling Deployments, StatefulSets & Custom Resources.

Установка KEDA через Helm

Базовая установка через Helm выглядит так:

helm repo add kedacore https://kedacore.github.io/chartshelm repo updatehelm install keda kedacore/keda \  --namespace keda \  --create-namespace

После установки можно проверить поды в namespace keda:

kubectl get pods -n keda

Такой способ установки указан в официальной документации KEDA: добавить Helm-репозиторий, обновить его и установить chart в отдельный namespace. KEDA: Deploying KEDA.

Базовая структура ScaledObject

Основная сущность для масштабирования workload — это ScaledObject.

Упрощенно структура выглядит так:

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: example-scalerspec:  scaleTargetRef:    name: example-deployment  minReplicaCount: 0  maxReplicaCount: 10  pollingInterval: 30  cooldownPeriod: 300  triggers:    - type: ...      metadata:        ...

Ключевые параметры:

  • scaleTargetRef — Целевой Deployment, StatefulSet или другой ресурс, который KEDA будет масштабировать.

  • minReplicaCount — Минимальное количество реплик. Для scale-to-zero обычно указывают 0.

  • maxReplicaCount — Верхняя граница реплик. Это технический лимит роста, который помогает заранее ограничить масштабирование.

  • pollingInterval — Как часто KEDA проверяет источник событий (в секундах).

  • cooldownPeriod — Пауза перед масштабированием обратно к 0 после того, как триггер перестал быть активным.

  • triggers — Источник нагрузки и параметры конкретного скейлера.

По спецификации ScaledObject, pollingInterval по умолчанию составляет 30 секунд, cooldownPeriod по умолчанию 300 секунд, minReplicaCount по умолчанию 0, а maxReplicaCount по умолчанию 100. KEDA: ScaledObject specification.

Scale-to-zero как FinOps-механика

Scale-to-zero — одна из главных функций KEDA с точки зрения FinOps.

Если событий нет, очередь пуста, расписание не наступило или трафика нет, KEDA может уменьшить количество реплик приложения до 0. Это значит, что сервис не держит вычислительные ресурсы только потому, что «вдруг пригодится».

В обычной инфраструктурной логике часто появляется постоянный резерв:

min replicas: 2traffic: 0queue: 0cost: continues

С scale-to-zero логика меняется:

min replicas: 0traffic: 0queue: 0cost for workload compute: reduced

Здесь важно не переобещать. Scale-to-zero не означает, что счет за весь кластер станет нулевым. Control plane, системные компоненты, ноды, диски, логи, сеть и другие ресурсы могут продолжать стоить денег. Но для конкретного workload это снижает холостой простой.

FinOps-смысл простой: платить не за ожидание, а за обработку нагрузки.

Реальный кейс: фоновый воркер обработки заказов

Рассмотрим типичный сценарий — воркер, который обрабатывает заказы из RabbitMQ.

До внедрения KEDA

Конфигурация:

  • Deployment с фиксированным количеством реплик: 5

  • Режим работы: 24/7

  • Пиковая нагрузка: рабочие дни 10:00–18:00

  • Ночью и в выходные: очередь пустая

Метрики:

  • Постоянно: 5 реплик × 24 часа = 120 pod-hours/день

  • Средняя утилизация CPU: 8% (большую часть времени воркеры простаивают)

  • Очередь пустая ~18 часов в сутки

  • Стоимость compute для workload: ~$180/месяц (из расчета $0.05/pod-hour)

Проблема: платим за 5 подов круглосуточно, хотя реальная работа происходит 6–8 часов в день.

После внедрения KEDA

Конфигурация:

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: orders-worker-scalerspec:  scaleTargetRef:    name: orders-worker  minReplicaCount: 0  maxReplicaCount: 10  triggers:    - type: rabbitmq      metadata:        queueName: orders        mode: QueueLength        value: "20"

Поведение:

  • 10:00–18:00 (пик): очередь 150–200 сообщений → 8–10 реплик

  • 18:00–22:00 (спад): очередь 20–40 сообщений → 1–2 реплики

  • 22:00–10:00 + выходные: очередь пустая → 0 реплик

Метрики:

  • Среднее за день: ~40 pod-hours/день

  • Утилизация CPU в активное время: 45–60% (поды заняты работой)

  • Cold start при появлении первого сообщения: ~12 секунд

  • SLA на обработку сообщения (5 минут): не нарушен

  • Стоимость compute для workload: ~$60/месяц

Результат:

  • Экономия: 67% на этом workload ($120/месяц)

  • Утилизация выросла с 8% до 45–60%

  • SLA не пострадал — cold start укладывается в допустимые рамки

  • maxReplicaCount: 10 защищает от бесконтрольного роста при аномалиях

График поведения реплик

До KEDA: горизонтальная линия на уровне 5, после KEDA: пилообразный профиль с пиками днем и нулями ночью

До KEDA: горизонтальная линия на уровне 5, после KEDA: пилообразный профиль с пиками днем и нулями ночью

Этот кейс типичен для:

  • воркеров обработки заказов, уведомлений, email-рассылок

  • batch-задач по расписанию

  • внутренних сервисов с рабочими часами

  • dev/staging окружений

Сценарий №1. Очереди: от постоянного резерва к событийному масштабированию

Классический сценарий для KEDA — фоновые воркеры, которые разбирают сообщения из RabbitMQ, Kafka, SQS или другой очереди.

Допустим, есть orders-worker, который обрабатывает сообщения из очереди orders.

Нужно добиться такой логики:

0 сообщений -> 0 подов10 сообщений -> 1 под100 сообщений -> 10 подовочередь опустела -> поды удалились

Для RabbitMQ можно использовать trigger rabbitmq. В режиме QueueLength параметр value задает порог на одну реплику. Например, если один под должен обрабатывать 10 сообщений, значение можно поставить равным 10. В документации KEDA для RabbitMQ указано, что mode: QueueLength масштабирует по числу сообщений в очереди, а value задает threshold per instance. Там же приведен пример: при пороге 10 и 30 сообщениях скейлер поднимет до 3 подов. KEDA: RabbitMQ Queue scaler.

Безопасное хранение credentials

Credentials для подключения к RabbitMQ нельзя хардкодить в YAML.

❌ Плохо:

metadata:  host: "amqp://admin:password123@rabbitmq:5672/"

✅ Хорошо — Kubernetes Secret:

apiVersion: v1kind: Secretmetadata:  name: rabbitmq-connection  namespace: apptype: OpaquestringData:  host: amqp://user:password@rabbitmq.app.svc.cluster.local:5672/---apiVersion: keda.sh/v1alpha1kind: TriggerAuthenticationmetadata:  name: rabbitmq-auth  namespace: appspec:  secretTargetRef:    - parameter: host      name: rabbitmq-connection      key: host

✅✅ Еще лучше — External Secrets Operator:

Для production рекомендую использовать External Secrets Operator с Vault, AWS Secrets Manager или аналогами:

apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:  name: rabbitmq-connection  namespace: appspec:  secretStoreRef:    name: vault-backend    kind: SecretStore  target:    name: rabbitmq-connection  data:    - secretKey: host      remoteRef:        key: prod/rabbitmq        property: connection_string

Это позволяет:

  • Централизованно управлять секретами

  • Ротировать credentials без правки YAML

  • Аудитировать доступ к секретам

  • Не хранить пароли в Git

Пример ScaledObject для RabbitMQ

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: orders-worker-rabbitmq-scaler  namespace: appspec:  scaleTargetRef:    name: orders-worker  minReplicaCount: 0  maxReplicaCount: 10  pollingInterval: 30  cooldownPeriod: 300  triggers:    - type: rabbitmq      metadata:        protocol: amqp        queueName: orders        mode: QueueLength        value: "10"      authenticationRef:        name: rabbitmq-auth

Что здесь важно с точки зрения эксплуатации:

  1. maxReplicaCount: 10 ограничивает верхнюю границу роста

  2. minReplicaCount: 0 разрешает scale-to-zero

  3. value: "10" задает целевую длину очереди на одну реплику

  4. cooldownPeriod: 300 не удаляет поды мгновенно после снижения нагрузки

С точки зрения FinOps это убирает постоянный «горячий резерв» там, где он не нужен круглосуточно.

Сценарий №2. Расписание: выключаем dev-окружения на ночь

Не вся нагрузка приходит из очереди. Иногда она заранее известна:

  1. ночные ETL-процессы

  2. утренние отчеты

  3. рабочие часы B2B-сервиса

  4. внутренние сервисы, которые не нужны ночью

  5. dev- и staging-окружения

Для таких сценариев подходит cron scaler.

Допустим, B2B-сервис должен работать в рабочие дни с 08:50 до 19:00. В это окно нужно держать 3 реплики. Вне окна сервис можно опускать до 0.

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: b2b-api-office-hours  namespace: appspec:  scaleTargetRef:    name: b2b-api  minReplicaCount: 0  maxReplicaCount: 5  cooldownPeriod: 300  triggers:    - type: cron      metadata:        timezone: Europe/Moscow        start: "50 8 * * 1-5"        end: "0 19 * * 1-5"        desiredReplicas: "3"

Для масштабирования до 0 вне рабочего окна KEDA рекомендует указывать minReplicaCount: 0 в ScaledObject и увеличивать число реплик в рабочее время через desiredReplicas. Фактическое масштабирование вниз произойдет после cooldownPeriod, по умолчанию через 5 минут после end. KEDA: Cron scaler.

Почему это лучше bash-скриптов

Раньше для таких сценариев писали bash-скрипты:

#!/bin/bash# /opt/scripts/scale-dev-env.shif [ $(date +%H) -ge 19 ] || [ $(date +%H) -lt 9 ]; then  kubectl scale deployment/api --replicas=0 -n develse  kubectl scale deployment/api --replicas=3 -n devfi

И запускали через cron на каком-то сервере.

Проблемы такого подхода:

  • Кто владелец? Скрипт может работать годами, пока не сломается. Тогда выясняется, что автор уволился 2 года назад.

  • Где документация? Почему 3 реплики? Почему 19:00? Почему этот namespace?

  • Где версионность? Скрипт лежит на сервере, не в Git.

  • Где observability? Упал скрипт или отработал — непонятно без ручной проверки.

  • Боязнь трогать: “Работает — не трожь” превращается в technical debt.

Cron Scaler не отменяет необходимость нормального ownership, но переносит расписание в понятную Kubernetes-конфигурацию:

  • Декларативная конфигурация в Git

  • Стандартный lifecycle через kubectl/GitOps

  • События в Kubernetes Events

  • Метрики KEDA в Prometheus

Сценарий №3. Масштабирование по метрикам Prometheus

Не всегда источник нагрузки — это очередь или расписание. Иногда нужная метрика уже есть в Prometheus.

Например, можно масштабировать API по количеству HTTP-запросов в секунду (RPS):

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: api-rps-scaler  namespace: appspec:  scaleTargetRef:    name: api-service  minReplicaCount: 1  maxReplicaCount: 20  pollingInterval: 30  cooldownPeriod: 300  triggers:    - type: prometheus      metadata:        serverAddress: http://prometheus.monitoring.svc.cluster.local:9090        query: sum(rate(http_requests_total{deployment="api-service"}[2m]))        threshold: "100"

По документации KEDA, Prometheus scaler масштабирует workload по запросу к Prometheus. При этом query должен возвращать один scalar или vector single element response, а threshold задает порог для масштабирования. KEDA: Prometheus scaler.

Важные проверки перед production

В этом сценарии важно проверить саму метрику:

  1. Query должен возвращать один результат — scalar или vector с одним элементом

  2. Метрика должна быть стабильной — без провалов и дублирования

  3. Окно rate должно соответствовать характеру нагрузки:

    • [2m] для быстрой реакции на пики

    • [5m] для более плавного масштабирования

  4. Threshold нужно подбирать по нагрузочному тесту, а не на глаз:

    • Запустить нагрузочное тестирование

    • Замерить RPS, который выдерживает 1 под

    • Поставить threshold с запасом (например, 70–80% от максимума)

  5. maxReplicaCount должен учитывать лимиты:

    • Пропускная способность базы данных

    • Rate limits внешних API

    • Лимиты connections pool

Примеры других метрик для масштабирования

# Масштабирование по задержкам (latency)query: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service="api"}[5m]))threshold: "0.5"  # 500ms на p95# Масштабирование по количеству активных соединенийquery: sum(nginx_connections_active{deployment="nginx"})threshold: "1000"# Масштабирование по длине очереди в Redisquery: redis_list_length{list="tasks"}threshold: "50"# Масштабирование по custom business-метрикеquery: sum(rate(orders_created_total[5m]))threshold: "10"  # 10 заказов/сек на реплику

Для FinOps это важный сценарий, потому что масштабирование можно привязать ближе к бизнес-нагрузке — RPS, конверсия, количество активных пользователей — а не только к CPU.

Сценарий №4. HTTP-трафик и краткие пики нагрузки

Маркетинговая акция, email-рассылка или внешний инфоповод могут резко увеличить трафик в несколько раз. Держать сервис постоянно готовым к такому пику дорого, потому что большую часть времени эта мощность будет простаивать.

Для HTTP-нагрузок у KEDA есть HTTP Add-on. Он масштабирует HTTP workloads по входящему трафику и поддерживает сценарии scale-to-zero.

Как работает HTTP Add-on

┌──────────┐      ┌─────────────────┐      ┌─────────────┐│ Ingress  │─────▶│ HTTP Interceptor│─────▶│ Application ││          │      │ (queue proxy)   │      │ Pods        │└──────────┘      └─────────────────┘      └─────────────┘                           │                           ▼                  ┌─────────────────┐                  │ KEDA Scaler     │                  │ (по RPS/pending)│                  └─────────────────┘

HTTP Interceptor перехватывает запросы:

  • Если поды есть → проксирует запросы

  • Если подов 0 → ставит запросы в очередь, сигналит KEDA → поды поднимаются → запросы передаются

Пример конфигурации

apiVersion: http.keda.sh/v1alpha1kind: HTTPScaledObjectmetadata:  name: api-http-scaler  namespace: appspec:  scaleTargetRef:    name: api-service    kind: Deployment    service: api-service    port: 8080  replicas:    min: 0    max: 20  scalingMetric:    requestRate:      targetValue: 100      window: 1m

Production checklist для HTTP Add-on

Перед раскаткой на production важно проверить:

1. Cold start

  • Сколько времени требуется от 0 до первого ready-пода?

  • Укладывается ли в timeout клиентов?

  • Настроен ли initialDelaySeconds в readiness probe?

2. Таймауты

  • Timeout на Ingress

  • Timeout на HTTP Interceptor

  • Timeout клиента

  • Должны быть согласованы: client > interceptor > ingress > pod

3. Readiness probes

readinessProbe:  httpGet:    path: /health    port: 8080  initialDelaySeconds: 5  periodSeconds: 3  failureThreshold: 2

4. Лимиты соединений

  • Проверить connection pool базы данных

  • Проверить rate limits внешних API

  • Настроить maxReplicaCount с учетом этих лимитов

5. План отката

# Быстрый откат: ставим min replicas обратноkubectl patch httpscaledobject api-http-scaler -n app \  --type='json' -p='[{"op": "replace", "path": "/spec/replicas/min", "value": 2}]'# Или полное отключение KEDA для этого workloadkubectl delete httpscaledobject api-http-scaler -n appkubectl scale deployment/api-service --replicas=3 -n app

Верхняя граница: maxReplicaCount как финансовый предохранитель

FinOps — это не только экономия в простое. Важно еще ограничивать расходы сверху.

Автоскейлинг без верхней границы может разогнать счет, если:

  1. в коде появился баг (бесконечный retry, memory leak)

  2. очередь начала расти из-за деградации внешнего сервиса

  3. метрика настроена слишком агрессивно

  4. приложение попало под аномальную нагрузку (DDoS, scraping)

  5. downstream-система стала отвечать медленнее → воркеры висят дольше → KEDA поднимает еще воркеров

maxReplicaCount задает максимальное количество реплик, выше которого workload не поднимется:

spec:  minReplicaCount: 0  maxReplicaCount: 10

Как правильно выбрать maxReplicaCount

Здесь важно смотреть на систему целиком:

❌ Слишком низко:

  • Защитил счет на облако

  • Но получил очередь, задержки, нарушение SLA

  • Downstream-системы в порядке, а ты не обрабатываешь

❌ Слишком высоко:

  • Защитил SLA

  • Но увеличил финансовый риск

  • Можешь задавить базу или внешний API

✅ Правильный подход:

  1. Замерить пропускную способность сервиса:

    # Нагрузочное тестирование# Сколько RPS выдерживает 1 под?# Сколько сообщений/сек обрабатывает 1 воркер?
  2. Проверить лимиты зависимостей:

    PostgreSQL connection pool: 100Если каждый под держит 10 connections→ максимум 10 подов
  3. Добавить запас на пики:

    Обычная нагрузка: 5 подовПиковая нагрузка: 8 подов→ maxReplicaCount: 12 (50% запас)
  4. Настроить алерт на приближение к лимиту:

    # Prometheus alert- alert: KEDANearMaxReplicas  expr: keda_scaledobject_replicas / keda_scaledobject_max_replicas > 0.8  for: 5m  annotations:    summary: "ScaledObject {{ $labels.scaledobject }} at 80% of maxReplicaCount"

Нормальный подход — не «поставить красивое число», а связать лимит с реальными возможностями инфраструктуры.

cooldownPeriod и плавное масштабирование вниз

cooldownPeriod задает паузу перед масштабированием обратно к 0 после того, как триггер перестал быть активным.

spec:  cooldownPeriod: 300  # 5 минут

Это значит, что после прекращения активности KEDA не будет сразу уводить workload в 0. Пауза помогает избежать ситуации, когда поды создаются и удаляются слишком часто из-за кратких колебаний нагрузки (flapping).

Production-настройка с плавным scale down

apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata:  name: api-rps-scaler  namespace: appspec:  scaleTargetRef:    name: api-service  minReplicaCount: 1  maxReplicaCount: 20  cooldownPeriod: 300  advanced:    horizontalPodAutoscalerConfig:      behavior:        scaleDown:          stabilizationWindowSeconds: 300          policies:            - type: Percent              value: 50              periodSeconds: 60            - type: Pods              value: 2              periodSeconds: 60          selectPolicy: Min  # Выбирать более консервативную политику        scaleUp:          stabilizationWindowSeconds: 0          policies:            - type: Percent              value: 100              periodSeconds: 15            - type: Pods              value: 4              periodSeconds: 15          selectPolicy: Max  # Выбирать более агрессивную политику  triggers:    - type: prometheus      metadata:        serverAddress: http://prometheus.monitoring.svc.cluster.local:9090        query: sum(rate(http_requests_total{deployment="api-service"}[2m]))        threshold: "100"

Что здесь происходит:

Scale Down (консервативный):

  • stabilizationWindowSeconds: 300 — смотреть на метрики за последние 5 минут

  • Уменьшать либо на 50% либо на 2 пода (что меньше) каждую минуту

  • selectPolicy: Min — выбирать более осторожный вариант

Scale Up (агрессивный):

  • stabilizationWindowSeconds: 0 — реагировать мгновенно

  • Увеличивать либо на 100% либо на 4 пода (что больше) каждые 15 секунд

  • selectPolicy: Max — выбирать более быструю реакцию

Такая конфигурация готова к production: быстро растем под нагрузкой, медленно сжимаемся при спаде.

Мониторинг KEDA в production

Внедрить KEDA без мониторинга — значит летать вслепую. Крайне рекомендую сделать дашборд с алертами, примеры под катом:

Запросы и алерты
# Количество активных ScaledObjectskeda_scaler_active# Текущее количество репликkeda_scaledobject_replicas# Максимальное количество реплик (из spec)keda_scaledobject_max_replicas# Текущее значение метрики скейлераkeda_scaler_metrics_value# Количество ошибок скейлераrate(keda_scaler_errors_total[5m])# Latency проверки скейлераkeda_scaler_metrics_latency

Обязательные алерты

1. Упор в maxReplicaCount

- alert: KEDAMaxReplicasReached  expr: keda_scaledobject_replicas >= keda_scaledobject_max_replicas  for: 5m  labels:    severity: warning  annotations:    summary: "ScaledObject {{ $labels.scaledobject }} reached maxReplicaCount"    description: "Workload hit the ceiling. Check if queue/load is growing."

2. Ошибки скейлера

- alert: KEDAScalerErrors  expr: rate(keda_scaler_errors_total[5m]) > 0  for: 2m  labels:    severity: critical  annotations:    summary: "KEDA scaler {{ $labels.scaler }} has errors"    description: "KEDA cannot fetch metrics. Autoscaling may be broken."

3. Рост очереди при максимальных репликах

- alert: QueueGrowingAtMaxScale  expr: |    keda_scaler_metrics_value{scaler="rabbitmq"} > 1000    and    keda_scaledobject_replicas >= keda_scaledobject_max_replicas  for: 10m  labels:    severity: critical  annotations:    summary: "Queue growing but already at max replicas"    description: "Need to increase maxReplicaCount or optimize workers."

4. Долгий cold start

- alert: SlowColdStart  expr: |    avg_over_time(keda_scaledobject_replicas[5m]) == 0    and    avg_over_time(some_app_request_duration_seconds[5m]) > 30  for: 5m  annotations:    summary: "Cold start taking too long"    description: "App scaled from 0 but startup is slow. Check image size, probes."

Debugging: что смотреть когда не работает

# 1. Логи KEDA operatorkubectl logs -n keda deployment/keda-operator -f# 2. Логи metrics serverkubectl logs -n keda deployment/keda-metrics-apiserver -f# 3. Статус ScaledObjectkubectl describe scaledobject <name> -n <namespace># Смотрим на поля:# - Status.Conditions — есть ли ошибки# - Status.ScaleTargetKind — правильный ли target# - Events — что происходило недавно# 4. HPA созданный KEDAkubectl get hpa -n <namespace>kubectl describe hpa keda-hpa-<name> -n <namespace># Проверяем:# - Current metrics# - Desired replicas vs Current replicas# - Conditions# 5. События в namespacekubectl get events -n <namespace> --sort-by='.lastTimestamp' | tail -20# 6. Проверить доступность источника метрик# Для Prometheus scaler:kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \  curl "http://prometheus.monitoring.svc:9090/api/v1/query?query=your_query"# Для RabbitMQ scaler:kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \  curl "http://user:pass@rabbitmq.app.svc:15672/api/queues/%2F/orders"

Типичные проблемы и решения

1. Cold start съедает SLA

Проблема: Первый запрос после scale-to-zero ждет 30+ секунд.

Причины:

  • Большой Docker-образ

  • Долгая инициализация приложения

  • База данных требует warm-up connection pool

  • Загрузка конфигов/секретов

Решения:

а) Если критично — отключить scale-to-zero:

minReplicaCount: 1  # вместо 0

б) Оптимизировать образ:

Пример оптимизации для python имеджа
# Multi-stage buildFROM python:3.11-slim as builderRUN pip install --user package1 package2FROM python:3.11-slimCOPY --from=builder /root/.local /root/.localCOPY app.py .CMD ["python", "app.py"]# Результат: образ 150MB вместо 800MB# Cold start: 8 секунд вместо 25

в) Настроить readiness probe:

readinessProbe:  httpGet:    path: /health    port: 8080  initialDelaySeconds: 10  periodSeconds: 2  failureThreshold: 3

г) Для HTTP Add-on — буферизация: HTTP Interceptor может держать запросы в очереди, пока поды поднимаются.

2. Очередь растет быстрее чем scale up

Проблема: Сообщения прибывают со скоростью 1000/сек, а поды поднимаются ~30 сек. Очередь растет быстрее чем обрабатывается.

Решения:

а) Уменьшить pollingInterval:

pollingInterval: 10  # вместо 30

б) Увеличить maxReplicaCount с запасом:

maxReplicaCount: 20  # вместо 10

в) Агрессивная политика scale up:

advanced:  horizontalPodAutoscalerConfig:    behavior:      scaleUp:        stabilizationWindowSeconds: 0        policies:          - type: Percent            value: 100            periodSeconds: 15

г) Проверить лимиты downstream:

Если база данных выдерживает только 100 connections,а каждый под держит 10 connections,то больше 10 подов = ошибки подключенияРешение:- Увеличить connection pool базы- Оптимизировать использование connections в коде- Использовать connection pooler (PgBouncer)

3. Flapping: KEDA создает/удаляет поды каждые 2 минуты

Проблема: Нагрузка колеблется вокруг порога. Поды постоянно создаются и удаляются.

Симптомы:

10:00 - 5 pods10:02 - 3 pods10:04 - 5 pods10:06 - 3 pods...

Решения:

а) Увеличить cooldownPeriod:

cooldownPeriod: 600  # 10 минут вместо 5

б) Использовать stabilizationWindow:

advanced:  horizontalPodAutoscalerConfig:    behavior:      scaleDown:        stabilizationWindowSeconds: 600

в) Пересмотреть threshold:

# Былоvalue: "10"  # слишком чувствительно# Сталоvalue: "20"  # более грубая гранулярность

г) Добавить гистерезис через разные метрики: Использовать разные пороги для scale up и scale down (пока не поддерживается напрямую в KEDA, но можно через composite scaler).

4. KEDA не видит метрику из Prometheus

Проблема:

ScaledObject Status:  Conditions:    Message: error getting metric: query returned no data

Решения:

а) Проверить query вручную:

kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \  curl "http://prometheus.monitoring.svc:9090/api/v1/query?query=sum(rate(http_requests_total[2m]))"

б) Проверить формат результата:

KEDA требует scalar или vector с одним элементом:✅ Хорошо:{  "data": {    "result": [{"value": [1234567890, "42.5"]}]  }}❌ Плохо (несколько элементов):{  "data": {    "result": [      {"metric": {"pod": "api-1"}, "value": [1234567890, "10"]},      {"metric": {"pod": "api-2"}, "value": [1234567890, "15"]}    ]  }}

в) Добавить aggregation:

# Былоquery: rate(http_requests_total{deployment="api"}[2m])# Сталоquery: sum(rate(http_requests_total{deployment="api"}[2m]))

г) Проверить доступность Prometheus:

kubectl get svc -n monitoring# Убедиться что serverAddress корректный

5. maxReplicaCount срабатывает постоянно

Проблема: Workload регулярно упирается в maxReplicaCount: 10, но нагрузка продолжает расти.

Диагностика:

# Сколько времени workload на максимумеavg_over_time((keda_scaledobject_replicas == keda_scaledobject_max_replicas)[1h:])# Растет ли очередьrate(rabbitmq_queue_messages[5m]) > 0

Решения:

а) Увеличить maxReplicaCount (если инфраструктура позволяет):

maxReplicaCount: 20

б) Оптимизировать код:

  • Профилировать узкие места

  • Уменьшить время обработки одного сообщения

  • Добавить батчинг

в) Вертикальное масштабирование:

resources:  requests:    cpu: 500m  # было 200m    memory: 1Gi  # было 512Mi

г) Проверить downstream: Возможно проблема не в воркерах, а в базе данных или внешнем API.

Доказываем экономику: какие метрики собирать до и после

Самая частая ошибка — внедрить автоскейлинг и считать, что экономия появилась сама.

Для FinOps этого недостаточно. Нужно заранее договориться, что именно сравниваем до и после.

Технические метрики

Утилизация ресурсов:  - pod-hours (реплик × часов работы)  - replica count over time  - CPU utilization (среднее, p50, p95)  - memory utilization (среднее, p50, p95)Бизнес-нагрузка:  - queue length (среднее, max)  - oldest message age  - request rate (RPS)  - throughput (сообщений/сек, заказов/час)Производительность:  - latency (p50, p95, p99)  - error rate  - success rate  - cold start time (p95, p99)  - time to scale (0→N реплик)События масштабирования:  - scale up events count  - scale down events count  - time at 0 replicas  - time at max replicas

Финансовые метрики

Важно: заранее договориться, какой тип cost view используем.

Если одна команда смотрит blended cost, другая unblended, а третья amortized, обсуждение быстро превращается в спор о цифрах вместо анализа решения.

Cost dimensions:  - Стоимость namespace (до/после)  - Стоимость workload (до/после)  - Стоимость node pool (до/после)  - Стоимость idle capacity  - Стоимость за единицу работы (cost per order, cost per message)Cost types (выбрать один):  - Blended cost (смешанная: on-demand + spot + savings plans)  - Unblended cost (чистая: без учета скидок)  - Amortized cost (амортизированная: распределение reserved instances)  - Net cost (с учетом всех скидок и кредитов)Time periods:  - 1 неделя до внедрения  - 1 месяц до внедрения  - 1 неделя после внедрения  - 1 месяц после внедрения

Операционные метрики

Stability:  - количество инцидентов  - количество нарушений SLA  - MTTR (mean time to recovery)Toil:  - количество ручных вмешательств  - количество manual scale операций  - время на troubleshooting autoscalingReaction time:  - время реакции на пик нагрузки  - стабильность scale up  - стабильность scale down  - количество flapping events

Пример дашборда для замера эффекта

Before KEDA (week 1-2)- **Pod-hours:** 336/week (5 pods × 24h × 7d)- **Avg CPU utilization:** 8%- **Cost:** $168/week- **Manual interventions:** 3 (weekend scale-down)After KEDA (week 3-4)- **Pod-hours:** 112/week- **Avg CPU utilization:** 52%- **Cost:** $56/week- **Manual interventions:** 0- **Cold start p95:** 12 seconds- **SLA violations:** 0Result- **Cost savings:** 67% ($112/week, $5,824/year for this workload)- **Utilization improvement:** 8% → 52%- **Toil reduction:** 3 manual ops → 0- **SLA:** no impact

С чего начать пилот

KEDA не нужно внедрять сразу во всю архитектуру.

Безопаснее идти по шагам.

Шаг 1. Найти подходящий workload

✅ Хорошие кандидаты для первого пилота:

  1. Фоновые воркеры

    • Обработка очередей (email, уведомления, отчеты)

    • Batch-задачи

    • ETL-процессы

  2. Dev/staging окружения

    • Не нужны ночью и в выходные

    • Низкий риск для production

    • Быстро покажут экономию

  3. Сервисы с рабочими часами

    • B2B API (9:00–18:00)

    • Внутренние инструменты

    • Admin-панели

  4. Внутренние API с понятным профилем нагрузки

    • Стабильная метрика (RPS, queue length)

    • Известные пиковые окна

    • Нет жестких SLA

❌ Плохие кандидаты для первого пилота:

  1. Критичный production с жестким SLA

  2. Stateful workloads (базы данных)

  3. Сервисы с долгим cold start (>30 сек)

  4. Сервисы с множеством внешних зависимостей

  5. Монолит с shared state

Шаг 2. Проверить источник событий

Перед настройкой нужно ответить на вопросы:

Источник метрики:  ✓ Есть ли стабильная метрика нагрузки?  ✓ Кто владеет этой метрикой?  ✓ Можно ли ей доверять?  ✓ Что будет, если источник метрики недоступен?  ✓ Какой лаг между событием и реакцией?Для очередей (RabbitMQ, Kafka, SQS):  ✓ Можем получить длину очереди?  ✓ Есть credentials для подключения?  ✓ Есть ли мониторинг очереди?Для Prometheus:  ✓ Query возвращает один скаляр?  ✓ Метрика не проваливается?  ✓ Есть алерт на отсутствие метрики?Для cron:  ✓ Расписание согласовано с командой?  ✓ Timezone корректный?

Если метрика нестабильна, KEDA будет честно масштабироваться по плохому сигналу.

Шаг 3. Задать границы

Для пилота нужно явно указать базовые параметры масштабирования:

minReplicaCount: 0      # или 1 если cold start критиченmaxReplicaCount: 10     # выбрать по capacity planningpollingInterval: 30     # как часто проверятьcooldownPeriod: 300     # пауза перед scale-to-zero

Как выбрать значения:

minReplicaCount:  # 0 → если cold start < 15 сек и SLA позволяет  # 1 → если нужна мгновенная реакция  # 2+ → если нужна redundancymaxReplicaCount:  # capacity = max_expected_load / throughput_per_pod  # + 20-50% запас  # Пример: 1000 msg/sec, 100 msg/sec на под → 10 подов + 50% = 15pollingInterval:  # 30 сек → для плавной нагрузки  # 10-15 сек → для быстрых пиков  # 5 сек → для critical workloads (но больше нагрузки на источник метрик)cooldownPeriod:  # 300 сек (5 мин) → стандарт  # 600 сек (10 мин) → для workloads с частыми колебаниями  # 120 сек (2 мин) → для dev-окружений

Шаг 4. Настроить observability

До запуска стоит подготовить дашборд:

Обязательные панели:

  • replicas over time

  • scaler metric value (queue length / RPS / etc)

  • latency (p50, p95, p99)

  • error rate

  • CPU / memory utilization

FinOps панели:

  • pod-hours per day

  • cost per day (если есть cost allocation)

  • cost per message/request

KEDA-специфичные:

  • keda_scaler_errors_total

  • distance to max replicas

  • time at 0 replicas

  • time at max replicas

Алерты:

Critical:

  • KEDA scaler errors

  • Max replicas reached + queue growing

  • Cold start > SLA threshold

Warning:

  • Distance to max replicas < 20%

  • Flapping (scale up/down each 5 min)

  • Queue age > SLA threshold

Без такого дашборда будет сложно понять, KEDA действительно снизил простой или просто перенес проблему в задержки.

Шаг 5. Запустить пилот на 1-2 недели

Шаг 6. Собрать данные и сравнить

Через 2 недели собрать данные:

Metric

Before

After

Change

Pod-hours/week

336

112

-67%

Avg CPU util

8%

52%

+44pp

Cost/week

$168

$56

-$112

P95 latency

120ms

150ms

+30ms

Error rate

0.01%

0.01%

SLA violations

0

0

Manual ops

3

0

-3

Шаг 7. Решить: раскатывать дальше или откатить

✅ Раскатывать дальше если:

  • Экономия подтверждена цифрами

  • SLA не пострадал

  • Команда понимает как работает

  • Observability настроена

  • Нет блокирующих проблем

⏸️ Доработать если:

  • Cold start слишком долгий → оптимизировать образ

  • Flapping → настроить behavior

  • Упираемся в max replicas → capacity planning

❌ Откатить если:

  • SLA нарушен

  • Проблемы с источником метрик

  • Команда не может troubleshoot

  • Экономика не подтвердилась

Что проверить перед production

Перед раскаткой на production стоит пройти короткий чек-лист:

Application readiness:  ✓ Сервис можно безопасно опускать до 0?  ✓ Cold start укладывается в SLA?  ✓ Приложение stateless?  ✓ Нет in-memory state между запросами?  ✓ Нет dependency на конкретный pod IP?Kubernetes configuration:  ✓ Readiness probe настроен корректно?  ✓ Liveness probe не убивает pod во время cold start?  ✓ Resource requests и limits заданы?  ✓ PodDisruptionBudget настроен?  ✓ Anti-affinity rules учтены?KEDA configuration:  ✓ minReplicaCount выбран обоснованно?  ✓ maxReplicaCount учитывает лимиты downstream?  ✓ pollingInterval адекватен скорости нагрузки?  ✓ cooldownPeriod не слишком агрессивный?  ✓ Trigger метрика стабильна?Dependencies:  ✓ Понятны лимиты базы данных?  ✓ Понятны лимиты брокера сообщений?  ✓ Понятны rate limits внешних API?  ✓ Connection pools настроены с запасом?Observability:  ✓ Дашборд для workload готов?  ✓ Алерты настроены и протестированы?  ✓ Логи агрегируются корректно?  ✓ Distributed tracing работает?Ownership:  ✓ Известно, кто владеет ScaledObject?  ✓ Есть runbook для troubleshooting?  ✓ Команда понимает как откатиться?  ✓ Есть алерт на рост очереди?  ✓ Есть алерт на упор в maxReplicaCount?FinOps:  ✓ Cost allocation tags проставлены?  ✓ Договорились какой cost view используем?  ✓ Baseline метрики собраны?  ✓ План замера эффекта через месяц?Rollback plan:  ✓ Известен план отката?  ✓ Протестирован откат на staging?  ✓ Команда знает как быстро вернуть fixed replicas?

Если хотя бы часть ответов неизвестна, это не значит, что KEDA нельзя внедрять. Это значит, что пилот нужно запускать на менее критичном участке.

Когда НЕ нужен KEDA

KEDA — не silver bullet. Есть сценарии, где он избыточен:

  • ❌ Стабильная нагрузка 24/7 Если сервис всегда держит 10 реплик и нагрузка не меняется — HPA по CPU достаточно.

  • ❌ Stateful workloads (базы данных) Scale-to-zero для PostgreSQL — плохая идея. Вертикальное масштабирование (VPA, bigger nodes) часто лучше.

  • ❌ Очень короткие задачи (< 1 секунда) Если задача выполняется за 100ms, overhead на создание пода (5-15 сек) слишком велик. Лучше держать min replicas или использовать serverless (Knative, Lambda).

  • ❌ Критичные сервисы с SLA < 1 секунды Cold start не укладывается в такие SLA. Нужен постоянный warm pool.

Q&A

Q&A

Q: KEDA заменяет HPA?

A: Нет, дополняет. KEDA создает HPA под капотом и передает ему external metrics через Metrics API Server. HPA управляет количеством реплик, KEDA — источником метрик.


Q: Можно ли использовать на stateful workloads (базы данных)?

A: С осторожностью. Scale-to-zero для БД обычно плохая идея (долгий startup, потеря кеша, state). Но:

  • ✅ Масштабировать read-replica по нагрузке — можно

  • ✅ Dev-окружения с БД на ночь опускать — можно

  • ❌ Production master БД в scale-to-zero — нет


Q: Что будет если KEDA упадет?

A: HPA продолжит работать с последней известной метрикой. Но:

  • Новые ScaledObject не создадутся

  • Метрики перестанут обновляться

  • Workload останется на текущем количестве реплик до восстановления KEDA

Критично: мониторить состояние keda-operator и keda-metrics-apiserver pods.


Q: Работает ли в on-premise Kubernetes?

A: Да, KEDA не привязан к облаку. Работает в любом Kubernetes (on-premise, cloud, edge). Главное — чтобы источник метрик был доступен (RabbitMQ, Prometheus, etc).


Q: Сколько стоит KEDA?

A: Open source, бесплатно (Apache 2.0 license). Платите только за саму инфраструктуру (compute, storage, network)


Q: Можно ли комбинировать несколько триггеров?

A: Да, ScaledObject поддерживает несколько triggers. Можно масштабироваться по очереди И по расписанию одновременно:

triggers:  - type: rabbitmq    metadata:      queueName: orders      value: "10"  - type: prometheus    metadata:      query: sum(rate(http_requests_total[2m]))      threshold: "100"

KEDA возьмет максимум из всех триггеров.


Q: Как откатиться если KEDA сломал production?

A: Быстрый откат:

# 1. Удалить ScaledObject (HPA тоже удалится)kubectl delete scaledobject <name> -n <namespace># 2. Вернуть фиксированное количество репликkubectl scale deployment/<name> --replicas=5 -n <namespace># 3. Или применить старый манифест Deploymentkubectl apply -f deployment-before-keda.yaml

Важно: держите backup манифестов до внедрения KEDA.

Что в итоге

KEDA не делает FinOps за команду. Он не заменяет:

  • Правила аллокации затрат

  • Ownership и ответственность

  • Бюджетные алерты

  • Регулярный анализ cloud bill

  • Договоренности между командами

  • Cost allocation tags и chargeback

Но он хорошо закрывает конкретный инженерный слой финансовых гардрейлов в Kubernetes.

KEDA помогает:

  • Масштабировать по фактической нагрузке Очереди, события, расписание, метрики — а не только CPU/Memory

  • Не держать поды включенными “на всякий случай” Scale-to-zero убирает холостой простой

  • Ограничивать верхнюю границу через maxReplicaCount Технический предохранитель от бесконтрольного роста

  • Управлять поведением scale down cooldownPeriod + HPA behavior для плавного масштабирования

  • Проверять экономику через пилот Месяц на dev/staging → цифры → решение о production

  • Снижать toil Автоматизация вместо bash-скриптов и ручных scale операций

Главная мысль

Инфраструктура не должна круглосуточно ждать редкий пик, если это не требуется бизнесом и SLA.

Она должна:

  • Включаться под работу → масштабироваться по событиям

  • Выключаться в простое → scale-to-zero когда нагрузки нет

  • Не расти бесконечно → maxReplicaCount как предохранитель

В этом смысле KEDA — это не просто автоскейлер. Это технический механизм, который помогает связать Kubernetes-нагрузку с финансовой логикой:

  • Меньше холостого простоя

  • Понятнее верхние лимиты

  • Прозрачнее эффект от оптимизации

Что дальше

Если у вас есть:

  • ✅ Воркеры с очередями, которые простаивают ночью

  • ✅ Dev/staging окружения работающие 24/7

  • ✅ Внутренние сервисы с рабочими часами

  • ✅ Batch-задачи по расписанию

  • ✅ API с пиковой нагрузкой в определенные часы

…попробуйте KEDA на одном workload.

План первых шагов:

  1. Выбрать пилотный workload (dev-воркер с очередью)

  2. Установить KEDA через Helm за 5 минут

  3. Создать ScaledObject с консервативными параметрами

  4. Настроить мониторинг (дашборд + алерты)

  5. Запустить на 2 недели и собрать метрики

  6. Сравнить до/после и принять решение

Месяц пилота даст:

  • Конкретные цифры экономии (pod-hours, $)

  • Понимание подводных камней до раската на весь кластер

  • Опыт команды с event-driven autoscaling

  • Baseline для дальнейшей оптимизации

Полезные ссылки

Документация:

Буду рад обсудить опыт внедрения KEDA в комментариях или в личных сообщениях.


Материал подготовлен в рамках серии публикаций для комьюнити «Практики FinOps», где мы разбираем прикладные подходы к управлению инфраструктурными затратами, cloud-cost и FinOps-практикам в инженерных командах.

Практики FinOps в Телеграмм.

ссылка на оригинал статьи https://habr.com/ru/articles/1056066/