Прямой ответ
Horizontal Pod Autoscaler держит число реплик деплоймента под нагрузкой: каждые 15 с контроллер получает метрики (metrics-server/внешний адаптер), считает желаемые_поды = ceil(текущие × метрика / цель) и меняет replicas между min и max. Рабочий минимум практики: (1) CPU-цель считается от requests — averageUtilization: 70 означает 70% от request контейнера (requests — обязательны, иначе HPA не работает вовсе: делить не на что); (2) метрика должна отражать реальную занятость — CPU подходит для вычислительных сервисов; IO-связанные (ждут БД/внешние API — CPU низкий при забитых воркерах) масштабируют по кастомным метрикам (Prometheus-адаптер: длина очереди задач, активные соединения, RPS на под — иначе HPA «не видит» перегрузку и спит); (3) приложение должно масштабироваться горизонтально по факту — stateless (12-фактор: сессии/состояние вовне), readiness-проба корректна (новый под принимает трафик только готовым — иначе скейлинг даёт всплеск 502 — healthcheck-дисциплина), и старт пода быстрее роста нагрузки (поднимается 3 минуты — HPA на минутные пики бесполезен — прогрев/лёгкий старт); (4) поведение (behavior) тюнить: scaleUp быстро (пик не ждёт — период 30–60 с), scaleDown медленно (стабилизация 5 мин — flapping «вверх-вниз» каждые полминуты вреден — каждый новый под — холодные кеши/пулы); (5) max — осмысленный потолок (не 100 «на всякий» — упрётся в узлы/БД-пулы — потолок нагрузки БД часто ниже pod-потолка — пулинг и лимиты на сторону данных считать вместе). И проверка: нагрузочный тест со скейлингом (locust — ступени RPS — смотреть «успевает ли HPA», p95 в момент скейла) — непроверенный автоскейлер — декорация.
Механика: формула, метрики, цели
Формула desired = ceil(current × metric / target) в деталях: utilization по CPU — среднее по подам от их requests (requests занижены — utilization завышен — HPA держит подов больше нужного и наоборот — requests честны по замерам — предсказуемость скейла); типы метрик: Resource (cpu/memory — metrics-server — из коробки), Pods/Object (метрика на под/объект — «очередь на воркер» — prometheus-adapter/KAEDA), External (внешние системы — длина очереди SQS/RabbitMQ — воркеры Celery скейлятся по глубине очереди — Celery-метрика прямо в HPA); память как цель — осторожно (память не «разгружается» после пика обработки — кеш/фрагментация — utilization держится — поды не сжимаются обратно — CPU/кастомные метрики предсказуемее). Целевые значения — от нагрузки-профиля: CPU 60–80% типично (низкая цель — больше подов в простое — дороже, но пик в запасе; высокая — экономия, риск не успеть); стабилизация/окна (behavior.scaleDown.stabilizationWindowSeconds — максимум «хочу меньше подов» за окно — 300 с дефолт; scaleUp-окно короче/агрессия selectPolicy=Max — реакция на всплеск без задержки); толерантность (по умолчанию ±10% — мелкие колебания не трогают replicas — «HPA дёргается на шуме» — увеличить tolerance). Множественные метрики — HPA берёт максимум желаемых (CPU и очередь вместе — «скейлим по худшему» — удобно для смешанных нагрузок); min/max — по модели отказов: min ≥ 2 (single pod — деплой/падение = простой; rolling-обновления на одной реплике — тоже пауза), max — минимум(потолок узлов, потолок БД/внешних лимитов) — «20 подов легли на Postgres без пулера» — скейлер вылечил CPU, убил БД.
Приложение под скейлер: readiness, старт, состояние
Горизонтальное масштабирование работает, когда приложение к нему готово: stateless (сессии — Redis/внешние (redis-паттерны), локальный кеш — допустимая потеря при смерти пода — инвалидация переживается; локальные файлы/загрузки — внешнее хранилище (S3) — иначе под «с состоянием» скейлится с потерями); readiness (readinessProbe — под в ротации только когда реально готов: пулы прогреты, миграции-иниты завершены, кеш-минимум загружен — healthcheck-семантика: readiness «жив», не «отвечает»); скорость старта критична (HPA-цикл: метрика → решение → под → ready → трафик — минуты на всё; тяжёлый старт (импорт ML-моделей, прогрев) — оптимизировать образ (docker-оптимизация), ленивую загрузку, или держать запас min-репликами; startupProbe для медленных стартов — readiness не убивает поднимающийся под); graceful shutdown (pod terminating — соединения дорабатывают — preStop/сигналы — shutdown-механика) — scale-down без обрывов; пулы соединений на под (каждый новый под — свой пул к БД — 20 подов × 20 соединений = 400 — БД задыхается — пулер/лимиты per pod считать × max реплик — PgBouncer); холодный кеш новых подов (первый трафик на свежий под — miss-шторм в БД — прогрев в readiness или постепенный ввод — canary-логика на уровне трафика). Отдельная ниша — VPA/cluster autoscaler (не путать: HPA — число подов, VPA — requests/limits подов (рекомендации/автоправка — с HPA по CPU конфликтует — вместе осторожно), cluster autoscaler — число узлов под запросы подов — в managed-кластерах обычно включён — «поды в Pending при скейле» — его зона — K8s-основы).
Практический сценарий: API с дневными пиками ×5
Исходник: FastAPI-сервис (uvicorn в K8s), 3 реплики статично; дневной пик 12:00–15:00 — нагрузка ×5 — p95 900 мс (CPU 95%), ночь — те же 3 пода в простое (утилизация 8%). Внедрение HPA: (1) requests приведены к замерам (500m/768Mi — раньше 100m «из примера» — utilization-математика HPA была бы бессмысленна); (2) HPA: min 2, max 12, CPU 70%, behavior: scaleUp period 30s (×2 подов за период допустимо), scaleDown stabilization 600s (вечерний спад не дёргает скейлер); (3) нагрузочная проверка ступенями (locust: 100→500→1000 RPS): на 500 RPS скейл 2→6 за 70 с (старт пода 25 с — образ облегчён предварительно), p95 в переходный период — 500 мс (приемлемо), на 1000 — 12 подов (max) — p95 380 мс; найдено: на 8+ подах p95 уперся не в CPU, а в Postgres-соединения (12×20=240 против max_connections 200 — ошибки пула) — pgbouncer в транзакционном режиме между подами и БД (пулинг) — потолок снят (заодно max_connections пересмотрен); (4) кастомная метрика добавлена позже для фоновых воркеров того же сервиса: HPA по длине Celery-очереди (>50 задач на воркер — скейл; очередь — честный сигнал занятости, CPU у IO-воркеров врал); (5) мониторинг скейлера: метрики desired/current + события HPA в Grafana-дашборд (дашборд) — «скейлинг произошёл» видно без kubectl. Итоги месяца: p95 пика — 320 мс (было 900), ресурсы — −45% (ночь/утро — 2 пода), инцидентов скейлинга ноль; единственный ручной случай — маркетинговая рассылка (всплеск ×8 за 2 минуты) — HPA успел частично (p95 подскочил на 90 с) — добавлен запас min=4 в часы рассылок по расписанию (cron-правка min — законная ручка; KEDA-cron как развитие — не понадобилось).
FAQ
HPA против KEDA? HPA — встроенный (Resource-метрики + адаптеры); KEDA — надстройка: 60+ скейлеров из коробки (очереди RabbitMQ/SQS/Kafka-lag, cron-расписания, Prometheus-запросы — без возни с prometheus-adapter) + умеет scale-to-zero (ночные батчи/редкие обработчики — ноль подов вне работы — HPA так не может — min 1); K8s-нативный CPU/RPS-скейл — HPA достаточен; событийные/очередные/расписательные нагрузки — KEDA удобнее; сосуществуют (KEDA использует HPA под капотом).
Почему HPA показывает <unknown>?
Метрики недоступны: metrics-server не установлен/не жив (Resource-метрики — kubectl top тоже не работает — проверить), requests не заданы на контейнер (utilization не вычислим — самая частая причина), кастомная метрика — адаптер не отдаёт серию (имя/labels в HPA не совпали с фактическими — kubectl get --raw /apis/custom.metrics.k8s.io/... — посмотреть, что реально есть); <unknown> = скейлер слеп (реплики не трогает) — не «игнорирует», а «не видит» — чинить подачу метрик.
Как избежать флаппинга (постоянных вверх-вниз)? Источники: цель впритык к факту (нагрузка колеблется около 70% — решения мечутся — цель ниже/толерантность выше), короткие окна стабилизации (scaleDown stabilization 300–600 с — снижение неспешное), шумная метрика (RPS-спайки — сглаживание окном/процентилем), холодный старт (поды скейлятся, греются, метрика падает — скейл вниз — по кругу — прогрев в readiness + стабилизация); наблюдение desired-vs-current в метриках — флаппинг виден пилой — лечится параметрами behavior, не отключением скейлера.
Масштабировать ли так воркеры с длинными задачами? Да, но по правильной метрике и с корректным завершением: метрика — глубина очереди/задач на воркер (CPU соврёт — задача ждёт IO при 5% CPU — Celery-мониторинг); scale-down опасен обрывом длинной задачи — terminationGracePeriod под длительность задачи (или drain-логика: воркер перестаёт брать новые, доделывает текущие — graceful) + acks_late-идемпотентность как страховка; длинные задачи > grace-периода — разбивать или выносить в Job-объекты (K8s Job на задачу — не деплоймент-скейлинг — другой паттерн).
Источники: документация Kubernetes (HPA, walkthrough, autoscaling) — ссылки в шапке.