5. Scaling и autoscaling
Масштабирование в Kubernetes: HPA, VPA, Cluster Autoscaler и Mechanical Sympathy в Go
В распределенных системах статическая нагрузка существует только в синтетических тестах. В реальном мире трафик подчиняется законам природы и социума: ночью сервисы погружаются в полудрему, а в «Черную пятницу» или во время утреннего всплеска поток заказов вырастает в сотни раз за считанные секунды.
Держать серверный парк постоянно развернутым под пиковую нагрузку — финансовое самоубийство для бизнеса. Масштабировать сервисы вручную командой kubectl scale --replicas=10 — медленно и ненадежно: дежурный инженер никогда не среагирует быстрее микросекундного всплеска трафика.
Kubernetes предлагает развитую трехуровневую систему автоматического масштабирования:
- Горизонтальное масштабирование подов (HPA — Horizontal Pod Autoscaler): изменение количества реплик сервиса.
- Вертикальное масштабирование подов (VPA — Vertical Pod Autoscaler): динамическое изменение вычислительных ресурсов (CPU/RAM) одного пода.
- Масштабирование физических узлов (Cluster Autoscaler / Karpenter): заказ и выключение реальных серверов у облачного провайдера.
Однако без понимания рантайма Go автоматика способна не спасти сервис, а окончательно добить его. Давайте разберем математику, физику и скрытые ловушки масштабирования с точки зрения механического сочувствия к машине.
1. Horizontal Pod Autoscaler (HPA): Механика и математика
Контроллер HPA встроен в kube-controller-manager. Он функционирует как непрерывный управляющий контур с отрицательной обратной связью (период опроса по умолчанию — каждые 15 секунд):
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
A["Metrics Server\n(Сбор метрик с kubelet cAdvisor)"]:::network -->|"CPU/RAM использование"| B["K8s API Server"]
C["HPA Controller\n(Цикл опроса каждые 15 сек)"] -->|"Запрос текущих метрик"| B
C -->|"Текущие реплики: 4, CPU 85%"| D{"Вычисление desiredReplicas"}
D -->|"Целевое CPU 50%"| E["Новое значение: 7"]
E -->|"Обновление Deployment.replicas"| B
B --> F["Deployment Controller создает 3 новых Пода"]
Математическая формула HPA
Алгоритм вычисления целевого числа реплик в исходном коде Kubernetes поразительно прост и элегантен:
$$\text{desiredReplicas} = \left\lceil \text{currentReplicas} \times \left( \frac{\text{currentMetricValue}}{\text{desiredMetricValue}} \right) \right\rceil$$
В кодовой базе K8s эта формула записывается как:
desiredReplicas = ceil[ currentReplicas * ( currentMetricValue / desiredMetricValue ) ]
Наглядный расчет:
Предположим, у нас развернуты 4 реплики Go-сервиса (currentReplicas = 4). Мы задали целевой уровень утилизации процессора в 50% (desiredMetricValue = 50%). Внезапно пошел наплыв пользователей, и текущее потребление достигло 85% (currentMetricValue = 85%).
Контроллер вычисляет:
desiredReplicas = ceil[ 4 * (85 / 50) ] = ceil[ 6.8 ] = 7.
Контроллер немедленно отправляет патч в спецификацию деплоймента, увеличивая число реплик с 4 до 7.
2. Ловушка CFS Throttling: Почему «душат» Go-процессы
Многие инженеры интуитивно настраивают HPA на метрику CPU и выставляют жесткие лимиты в манифесте:
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Здесь кроется фундаментальная архитектурная ловушка ядра Linux и Kubernetes.
Kubernetes реализует limits.cpu через механизм CFS (Completely Fair Scheduler) и подсистему cpu.cfs_quota_us в cgroups.
Лимит limits: cpu: "500m" означает, что процессу положено 0.5 процессорного ядра. Но ядро Linux не замедляет тактовую частоту процессора. Оно делит время на фиксированные окна (по умолчанию cfs_period_us = 100ms).
Квота в 500m означает: за каждые 100 мс процессам пода разрешено суммарно работать на ядрах CPU не более 50 мс.
gantt
title CFS Quota Throttling (Период 100 мс)
dateFormat X
axisFormat %s
section Выполнение Go
Работа горутин (4 потока M) :active, 0, 50
CFS THROTTLING (Ядро замораживает процесс!) :crit, 50, 100
Возобновление работы в новом окне :active, 100, 150
3. Mechanical Sympathy: Почему CPU и RAM обманывают HPA в Go
Стандартные шаблоны мониторинга предлагают масштабировать сервисы по CPU и RAM. Для стеков с синхронным вводом-выводом (Python WSGI, Ruby) это имеет смысл. Но для архитектуры Go это прямой путь к сбоям.
1. Проблема с CPU
Модель многозадачности Go основана на неблокирующем сетевом полинге (netpoller). Десятки тысяч горутин могут обслуживать клиентов, проводя 95% времени в ожидании ответа от базы данных или сокета. В этот момент загрузка CPU стремится к нулю. Сервис может находиться на грани исчерпания системных файловых дескрипторов или соединений к БД, а HPA не пошевелится.
И наоборот: фоновый проход Garbage Collector или парсинг тяжелого XML кратковременно нагрузит ядра на 100% в течение 200 мс, провоцируя HPA на паническое и ненужное выделение новых подов.
2. Проблема с Memory (RAM)
Управление памятью в Go — дискретное. Рантайм не возвращает освобожденную память операционной системе мгновенно.
По умолчанию сборщик мусора ориентируется на переменную GOGC=100: следующий цикл GC инициируется, когда размер рабочей кучи (heap) удваивается по сравнению с размером живых данных после предыдущей сборки.
В результате потребление памяти Go-сервисом выглядит как классическая пила:
- Сервис работает, куча растет. Память доходит до 75% от лимита контейнера.
- HPA, настроенный на порог в 70% RAM, видит превышение и немедленно отдает команду: «Скейлимся вверх, запускаем еще 5 реплик!».
- В следующую секунду рантайм Go запускает GC: память очищается и падает до 35%.
- HPA видит, что кластер недогружен, выжидает cooldown-интервал и начинает удалять поды (scale down).
- Цикл повторяется по кругу.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Alloc["Выделение памяти (куча растет до 75%)"]:::process -->|"HPA видит порог > 70%"| ScaleUp["HPA добавляет поды (Scale Up)"]
ScaleUp --> GC["Срабатывает Go GC (память падает до 35%)"]
GC -->|"HPA видит недогруз < 70%"| ScaleDown["HPA удаляет поды (Scale Down)"]
ScaleDown --> Alloc
Это деструктивное поведение называется Flapping (Хлопанье) — кластер непрерывно вводит и выводит реплики из эксплуатации, нагружая планировщик K8s и попутно сбрасывая кэши приложений.
4. Кастомные метрики: Истинный Scaling для Go-бэкенда
Если аппаратные ресурсы — плохой индикатор здоровья Go-приложения, на что же должен опираться настоящий SRE-инженер? Ответ прост: на бизнес-метрики и задержки (Latency/Queue Depth).
Для проброса прикладных метрик в управляющий контур K8s используется Prometheus Adapter (или KEDA — Kubernetes Event-driven Autoscaling). Адаптер опрашивает TSDB Prometheus по PromQL-запросам и регистрирует виртуальное API custom.metrics.k8s.io, доступное для HPA.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
GoApp["Go Application\n(/metrics endpoint)"]:::process -->|"Scrape metrics"| Prom["Prometheus TSDB"]
Prom -->|"PromQL query"| Adapter["Prometheus Adapter\n(custom.metrics.k8s.io)"]
Adapter -->|"API Custom Metrics"| APIServer["K8s API Server"]
HPA["HPA Controller"] -->|"Запрос RPS / Goroutines"| APIServer
HPA -->|"Scale Deployment"| Deploy["Deployment"]
Золотые метрики для автомасштабирования Go:
- RPS (Requests Per Second): Количество запросов в секунду на один под. Зная из нагрузочного тестирования, что один pod гарантированно держит 1 000 RPS с идеальным SLA, мы выставляем целевое среднее значение в 800–1000 RPS.
- Goroutine Count (
runtime.NumGoroutine()): Количество параллельно исполняемых легковесных потоков. Если число горутин аномально растет (например, превысило 5 000 на инстанс), это прямой сигнал зависания на внешних вызовах или приближения к лимитам планировщика. - Latency (p95 / p99): Задержка обработки запросов. Если 99-й перцентиль времени отклика переваливает за 200 мс — кластер обязан расширяться до того, как клиенты начнут получать ошибки.
Манифест HPA на базе кастомной метрики http_requests_per_second:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: go-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: go-api
minReplicas: 3
maxReplicas: 50
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000" # Скейлимся, если на 1 под приходится более 1000 RPS
5. Cold Start и Readiness Пробы: Спасение старых подов от каскадного сбоя
Когда HPA принимает решение о масштабировании, новые поды не материализуются со скоростью света. Им требуется время:
- Выгрузка Docker-образа с реестра на новую ноду (если образ не закэширован).
- Выделение сетевого пространства (
ContainerCreating). - Инициализация Go-рантайма, выделение пулов соединений к PostgreSQL/Redis (
sql.Open,pgxpool.New). - Прогрев локальных кэшей данных.
Пока новый под находится в статусе NotReady, входящий шквал трафика продолжают принимать старые, уже задыхающиеся поды. Они начинают падать по тайм-аутам, провоцируя лавинообразный отказ (Cascading Failure).
6. VPA (Vertical Pod Autoscaler) и Cluster Autoscaler (CA)
Помимо HPA, инфраструктурный арсенал Kubernetes включает еще два типа автомасштабирования:
flowchart TB
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph AppLevel["Уровень приложения (Pods)"]
HPA_Node["HPA\n(Количество подов: 3 -> 10)"]
VPA_Node["VPA\n(Размер пода: 0.5 CPU -> 2 CPU)"]
end
subgraph InfraLevel["Уровень серверов (Nodes)"]
CA_Node["Cluster Autoscaler / Karpenter\n(Количество VM в AWS/GCP: 5 -> 12)"]
end
HPA_Node -.->|"Поды в статусе Pending"| CA_Node
6.1. VPA (Vertical Pod Autoscaler)
VPA не трогает количество подов — он анализирует профиль потребления ресурсов и подбирает оптимальные значения requests и limits.
- Режим
Off(Рекомендательный): Идеален для Go. VPA только выдает рекомендации дашбордам, позволяя инженерам точно калибровать манифесты без неожиданностей в проде. - Режим
Auto: Крайне опасен для stateless веб-сервисов! Чтобы изменить лимиты CPU/памяти контейнера, Kubernetes обязан перезапустить Pod. Это неизбежно вызывает микро-даунтаймы и разрыв сетевых сессий. - Конфликт HPA и VPA: Никогда не используйте HPA и VPA на базе одной и той же метрики (например, CPU)! Возникнет паразитный резонанс: VPA увеличит лимит -> процент утилизации упадет -> HPA удалит «лишние» поды -> оставшиеся поды перегрузятся -> VPA снова поднимет лимит.
6.2. Cluster Autoscaler (CA)
Когда HPA создает новые поды, на существующих рабочих нодах может физически закончиться процессорная мощность или память. Планировщик kube-scheduler переводит поды в статус Pending.
Здесь подключается Cluster Autoscaler (или современный бессерверный планировщик узлов Karpenter):
- Контроллер видит
Pendingподы, которым не хватает ресурсов. - Он обращается к API облачного провайдера (AWS ASG — Auto Scaling Groups, GCP MIG — Managed Instance Groups, Yandex Cloud Compute Groups) и запускает новые виртуальные машины.
- Когда пик спадает, и поды удаляются, Cluster Autoscaler находит недозагруженные ноды, аккуратно выселяет оставшиеся поды (процедура Drain) и удаляет пустые серверы, снижая расходы на облако до нуля.
Резюме
- Математика HPA: Вычисляет реплики по формуле
desiredReplicas = ceil[ currentReplicas * ( currentMetricValue / desiredMetricValue ) ]. - CFS Throttling: Жесткие
limits: cpuв Linux приводят к усыплению параллельных горутин Go на десятки миллисекунд. В Go-сервисах отдавайте предпочтение честнымrequests. - Память и Flapping: Из-за циклов
GOGC=100масштабирование по RAM приводит к паразитному хлопанью подами. Для контроля используйтеGOMEMLIMITна уровне 80–90% от лимита. - Кастомные метрики (Prometheus Adapter): Настоящее масштабирование бэкенда строится на RPS (
http_requests_per_second), времени отклика (Latency p99) и числе горутин (runtime.NumGoroutine()). - Cold Start: Для защиты подов во время старта используйте
startupProbe, PDB и техники Rate Limiting / Load Shedding (429 Too Many Requests). - VPA и CA: VPA в Go безопасен преимущественно в режиме
Off, а Cluster Autoscaler решает проблему нехватки нод дляPendingподов.
Горизонтальное масштабирование без состояния (Stateless) просто и прозрачно: поды можно уничтожать и плодить сотнями. Но что делать, если нашему сервису требуется хранить петабайты данных на диске, сохранять постоянные сетевые имена и соблюдать строгий кворум? В следующей лекции мы погрузимся в мир постоянства данных: 6. Stateful приложения.