У вас Kafka-топик, консьюмеры разгребают сообщения бэтчами, и нагрузка скачет от нуля ночью до тысяч сообщений в минуту в пиковый час. HPA по CPU в этой картине почти бесполезен: под пустой очередью консьюмер не грузит процессор, но простаивающие поды никто не остановит, а под резким всплеском метрика CPU догоняет реальность с опозданием в несколько минут. KEDA решает задачу иначе — скейлит по самому событию: глубине очереди, значению метрики, cron-расписанию — и, в отличие от HPA, умеет гасить нагрузку до нуля, когда работы нет вообще.
Содержание
Открыть содержание
Почему HPA не отвечает на вопрос «сколько воркеров нужно»
Horizontal Pod Autoscaler в Kubernetes изначально проектировался под один сценарий: веб-сервис, у которого нагрузка более-менее коррелирует с потреблением CPU или памяти. Больше запросов — больше CPU — HPA добавляет реплики. Для очередей, батчевой обработки и cron-подобных нагрузок эта корреляция ломается:
- Консьюмер очереди может простаивать с CPU в 2% при 10 000 необработанных сообщениях в Kafka — он просто ждёт ответа от медленного downstream-сервиса.
- Батчевая джоба нагружает CPU только в момент реальной обработки, а между запусками не должна занимать ресурсы вообще.
- CPU-метрика запаздывает: HPA усредняет её по окну в несколько минут, а очередь может вырасти за секунды.
Ещё жёстче системное ограничение: HPA не умеет масштабировать Deployment до нуля реплик. Это не баг, а архитектурное решение — HPA управляет уже существующими подами и им же нужен минимум один под, чтобы вообще было что мерить. Значит, любая нагрузка «то густо, то пусто» либо держит нулевые по факту, но оплаченные реплики круглые сутки, либо скейлится вручную.
Что KEDA делает поверх HPA
KEDA (Kubernetes Event-Driven Autoscaling) — graduated-проект CNCF, который не заменяет HPA, а достраивает его в двух направлениях сразу.
Во-первых, KEDA приносит более 60 scalers — коннекторов к источникам событий: Kafka, RabbitMQ, Prometheus, AWS SQS, PostgreSQL, cron-расписание и десятки других. Каждый scaler умеет превратить «глубину очереди» или «значение метрики» в число, понятное Kubernetes.
Во-вторых, и это ключевое отличие, KEDA решает проблему нуля. Под капотом это выглядит так: вы описываете ScaledObject — CRD, который ссылается на ваш Deployment или StatefulSet и задаёт триггеры. KEDA-оператор в ответ на это сам создаёт стандартный Kubernetes HPA, подключённый к KEDA как к External Metrics API — то есть с точки зрения Kubernetes ничего экзотического не происходит, обычный HPA просто питается метрикой, которую поставляет KEDA. Но переход между 0 и 1 репликой HPA сделать не может в принципе — этим занимается отдельная логика самого KEDA-оператора: пока реплик 0, HPA не создаётся вообще, а KEDA сам следит за источником событий и при появлении активности поднимает Deployment до 1 реплики, передавая управление дальше уже штатному HPA.
Для одноразовых задач вместо ScaledObject есть ScaledJob — он создаёт по Kubernetes Job на каждую единицу работы (например, на каждое сообщение в очереди) вместо того, чтобы масштабировать долгоживущий Deployment.
Сравнение
| HPA | KEDA | |
|---|---|---|
| Источник метрики | CPU, память, кастомные метрики через ручной adapter | 60+ встроенных scalers: очереди, БД, метрики, cron |
| Минимум реплик | 1 (архитектурно не может быть 0) | 0 — есть отдельная логика активации |
| Что реально скейлит | Deployment/StatefulSet напрямую | создаёт и обслуживает HPA + свой 0↔1 контроллер |
| Расписание (cron) | нет | встроенный cron scaler |
| Кастомные метрики | нужен свой Metrics/External adapter | External Metrics API из коробки |
| Одноразовые задачи по событию | нет | ScaledJob — Job на единицу работы |
Что нужно, чтобы завести автоскейлинг по Kafka
Ставим KEDA в кластер (одна установка на кластер, дальше ScaledObject заводится на каждый workload отдельно):
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace
Дальше — ScaledObject, который скейлит Deployment консьюмера по глубине топика orders и гасит его до нуля, когда очередь пуста:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
namespace: default
spec:
scaleTargetRef:
name: order-consumer
minReplicaCount: 0
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-broker:9092
consumerGroup: order-consumer-group
topic: orders
lagThreshold: "50"
activationLagThreshold: "1"
authenticationRef:
name: kafka-trigger-auth
Два порога здесь делают разные вещи: lagThreshold — это значение, которое KEDA передаёт в HPA как target для масштабирования от 1 до maxReplicaCount (лаг 500 при пороге 50 → HPA целится в 10 реплик). activationLagThreshold — отдельный, более чувствительный порог именно для перехода 0→1: он может быть намного меньше, чтобы консьюмер поднимался при первом же сообщении, а не ждал, пока лаг накопится до полноценного scaling-порога.
Как проверить, что скейлинг работает
KEDA не прячет свою логику — она видна как обычные Kubernetes-объекты:
# ScaledObject существует и активен
kubectl get scaledobject order-consumer -o wide
# HPA, который KEDA создал и обслуживает сама
kubectl get hpa keda-hpa-order-consumer
# статус активации и условия — почему KEDA считает источник активным/неактивным
kubectl describe scaledobject order-consumer | grep -A6 Conditions
# реплики в реальном времени под нагрузкой
kubectl get pods -l app=order-consumer -w
Хороший тест — руками закинуть пачку сообщений в топик и посмотреть, что order-consumer поднимается из нуля в течение pollingInterval, а затем, спустя cooldownPeriod после опустошения очереди, гасится обратно до нуля.
Ловушки
pollingInterval— это задержка реакции, а не что-то бесплатное. По умолчанию 30 секунд: именно с такой периодичностью KEDA опрашивает scaler. Меньшее значение — быстрее реакция на всплеск, но больше нагрузка на источник метрики (тот же Prometheus или Kafka broker при сотняхScaledObjectв кластере).- Activation threshold и scaling threshold — это разные пороги, и их легко перепутать. Первый решает «поднимать ли из нуля вообще», второй — «до скольких реплик масштабироваться». Одинаковое значение для обоих либо слишком долго держит воркер на нуле при малой нагрузке, либо, наоборот, поднимает реплику раньше, чем реально нужно.
cooldownPeriodзащищает от дребезга, но стоит денег. Дефолт — 300 секунд простоя, прежде чем KEDA погасит Deployment до нуля. Это осознанный компромисс: слишком короткий период — поды скейлятся туда-сюда на каждое временное затишье в очереди (и это же будит cold start ниже), слишком длинный — вы платите за простаивающие реплики почти столько же, сколько без KEDA вообще.- Cold start — цена, а не баг. Пока Deployment на нуле реплик, первое сообщение не обрабатывается мгновенно: KEDA должно заметить активность (до
pollingInterval), поднять под, а поду — пройти pull образа и инициализацию приложения. Если SLA требует обработки за секунды, держитеminReplicaCount: 1хотя бы для критичных очередей — scale-to-zero имеет смысл там, где допустима задержка на холодный старт.
Итог
KEDA не переизобретает автоскейлинг — она снимает с HPA ограничение, которое было архитектурным с самого начала: невозможность смотреть на что-либо, кроме CPU/RAM, и невозможность уйти в ноль. Для стабильной веб-нагрузки HPA по-прежнему достаточно. Но там, где нагрузка определяется очередью, расписанием или внешней метрикой, а не тем, сколько процессора съедает под, — KEDA превращает scale-to-zero из ручного костыля в конфигурацию по умолчанию, а разницу видно в первом же счёте за кластер.