Горизонтальное автоскалирование в Kubernetes обычно начинается с HPA. Это понятный и практичный механизм: контроллер смотрит на метрику, например CPU, и меняет число реплик Deployment. Для многих сервисов этого достаточно.
Проблема начинается там, где нагрузка меняется быстрее, чем контур успевает на неё отреагировать. Метрика должна быть собрана, решение должно быть принято, новые Pod’ы должны запуститься и пройти readiness. Пока всё это происходит, старые Pod’ы уже могут работать на пределе, а хвостовые задержки p95/p99 — расти.
Отсюда естественная идея: если реактивный контур запаздывает, можно добавить короткий прогноз спроса и выбирать число реплик заранее. Я проверял именно такой вариант: внешний Hybrid-контроллер, в котором прогноз и защитные эвристики объединены с QP-задачей в стиле Model Predictive Control.
Для проверки я собрал отдельный стенд: toy-load, Prometheus, базовый HPA-контур, внешний Python-контроллер, CVXPY/OSQP и набор повторяемых A/B-прогонов. Предиктивный контур действительно снизил ресурсный резерв в части сценариев, но не стал безусловно лучше HPA: на коротком 30-секундном Spike Hybrid-SA проиграл HPA60 по p95/p99.
Дальше — как устроен стенд, что именно оптимизирует контроллер, и почему один из сценариев упёрся не в QP-формулировку, а в задержки самого Kubernetes-контура: метрики приходят не сразу, Pod’ы переходят в состояние Ready не сразу, а короткий пик может закончиться раньше, чем кластер добавит ёмкость.
Почему HPA не всегда достаточно
Для начала разложим задержку реакции HPA на части. На коротком всплеске она раскладывается на несколько шагов:
-
метрика должна попасть в Prometheus / конвейер метрик;
-
контроллер должен принять решение;
-
Kubernetes должен создать новые Pod’ы;
-
Pod’ы должны пройти readiness;
-
сервис должен реально получить дополнительную ёмкость.
Можно сделать HPA осторожным: поставить низкую целевую CPU-утилизацию и держать запас. Пики он переживёт лучше, но часть реплик будет простаивать. Можно поднять целевую CPU-утилизацию: средний масштаб снизится, но риск хвостовых задержек и таймаутов вырастет.
Отсюда и идея Hybrid-контроллера: смотреть не только на текущую метрику, а на короткий горизонт. Внутри нет ML-модели: есть приближённый сигнал спроса, экспоненциальное сглаживание, QP-задача, защитный слой и обычный PATCH в Kubernetes API.
Что было построено
Стенд одноузловой. Это не промышленный контур, а контролируемая среда для повторяемых сценариев: каждый прогон сохраняет артефакты, таблицы и журналы контроллера.
|
Компонент |
Роль |
|---|---|
|
toy-load |
Go HTTP-сервис с управляемой CPU-нагрузкой, jitter, payload и error-rate. |
|
vegeta |
Генератор HTTP-нагрузки с открытым контуром: задаёт фазовую интенсивность и не замедляется из-за долгих ответов. |
|
Prometheus |
Сбор RPS, гистограммы задержек, числа активных запросов и служебных метрик. |
|
CPU-HPA |
Базовый Kubernetes HPA-контур по CPU. |
|
Hybrid controller |
Внешний Python-контур: читает Prometheus, строит прогноз, решает QP, применяет защитный слой. |
|
CVXPY/OSQP |
QP-ядро оптимизации. |
|
Grafana / артефакты |
Панели, |
Ключевая деталь конфигурации: Prometheus в финальной серии собирал метрики раз в 30 секунд. Hybrid при этом работал с шагом 15 секунд для Step/Seasonality и 5 секунд для Spike. То есть на коротком Spike контроллер иногда принимал несколько решений по одному и тому же срезу телеметрии.
Что оптимизирует Hybrid-контроллер
QP нужна, чтобы формализовать конфликт между несколькими требованиями: быстро добавлять Pod при риске перегрузки, не дёргать масштаб слишком часто и не держать лишние реплики.
Я вынес этот конфликт в QP-задачу. На входе — текущее число реплик , прогноз спроса и виртуальное состояние накопленной перегрузки
.
Если сильно упростить, контроллер пытается минимизировать: риск перегрузки + резкость изменения масштаба + лишнюю ёмкость.
Получился такой функционал:
Смысл слагаемых:
-
— штраф за виртуальную перегрузку. Если прогнозируемый RPS выше целевой ёмкости одной реплики,
накапливает дефицит. Квадрат делает долгую перегрузку дорогой и заставляет контроллер реагировать агрессивнее. Это не p95-модель, а приближённая оценка риска хвостовых задержек;
-
— штраф за резкие изменения масштаба. Каждое изменение числа реплик проходит через Kubernetes API, обновление служебных объектов и сетевую подсистему. Эти накладные расходы напрямую не измерялись, поэтому
используется как показатель плавности: траектория «добавить 5 реплик, через 5 секунд убрать 4» считается плохой;
-
— линейный штраф за ёмкость. Он прижимает решение вниз, когда нагрузка ушла;
-
— планируемое число реплик на горизонте.
Почему не оптимизировать p95/p99 напрямую? Потому что хвостовые задержки зависят от сервиса, CPU limits, очередей, сети, readiness, поведения планировщика и текущего состояния кластера. Поэтому QP использует приближённый риск перегрузки, а реальные p95/p99 проверяются только на полном Kubernetes-контуре.
Важно не переинтерпретировать этот параметр: — не настоящая очередь и не оценка p95. Это инженерная память о дефиците ёмкости.
Как сравнивались контроллеры
Я сравнивал не «функцию в вакууме», а полный контур: метрики, решение, Kubernetes API, готовность Pod, результаты нагрузки.
Основные сценарии:
|
Сценарий |
Профиль |
Что проверяет |
|---|---|---|
|
Step |
20 RPS -> 80 RPS -> 40 RPS |
Устойчивое изменение нагрузки. |
|
Spike |
20 RPS -> 200 RPS на 30 секунд -> 20 RPS |
Короткий резкий всплеск. |
|
Seasonality |
синусоидальный профиль 20–120 RPS |
Плавная нестационарная нагрузка. |
Финальный прогон:
-
CPU-HPA60:
averageUtilization=60%, общий потолок 70 реплик; -
Hybrid-SA: заранее выбранный сценарный профиль, тот же потолок 70 реплик;
-
по 8 повторов каждого сценария;
-
задержки смотрел по медианам и IQR;
-
если успешность ниже 95%, режим считаю непригодным, даже если он экономит реплики.
Смотрел на следующие показатели:
-
доля успешных запросов;
-
p95/p99/max задержки;
-
пропускная способность по фазам сценария;
-
среднее число реплик;
-
полная вариация масштаба
;
-
приближённая оценка стоимости по запрошенным CPU и памяти.
Отдельно прогнал диагностику: сетку целевой CPU-утилизации для CPU-HPA, Vanilla-HPA80, вариант без QP, proxy-HPA+safety и Hybrid-Common с единым профилем.
Основной результат: компромисс, а не победа
Ниже — CPU-HPA60 против Hybrid-SA при maxReplicas=70. В Step и Seasonality Hybrid-SA даёт хороший ресурсный результат. В Spike задержки полного Kubernetes-контура оказались критичнее, чем выигрыш от прогноза.
Сначала задержки:
|
Сценарий |
Контур |
p95, мс |
p99, мс |
Успешность |
|---|---|---|---|---|
|
Step |
HPA60 |
54.6 |
75.9 |
100% |
|
Step |
Hybrid-SA |
53.7 |
56.2 |
100% |
|
Spike |
HPA60 |
87.1 |
131.8 |
100% |
|
Spike |
Hybrid-SA |
172.2 |
251.4 |
100% |
|
Seasonality |
HPA60 |
119.2 |
160.5 |
100% |
|
Seasonality |
Hybrid-SA |
88.0 |
127.4 |
100% |
Теперь реплики и приближённая стоимость:
|
Сценарий |
Контур |
Средние реплики |
V |
AWS-оценка, $/ч |
|---|---|---|---|---|
|
Step |
HPA60 |
17.807 |
22 |
0.0770 |
|
Step |
Hybrid-SA |
6.650 |
22 |
0.0288 |
|
Spike |
HPA60 |
16.246 |
22 |
0.0703 |
|
Spike |
Hybrid-SA |
16.619 |
67 |
0.0719 |
|
Seasonality |
HPA60 |
29.662 |
66 |
0.1283 |
|
Seasonality |
Hybrid-SA |
5.287 |
8 |
0.0229 |
Короткая версия:
-
в Step Hybrid-SA почти не меняет p95, улучшает p99 и сильно снижает среднее число реплик;
-
в Seasonality Hybrid-SA улучшает хвостовые задержки и резко снижает средний масштаб;
-
в Spike Hybrid-SA проигрывает по p95/p99 и по вариации масштаба.
В агрегированной таблице сценарии свёрнуты по длительности и повторам. Это не среднее по строкам выше. В такой свёртке Hybrid-SA относительно CPU-HPA60 даёт:
-
среднее число реплик ниже на 68%;
-
приближённая ресурсная стоимость ниже на 67%;
-
ниже с 110 до 97;
-
успешность остаётся 100%;
-
худший p95/p99 хуже из-за Spike: 172.2/251.4 мс.
Поэтому результат не сводится к тезису «новый контроллер лучше HPA». Hybrid-SA держит более плотный ресурсный режим, но на коротком Spike проигрывает по задержкам.
Почему короткий Spike оказался проблемным
Spike длился 30 секунд: 20 RPS, затем 200 RPS на 30 секунд, затем снова 20 RPS. В этом сценарии Hybrid-SA показал p95 172.2 мс против 87.1 мс у HPA60.
Первая гипотеза — влияние устаревшей телеметрии: Prometheus собирает метрики раз в 30 секунд, а шаг управления в пике равен 5 секундам. Эта гипотеза подтверждается частично. В этой конфигурации у Hybrid были неблагоприятные условия наблюдаемости:
-
шаг управления в Spike — 5 секунд;
-
окно
rate()— 15 секунд; -
Prometheus scrape interval — 30 секунд;
-
стрессовая фаза — тоже 30 секунд.
Несколько решений контроллера могли опираться на один и тот же срез данных. Я не проверял режим scrape interval = 2s: это уже другой эксперимент с отдельной оценкой нагрузки на TSDB. В этой серии контур проверялся при неидеальной наблюдаемости, а не при мгновенном поступлении метрик.
Затем я проверил журналы готовности Pod из локального архива экспериментов. Там хорошо видна задержка применения: лаг между командой «нужно больше реплик» и моментом, когда новые Pod реально готовы принимать трафик.
В Spike медианное окно между командой скейлиться и готовыми Pod было около 40 секунд. Для 30-секундного пика это поздно. В одном повторе лаг перекрывал 20 секунд стрессовой фазы, а p95/p99/max в этой фазе были 172.5/251.9/398.7 мс против примерно 22 мс в соседних фазах.
Я не утверждаю, что это единственная причина регрессии. Задержку readiness нельзя полностью отделить от устаревшей телеметрии, решений контроллера и возможного CPU тротлинга. Но для этого стенда видно ограничение: Hybrid-SA не успел помочь на импульсной нагрузке, где инфраструктура вводила новую ёмкость дольше, чем длился сам импульс.
В этой конфигурации на коротком Spike лучше сработал реактивный защитный слой: он опирается на текущий дефицит ёмкости и активные запросы, а не на планирование по горизонту.
Почему повышение целевой CPU-утилизации не решает проблему
Можно возразить: HPA60 слишком осторожный. Поднимем averageUtilization, и HPA сам станет дешевле.
Я проверил это отдельной CPU-HPA-сеткой с историческим потолком maxReplicas=12. Подставлять её вместо финальной A/B-серии с потолком 70 реплик нельзя, но как диагностика она показательная: рост целевой CPU-утилизации снижает средний масштаб, а в Spike начиная с HPA150 появляются массовые таймауты. В точке HPA350 медианная успешность падает до 34.42%.
То есть «меньше реплик» само по себе не победа. Политику автоскалирования нужно читать минимум по четырём осям:
-
успешность;
-
хвостовые задержки;
-
средний масштаб / приближённая стоимость;
-
динамика изменения реплик.
Одна метрика здесь недостаточна.
Компонентный анализ: QP — не единственный источник эффекта
Сравнение только с дефолтным HPA легко даёт завышенную оценку эффекта. Поэтому я добавил контрольные варианты: proxy-HPA+safety и контур без QP используют тот же приближённый сигнал спроса, а HPA-строки остаются базовыми CPU-контурами.
|
Контур |
Средние реплики |
V |
AWS-оценка, $/ч |
Худший p95/p99, мс |
Успешность |
|---|---|---|---|---|---|
|
HPA60 |
23.276 |
110 |
0.1007 |
119.2/160.5 |
100% |
|
Hybrid-SA |
7.555 |
97 |
0.0327 |
172.2/251.4 |
100% |
|
proxy-HPA+safety |
6.737 |
102 |
0.0291 |
98.4/145.8 |
100% |
|
без QP |
5.477 |
108 |
0.0237 |
143.4/194.1 |
100% |
Таблицу не стоит читать как рейтинг. Дешёвый контур может проигрывать по хвостам или плавности, а контур с хорошими задержками может держать больше реплик.
Компонентный анализ оказался важнее основного сравнения: proxy-HPA+safety оказался дешевле Hybrid-SA и быстрее по худшим p95/p99. Он уступил только по плавности . Значит, весь эффект нельзя приписать QP/MPC-ядру. Значимую часть дают приближённый сигнал спроса, защитный слой, параметры стабилизации, частота телеметрии и сценарный профиль.
Роль QP-ядра видна по , полной вариации масштаба. У реактивного
proxy-HPA+safety она равна 102, у контура без QP — 108, у Hybrid-SA — 97. Разница не огромная, но роль читается: QP работает как демпфер. Он видит горизонт планирования и сглаживает траекторию; ценой стали худшие p95/p99 на Spike.
Короткие высокочастотные возмущения лучше обрабатывает реактивный слой. QP полезнее там, где важна траектория на горизонте планирования, а не одиночный импульс, который заканчивается раньше, чем Kubernetes успевает поднять готовую ёмкость.
Что можно воспроизвести без кластера
Репозиторий — не готовый автоскейлер, а стенд для проверки идей.
Минимальный путь без Kubernetes:
Инструкция по запуску симулятора локально
python3 -m pip install -e analysismpc-validate-trace --trace-csv analysis/mpc_autoscaler_analysis/data/traces/baseline_spike_profile_dt15.csvmpc-offline-sim \ --trace-csv analysis/mpc_autoscaler_analysis/data/traces/baseline_spike_profile_dt15.csv \ --out-dir analysis/out/offline/spike
Этот путь не воспроизводит живой прогон в Kubernetes, но позволяет проверить формат трасс, офлайн-симулятор и базовые предположения контроллера.
Полезные ссылки:
-
Репозиторий: https://github.com/vshulcz/mpc-autoscaler
-
Методика: https://github.com/vshulcz/mpc-autoscaler/blob/main/docs/METHODOLOGY.md
-
Ограничения: https://github.com/vshulcz/mpc-autoscaler/blob/main/docs/LIMITATIONS.md
-
Матрица экспериментов: https://github.com/vshulcz/mpc-autoscaler/blob/main/docs/BENCHMARK_MATRIX.md
Чего этот результат не доказывает
Это не доказательство, что MPC лучше HPA вообще. Стенд одноузловой и синтетический. Выводы зависят от интервала сбора Prometheus, задержки readiness, CPU-лимитов, поведения планировщика и параметров контроллеров.
Что дальше
Что я хочу проверить дальше:
-
базовый контур в стиле KEDA;
-
предиктивный HPA;
-
трассы, похожие на промышленную нагрузку;
-
отказные сценарии: пропавшие метрики, резервный режим солвера, холодный старт, шумный прогноз, ограничения частоты запросов;
-
многоузловой стенд и взаимодействие с Cluster Autoscaler;
-
автоматический выбор профиля нагрузки;
-
адаптация параметров QP во время прогона;
-
агрегированные таблицы по нескольким seed и прогонам.
Отдельно интересен режим нехватки узлов. Hybrid может заранее запросить больше Pod, но Cluster Autoscaler ещё не успеет добавить ёмкость узлов. В таком режиме возможны очередь Pending Pod и перерегулирование. Это ближе к промышленным проблемам, чем одноузловой стенд.
Вывод
Идеального автоскейлера нет. Есть Парето-граница между задержками, ресурсным резервом и резкими изменениями числа реплик.
Консервативный HPA60 даёт хорошие задержки, но держит много лишних Pod’ов. Реактивный контур лучше переживает короткий Spike, но сильнее дёргает масштаб. Hybrid-SA с QP-ядром снижает ресурсный резерв и сглаживает траекторию, но проигрывает там, где новая ёмкость становится Ready позже самого пика.
Мой итог: автоскалирование надо оценивать как полный контур управления, а не как оптимизационную функцию на бумаге.
ссылка на оригинал статьи https://habr.com/ru/articles/1043074/