✻ Урок 5.11 · Тема 5: Kubernetes и Helm
Масштабирование: HPA и metrics-server
Содержание урока
Зачем это нужно
Днём «Заметки» получают в десять раз больше запросов, чем ночью (запрос это одно обращение к приложению, например открыть страницу со списком заметок). Можно держать круглосуточно шесть реплик (одинаковых копий приложения, урок 5.2): это дорого, потому что ночью пять из них простаивают, а ресурсы кластера оплачены. Можно держать две: это дёшево, но днём под нагрузкой сервис начнёт отвечать медленно и упадёт.
Третий путь: пусть кластер сам добавляет реплики, когда нагрузка растёт, и убирает, когда она спадает. Этим занимается горизонтальный автоскейлер подов (Horizontal Pod Autoscaler, HPA): программа внутри кластера, которая следит за загрузкой и меняет число реплик. Это как управляющий кофейни, который по длине очереди вызывает кассиров из подсобки; без него число реплик остаётся таким, каким ты его вписал, и ночью ты платишь за пустые кассы, а днём очередь упирается в дверь. «Горизонтальный» значит «вширь, добавлением копий», а не «вверх, одной копии побольше» (разница ниже в теории). На работе его ставят почти на каждый сервис без собственного состояния, а на собеседованиях спрашивают, почему HPA показывает <unknown> и почему реплики то растут, то падают.
HPA работает только при двух условиях: в кластере есть metrics-server (программа, которая измеряет нагрузку, как счётчик на электрощитке) и у подов заданы requests (сколько ресурсов под просит у кластера, как бронь столика: «мне нужно место как минимум на это»). Без любого из двух он молчит. Оба условия разберём подробно.
Шаг проекта: в чарт helm/notes добавляется templates/hpa.yaml (включается флагом hpa.enabled), в кластере ставится metrics-server, нагрузка создаётся эндпоинтом /burn. Версия чарта становится 0.1.1.
Что нужно знать
- Урок 1.5: диск, память и процессор: эндпоинт
/burnзагружает одно ядро процессора на заданное число секунд; cgroups (механизм ядра, который считает и ограничивает ресурсы группы процессов) считают, сколько CPU и памяти съел контейнер. - Урок 5.2: Pod и Deployment: под (Pod) это обёртка над контейнерами, Deployment держит заданное число копий пода (реплик) и умеет менять это число.
- Урок 5.3: Service и DNS: Service раздаёт запросы между подами, к нему обращаются по имени
notes.notes.svc. - Урок 5.7: пробы, ресурсы, rolling update:
requestsиlimits, проба готовности (readiness). - Урок 5.9: Helm: чарт
helm/notes, файлvalues.yaml, командаhelm upgrade.
Всё остальное (что такое миллиядро, как работает петля контроллера, что такое окно стабилизации) объясняется ниже. Коротко: миллиядро это тысячная доля ядра процессора, петля контроллера это «измерь, сравни с желаемым, исправь, повтори», окно стабилизации это пауза перед тем, как убирать реплики, чтобы не раскачивать сервис.
Ещё одно слово из этой темы: limits это потолок ресурсов контейнера, которые ему можно потребить (про пару requests и limits в уроке 5.7). Rolling update (плавное обновление) это постепенная замена старых подов новыми, как смена караула по одному человеку, чтобы пост никогда не оставался пустым.
Картина целиком
Представь кофейню с одной кассой. Утром приходят три человека в час, хватает одного кассира. В обед приходят сто человек, очередь до двери. Управляющий смотрит на очередь и вызывает ещё кассиров из подсобки. Когда поток спадает, он не отпускает их сразу: вдруг это короткая пауза, и через пять минут снова хлынут. Отпускает, только если тишина держится долго.
В кластере всё то же самое, только вместо управляющего HPA, вместо кассиров реплики «Заметок», а очередь это загрузка процессора.
flowchart TD
LOAD["нагрузка на поды notes"] --> KL["kubelet на узле<br>знает CPU и RAM каждого пода"]
KL -->|"раз в 15 с: сколько съели поды?"| MS["metrics-server<br>хранит снимок «сколько сейчас»"]
MS -->|"API metrics.k8s.io"| HPA["HPA (раз в 15 с) считает:<br>нужно = ceil(реплик * сейчас / цель)"]
HPA -->|"поставь replicas = N"| DEP["Deployment notes<br>создаёт и убирает поды"]
За урок ты разберёшь каждую стрелку: откуда берутся цифры нагрузки, в чём именно они измеряются, по какой формуле HPA решает, сколько реплик нужно, почему вверх он идёт быстро, а вниз медленно, и чего HPA не умеет. Потом поставишь всё это в kind, сломаешь и починишь.
Теория
Зачем размножать поды и чем это отличается от «сервер побольше»
Когда сервису не хватает мощности, есть два пути.
- Вертикальное масштабирование (scale up): дать одному экземпляру больше процессора и памяти. Как заменить кассира на более быстрого. Быстро придумать, но у скорости есть потолок (самый быстрый кассир всё равно один), а для смены размера под обычно приходится перезапускать.
- Горизонтальное масштабирование (scale out): поставить рядом ещё такие же экземпляры. Как открыть вторую, третью кассу. Потолка почти нет, а если один экземпляр умрёт, остальные продолжают работу.
Kubernetes лучше всего умеет второе: Deployment уже держит нужное число одинаковых реплик, а Service раздаёт между ними запросы. Поэтому HPA («горизонтальный») просто меняет одно число в Deployment: replicas.
Условие, при котором это работает: приложение не хранит важное состояние внутри себя, а значит все реплики взаимозаменяемы. «Заметки» хранят данные в PostgreSQL (урок 5.5), а не в памяти пода, поэтому любая реплика ответит на любой запрос. Такие приложения называют stateless (без собственного состояния).
Осторожно, путаница: «автомасштабирование ускоряет сервис». Нет. Оно добавляет параллельные обработчики. Если один запрос обрабатывается 5 секунд, то с десятью репликами он всё равно обработается за 5 секунд, просто одновременно можно обслужить больше запросов.
Прикинь сам: сервис отвечает на один запрос за 3 секунды, и это слишком долго. Поможет ли HPA?
Нет: HPA увеличивает число одновременно обслуживаемых запросов, а не скорость одного. Медленный запрос ускоряют оптимизацией кода или базы.
Проверь понимание: сервис отвечает на один запрос за 3 секунды, и это слишком долго. Поможет ли HPA?
Ответ
Нет. HPA увеличивает число одновременно обслуживаемых запросов, а не скорость одного. Медленный запрос нужно ускорять оптимизацией кода или базы, а HPA пригодится, когда медленно становится оттого, что запросов слишком много для имеющихся реплик.
Главное: горизонтальное масштабирование добавляет копии и работает для stateless-приложений; одиночный запрос оно не ускоряет.
Чтобы HPA сказал «нагрузка 100%», нужно понять, от чего считаются проценты.
Миллиядра и requests: в чём HPA измеряет нагрузку
Чтобы HPA мог сказать «нагрузка 100%», нужно понять, от чего считаются проценты. Для этого вспомним, как Kubernetes измеряет процессор.
Ядро и миллиядро. Загрузка процессора в Kubernetes измеряется в ядрах (cores): 1 ядро это один полностью занятый поток вычислений. Дробные значения записывают в миллиядрах (millicores, суффикс m): 1 ядро = 1000m, а 50m это 5% одного ядра. Как рубль и копейки: удобно, когда нужны мелкие суммы.
requests и limits (урок 5.7) заданы в описании контейнера, у каждого свой смысл:
requests.cpuэто «сколько мне нужно как минимум». Планировщик (компонент Kubernetes, который выбирает узел для нового пода) подбирает под узел, где такой запас ещё свободен. Как бронь столика: ресторан оставляет место для тебя.limits.cpuэто «больше этого не давай». Процессор сверх лимита контейнеру просто не выдают, он замедляется. Как потолок на счёте карты.
Вот кусок Deployment «Заметок» из урока 5.7, который нас интересует:
resources:
requests:
cpu: 50m # бронь: 5% ядра, по этому числу HPA считает проценты
memory: 64Mi
limits:
cpu: 200m # потолок: больше 20% ядра контейнеру не дадут
memory: 128Mi
Как HPA считает загрузку. Он берёт, сколько CPU контейнер использует сейчас, и делит на requests:
загрузка (%) = использует сейчас / requests.cpu * 100
Разбор на числах. Под «Заметок» использует 100m при requests.cpu: 50m: 100 / 50 * 100 = 200%. Да, загрузка может быть выше 100%: это значит только «использую вдвое больше брони». Когда под простаивает и использует 1m, загрузка 1 / 50 * 100 = 2%. Именно эти 2% ты увидишь в выводе kubectl get hpa в покое.
Отсюда важнейшее следствие: если requests не задан, делить не на что, и HPA не может посчитать процент. Он показывает <unknown> (неизвестно) и ничего не делает. Это самая частая причина «HPA не работает».
Осторожно, путаница: проценты считаются не от limits и не от размера узла. Поменяешь requests вдвое, и та же нагрузка покажет вдвое другой процент, хотя реальное потребление не изменилось.
Есть ещё тонкость. Если у контейнера задан limits, но не задан requests, Kubernetes сам подставит requests = limits. Поэтому «убрать только requests» и получить <unknown> не выйдет: чтобы сломать проценты, нужно убрать блок resources целиком. Ты сделаешь это в разделе «Сломай и почини».
Прикинь сам:
requests.cpu: 100m, контейнер использует 30m. Какая загрузка покажется HPA? А еслиrequests.cpu: 300m?
30 / 100 * 100 = 30%. При 300m получится 10%: потребление то же, а процент втрое ниже, потому что HPA считает от «брони».
Проверь понимание:
requests.cpu: 100m, контейнер использует 30m. Какая загрузка покажется HPA? А если поставитьrequests.cpu: 300mпри том же потреблении?
Ответ
30 / 100 * 100 = 30%. При requests.cpu: 300m: 30 / 300 * 100 = 10%. Потребление то же, а процент втрое ниже: цифра HPA зависит от того, сколько ты «забронировал».
Главное: загрузка равна текущему потреблению, делённому на
requests.cpu; безrequestsHPA показывает<unknown>и молчит.
Потребление HPA не измеряет сам, ему нужен отдельный сборщик метрик.
Откуда HPA берёт цифры: metrics-server
Кто-то должен измерять, сколько CPU и памяти съел каждый под. У HPA собственных датчиков нет, он спрашивает об этом отдельный компонент.
Цепочка такая:
- На каждом узле работает kubelet (агент Kubernetes, который запускает и следит за подами на этом узле, урок 5.1). Он знает потребление каждого контейнера, потому что читает счётчики cgroups (урок 1.5).
- Metrics-server (сборщик метрик ресурсов) раз в 15 секунд обходит kubelet’ы всех узлов и запоминает последние значения.
- Он отдаёт их через специальный раздел API кластера
metrics.k8s.io. Это тот же API, к которому ходитkubectl, поэтому HPA иkubectl topчитают из одного места.
flowchart LR
C1["контейнер, cgroups"] --> K1["kubelet (узел 1)"]
C2["контейнер, cgroups"] --> K2["kubelet (узел 2)"]
C3["контейнер, cgroups"] --> K3["kubelet (узел 3)"]
K1 --> MS["metrics-server<br>(снимок в памяти)"]
K2 --> MS
K3 --> MS
MS --> API["API metrics.k8s.io"]
API --> TOP["kubectl top"]
API --> HPA["HPA"]
Metrics-server хранит только текущий снимок в памяти, истории нет. Он отвечает на вопрос «сколько сейчас», а не «что было час назад». Поэтому он не заменяет Prometheus (тема 8): нельзя построить график и настроить алерт. Зато он лёгкий и есть в любом кластере, а Prometheus это отдельная система, которую нужно ставить и обслуживать. Штатный HPA от Prometheus не зависит.
Metrics-server общается с kubelet по HTTPS и проверяет его сертификат (удостоверение, по которому один компьютер убеждается, что говорит именно с тем, с кем нужно). В kind сертификаты kubelet выдаёт сам себе и не указывает в них IP-адрес узла, поэтому проверка не проходит и metrics-server не может собрать данные. Флаг говорит «не проверяй». В лаборатории это нормально. В проде так делать нельзя: без проверки к сборщику можно подсунуть чужой «kubelet», поэтому там у kubelet’ов настоящие сертификаты кластерного центра сертификации.
Осторожно, путаница: «kubectl top показывает то же, что Prometheus». Цифры похожи, но top это снимок, усреднённый за последние секунды, а Prometheus копит историю. Для решения «добавить реплику сейчас» снимка достаточно.
Прикинь сам: что ответит
kubectl top podsв кластере без metrics-server?
error: Metrics API not available: API metrics.k8s.io не существует. После установки без --kubelet-insecure-tls в kind под metrics-server запустится, но не станет Ready.
Проверь понимание: что ответит
kubectl top podsв кластере без metrics-server и что изменится, если сервер поставить, но не разрешить ему доверять kubelet’ам в kind?
Ответ
Без metrics-server: error: Metrics API not available (API metrics.k8s.io просто не существует). После установки без --kubelet-insecure-tls под metrics-server запустится, но не станет Ready: он не сможет проверить сертификаты kubelet, и top продолжит выдавать ошибку.
Главное: metrics-server раз в 15 секунд собирает снимки из kubelet и отдаёт их через
metrics.k8s.io; истории он не хранит и Prometheus не заменяет.
С цифрами понятно, теперь посмотрим, как HPA превращает их в число реплик.
Как HPA решает, сколько реплик нужно
HPA это контроллер: программа-цикл внутри кластера. Раз в 15 секунд она делает одно и то же: смотрит на текущее состояние, сравнивает с желаемым и, если они не совпадают, меняет систему. Как термостат: измерил температуру, сравнил с заданной, включил или выключил батарею. Через 15 секунд повторил.
Цель, которую ты задаёшь, называется целевой загрузкой: например, «держи среднюю загрузку CPU на 50% от requests». Формула такая:
нужно реплик = ceil(текущих реплик * текущее значение / целевое значение)
ceil означает «округлить вверх до целого» (2,1 станет 3): реплик не бывает 2,1, а недобрать опаснее, чем перебрать. «Текущее значение» это среднее по всем подам.
Разберём пример целиком. Работает 3 пода, цель 50%, средняя загрузка 100%. Подставляем: 3 * 100 / 50 = 6, ceil(6) = 6. Нужно 6 реплик. Логика проста: нагрузка вдвое выше цели, значит подов должно быть вдвое больше, чтобы нагрузка на каждый упала до цели.
Теперь второй проход, после того как поды стало 6. Общая нагрузка не изменилась, она размазалась по шести подам: средняя загрузка стала 100 * 3 / 6 = 50%. Считаем: 6 * 50 / 50 = 6. Ничего менять не надо, система пришла в равновесие.
Три ограничителя вокруг формулы:
minReplicasиmaxReplicas: результат всегда зажимается между ними. Если формула даёт 24, аmaxReplicas6, будет 6. Максимум защищает от того, чтобы ошибка или атака не съела весь кластер. Минимум держит запас на случай внезапного пика и падения узла.- Допуск 10% (tolerance): если отношение «текущее / цель» отличается от 1 меньше чем на 0,1, HPA ничего не меняет. Например, при цели 50% загрузка от 45% до 55% оставляет реплики как есть. Иначе он дёргал бы реплики из-за малейших колебаний.
- Неготовые поды: свежий под ещё стартует и нагрузку принимать не может. Реальные цифры пода, пока он не Ready, HPA отбрасывает. А когда нагрузка растёт и HPA считает подъём, такие поды входят в расчёт с загрузкой 0% от request: это сдерживает рост и не даёт заказать лишнее из-за ещё не прогревшихся подов.
Прикинь сам: 4 пода, цель 50% CPU, средняя загрузка 52%. Сколько реплик потребует HPA?
Останется 4: отношение 52 / 50 = 1,04 лежит в допуске 10%. Без допуска вышло бы ceil(4 * 1,04) = 5.
Осторожно: две путаницы. Первое: «HPA смотрит на нагрузку узла». Нет, только на загрузку самих подов относительно их requests. Второе: «HPA сразу ставит нужное число реплик». Он выставляет replicas в Deployment, а поды затем создаёт сам Deployment, и до готовности пода проходит время.
Проверь понимание: 4 пода, цель 50% CPU, средняя загрузка 25%,
minReplicas: 2. Сколько реплик потребует HPA? Пересчитай для средней загрузки 52%.
Ответ
Для 25%: ceil(4 * 25 / 50) = ceil(2) = 2. Равно минимуму, HPA уменьшит до 2 (но не сразу, см. ниже про окно стабилизации). Для 52%: отношение 52 / 50 = 1,04, это в пределах допуска 10%, реплики останутся 4. Без допуска получилось бы ceil(4 * 1,04) = 5, вот почему допуск нужен.
Главное: нужно реплик =
ceil(текущих * текущее / цель); результат зажат междуminReplicasиmaxReplicas, а допуск 10% гасит мелкие колебания.
Теперь посмотрим, как это записано в манифесте HPA.
Как устроен объект HPA: читаем манифест
HPA это такой же объект Kubernetes, как Deployment: его описывают в YAML. Пока ты не умеешь читать этот манифест, любая настройка («поменяй цель», «поставь максимум») выглядит как заклинание.
Инструкция для термостата в доме: «какой прибор регулировать, в каких пределах, по какому датчику и до какой температуры». Оговорка: у термостата одна ручка, а у HPA несколько независимых параметров, и они взаимодействуют.
В манифесте HPA пять смысловых частей:
scaleTargetRef: на кого нацелен HPA. Здесь Deployment с именемnotes. HPA не создаёт поды сам, он только меняет числоreplicasу указанного объекта.minReplicasиmaxReplicas: нижний и верхний предел числа реплик.metrics: по чему считать. ТипResourceзначит «ресурс пода» (cpuилиmemory).target.type: Utilizationзначит «проценты отrequests», аaverageUtilizationэто сама цель, например 50.behavior: правила скорости (окно стабилизации уменьшения и шаг роста). Необязательный блок, у всех значений есть умолчания.apiVersion: autoscaling/v2: версия API. Она нужна, чтобы можно было задавать несколько метрик и блокbehavior; у старойv1этого нет.
Возьмём значения из чарта «Заметок»:
spec:
scaleTargetRef: # кем управляем
apiVersion: apps/v1
kind: Deployment
name: notes
minReplicas: 2 # не меньше двух, чтобы пережить падение одного пода
maxReplicas: 6 # не больше шести, чтобы не съесть весь кластер
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # держать среднюю загрузку на 50% от requests
Читается так: «следи за Deployment notes, держи между 2 и 6 подами, регулируй так, чтобы в среднем каждый под использовал половину своей брони CPU». Если сейчас 2 пода при 100%, по формуле нужно ceil(2 * 100 / 50) = 4, HPA поставит replicas: 4.
Прикинь сам: в манифесте
minReplicas: 2,maxReplicas: 6, а формула дала 9. Сколько реплик выставит HPA? А если формула дала 1?
Для 9 будет 6 (потолок), для 1 будет 2 (пол). Результат формулы всегда зажимается между границами.
Осторожно: не думай, что HPA хранит число реплик у себя. Число записано в поле replicas самого Deployment, а HPA лишь периодически его перезаписывает. Поэтому «ручное» kubectl scale на работающем HPA проживёт до следующего цикла (15 секунд) и будет перезаписано.
Проверь понимание: в манифесте
minReplicas: 2,maxReplicas: 6, а формула дала 9. Сколько реплик выставит HPA? А если формула дала 1?
Ответ
Для 9: 6 (потолок maxReplicas). Для 1: 2 (пол minReplicas). Результат формулы всегда зажимается между границами.
Главное: HPA нацелен на объект через
scaleTargetRef, метрики задаются вmetrics, а число реплик живёт в Deployment, а не в самом HPA.
Загрузка CPU видит не все проблемы, и можно считать несколько метрик.
Несколько метрик и память: что ещё можно считать
Загрузка CPU отражает не всякую проблему. Приложение может упереться в память, и тогда процессор скучает, а HPA по CPU ничего не делает.
Кассиры в кофейне устают по-разному: один считает деньги быстро, другой медленно. Если смотреть только на скорость счёта денег, не заметишь, что на складе закончились стаканы. Оговорка: в отличие от кофейни, HPA умеет следить за несколькими показателями одновременно.
В блок metrics можно записать несколько пунктов: например, CPU 50% и память 70%. HPA считает нужное число реплик по каждой метрике отдельно и берёт наибольшее. Причина проста: если хоть один показатель говорит «не хватает», значит, не хватает. Память в роли метрики хуже CPU в одном: память редко сама падает после спада нагрузки (приложение держит её занятой), поэтому при таком сигнале реплики долго не уменьшаются.
Сейчас 4 пода. По CPU: цель 50%, средняя 40%, ceil(4 * 40 / 50) = ceil(3,2) = 4. По памяти: цель 70%, средняя 105%, ceil(4 * 105 / 70) = ceil(6) = 6. HPA выберет большее: 6.
Прикинь сам: по CPU формула даёт 3 реплики, по памяти 5. Сколько реплик поставит HPA?
Пять: HPA берёт максимум из результатов по всем метрикам.
Осторожно: не думай, что метрики «усредняются» между собой. Нет: побеждает самая требовательная. Поэтому добавление второй метрики может только увеличить число реплик, а не уменьшить.
Проверь понимание: по CPU формула даёт 3, по памяти 5. Сколько реплик поставит HPA?
Ответ
5: берётся максимум из результатов по всем метрикам.
Главное: каждая метрика считается отдельно, побеждает самая требовательная; память как метрика плохо уменьшается после спада нагрузки.
Все числа HPA придумывает человек, и их лучше подбирать по измерениям.
Как выбрать requests, цель и границы для нового сервиса
Все числа HPA (requests, цель, minReplicas, maxReplicas) придумывает человек. Если поставить их с потолка, HPA будет либо молчать, либо метаться. Есть простой способ выбрать их по наблюдению.
Подбор размера одежды: сначала меряешь себя (реальное потребление), потом берёшь размер с небольшим запасом, а не «на глаз». Оговорка: у сервиса «размер» меняется со временем, поэтому мерку приходится переснимать после больших изменений приложения.
Порядок из четырёх шагов:
- Замерь потребление CPU одного пода в спокойное время и под обычной нагрузкой (
kubectl top pod, задание 1 этого урока). Получи типичное число, например 40m. - Поставь
requests.cpuчуть выше типичной нагрузки, чтобы в обычное время загрузка была ниже цели. - Цель HPA выбирай в диапазоне 50-70%: запас нужен, потому что новый под стартует минуту, и всё это время остальные работают под нагрузкой.
minReplicasне меньше двух, если сервис должен пережить падение одного пода или узла;maxReplicasвыбирай по ёмкости кластера и базы данных, а не «побольше».
Под «Заметок» в покое 1m, под обычной нагрузкой 35m, в пике 120m. Ставим requests.cpu: 50m: в обычное время загрузка 35 / 50 = 70%, это уже у цели, в пике 240%. Цель 50%: при одном поде под нагрузкой 35m HPA захочет ceil(1 * 70 / 50) = 2 пода. Значит, requests лучше поднять до 70m: обычная загрузка станет 50%, и HPA в покое ничего не меняет.
Прикинь сам: в обычное время загрузка одного пода 70% при цели 50%, и HPA держит 2 пода вместо 1. Что в этом неверно?
Ничего не сломано: requests занижен относительно реального потребления. Подними requests или цель: изменится масштаб измерения, а не потребление.
Осторожно: не думай, что maxReplicas можно поставить 100 «на всякий случай». При ошибке (например, утечке CPU или атаке) HPA честно поднимется до сотни подов, съест кластер и зальёт базу соединениями. Потолок это страховка для кластера.
Проверь понимание: в обычное время загрузка одного пода 70% при цели 50%. HPA держит 2 пода вместо 1. Что в этом неверно и что поправить?
Ответ
Ничего не сломано: requests занижен относительно реального потребления, и 70% от брони это «много» по мнению HPA. Либо подними requests (бронь станет реалистичной), либо подними цель. Поменяется не потребление, а масштаб, в котором оно измеряется.
Главное:
requestsставят чуть выше типичной нагрузки, цель 50-70%,minReplicasне меньше двух,maxReplicasпо ёмкости кластера и базы.
Важная особенность HPA в том, что он спешит вверх и не спешит вниз.
Быстро вверх, медленно вниз
Ошибка «слишком мало реплик» и ошибка «слишком много» стоят по-разному. Мало реплик: пользователи ждут и получают ошибки, а это авария. Много: просто чуть дороже. Поэтому HPA устроен асимметрично: вверх торопится, вниз не спешит.
Если бы он убирал реплики при каждом падении нагрузки, сервис бы «качало»: нагрузка на секунду упала, реплик стало меньше, нагрузка на оставшихся выросла, реплики добавились, нагрузка упала, снова убрали. Такие качели называются флаппинг (flapping, «хлопанье»). Каждый цикл стоит времени на запуск и остановку подов.
Для этого у HPA есть блок behavior (поведение):
scaleUp(рост) по умолчанию без задержки, может быстро удваивать число реплик или добавлять несколько штук за 15 секунд.scaleDown(сокращение) использует окно стабилизации (stabilization window) 300 секунд. Механизм: HPA каждые 15 секунд считает «сколько реплик нужно сейчас» и запоминает результат. Уменьшает он только до максимума из всех результатов за последние 5 минут.
Пример окна. Цель 50%, сейчас 6 реплик, нагрузка упала. Расчёты за последние 5 минут были такие: 6, 6, 5, 4, 2, 2, 2. Максимум 6, поэтому HPA не убирает ничего. Только когда все пять минут подряд формула говорит «нужно 2 или меньше», реплик станет 2. Если за это время хоть раз всплеск давал 5, реплик останется 5. Так один короткий провал нагрузки не приводит к уменьшению.
Чтобы уроки шли быстрее, в лаборатории окно ставят 60 секунд, в проде оставляют 300.
Ещё одно обстоятельство: новый под готов не мгновенно. Образ (image) может скачиваться, приложение запускается, проходят проба старта и проба готовности (урок 5.7). Пока это длится, старые поды работают под пиковой нагрузкой. Поэтому цель ставят не 90%, а 50-70%: запас нужен именно на время, пока стартуют новые.
Прикинь сам: нагрузка упала в 12:00. Через 2 минуты HPA всё ещё держит 6 реплик, хотя формула давно говорит «нужно 2». Это поломка?
Нет, это окно стабилизации: уменьшение идёт до максимума рекомендаций за последние 300 секунд, и пять минут ещё не прошли.
Осторожно: ошибка «слишком мало реплик» дороже ошибки «слишком много», поэтому HPA устроен несимметрично.
Проверь понимание: нагрузка резко упала в 12:00. Через 2 минуты HPA всё ещё держит 6 реплик, хотя формула уже давно говорит «нужно 2». Это поломка?
Ответ
Нет, это окно стабилизации. Уменьшение идёт до максимума рекомендаций за последние 300 секунд, а пять минут после 12:00 ещё не прошли. Ближе к 12:05 реплик станет 2.
Главное: вверх HPA идёт быстро, вниз по окну стабилизации в 300 секунд, чтобы избежать флаппинга; запас цели 50-70% нужен на время старта подов.
Посмотрим, сколько в сумме проходит от пика до новых подов.
Сколько времени проходит от пика до нового пода
HPA не мгновенный, и это надо понимать заранее, иначе «автоскейлер тормозит» будет неожиданностью. Вот путь одной волны нагрузки с реальными задержками (числа примерные, у тебя будут другие):
flowchart TD
T0["0 с: нагрузка выросла"] --> T1["0-15 с: metrics-server ещё не обновил снимок<br>(обновляется раз в 15 с)"]
T1 --> T2["до 15 с: HPA ещё не заметил<br>(он сверяется раз в 15 с)"]
T2 --> T3["около 30 с: HPA посчитал и поставил replicas = 6 в Deployment"]
T3 --> T4["+5 с: Deployment создал новые поды, планировщик выбрал узлы"]
T4 --> T5["+10-30 с: образ уже на узле (или скачивается), контейнер стартует"]
T5 --> T6["+10 с: проба готовности прошла, под получает запросы"]
T6 --> T7["итого 1-2 минуты от пика до полной помощи"]
Отсюда два вывода. Первый: HPA не спасает от мгновенного пика (за минуту сервис успеет упасть), для этого нужен запас в minReplicas и цель 50-70%. Второй: чем быстрее стартует под, тем лучше работает автоскейлинг. Лёгкий образ, быстрая проба готовности, отсутствие долгой инициализации важны не только для выкатки (урок 5.7), но и для масштабирования.
Осторожно, путаница: «HPA реагирует за секунды». Нет, самое быстрое, на что можно рассчитывать, это десятки секунд до решения плюс время старта пода.
Прикинь сам: сайт ждёт рассылку в 10:00 и рост нагрузки в 20 раз за 10 секунд. Хватит ли HPA с
minReplicas: 2?
Нет: пока HPA заметит рост и поды запустятся, пройдёт минута-две, а две реплики упадут раньше. Перед известным событием реплики поднимают заранее.
Проверь понимание: сайт ждёт рекламную рассылку в 10:00 и ожидает рост нагрузки в 20 раз за 10 секунд. Хватит ли одного HPA с
minReplicas: 2?
Ответ
Нет. Пока HPA заметит рост, посчитает и запустит новые поды, пройдёт минута-две, а две реплики упадут раньше. Перед известным событием реплики поднимают заранее (временно повысить minReplicas или вручную), а HPA остаётся страховкой от неожиданного роста.
Главное: от пика до помощи проходит 1-2 минуты, HPA не спасает от мгновенного пика, а быстрый старт пода улучшает автоскейлинг.
Чтобы проверять работу HPA, нужно уметь читать его описание.
Как читать kubectl describe hpa
Главный инструмент разбора при проблемах: kubectl describe hpa notes. В нём три места, на которые смотришь по порядку.
- Metrics: строка
resource cpu on pods (as a percentage of request): 187% (93m) / 50%. Слева текущее значение (в процентах и в миллиядрах), справа цель. Если вместо числа<unknown>, дальше можно не читать: метрик нет. - Conditions: три условия с состоянием
TrueилиFalse.AbleToScale(может ли HPA менять реплики),ScalingActive(есть ли метрики, с которыми можно считать;Falseпри<unknown>) иScalingLimited(упёрлись ли вminилиmax;Trueсо словамиTooManyReplicasзначит «хочет больше, но потолок»). - Events: журнал решений:
SuccessfulRescaleс причиной и новым размером, либо ошибки вродеFailedGetResourceMetric.
Такой порядок быстро отвечает на вопрос «почему HPA не масштабирует»: нет метрик, не может менять, упёрся в границу или всё в порядке и цель просто не достигнута.
Прикинь сам: в
Conditionsу HPAScalingActive: False. Что это говорит?
У HPA нет метрик, с которыми можно считать: чаще всего <unknown> из-за отсутствия requests или metrics-server.
Осторожно: <unknown> в Metrics означает, что считать нечего, и дальше читать не нужно.
Главное: смотри по порядку
Metrics(текущее и цель),Conditions(AbleToScale,ScalingActive,ScalingLimited) иEvents.
Теперь о том, что видят пользователи, пока HPA меняет поды.
Что видят пользователи, когда HPA добавляет и убирает поды
Новичок думает, что раз реплики меняются сами, то запросы в этот момент могут пропасть. Этого не происходит, если помнить, как Service выбирает, кому отдать запрос.
Новая касса в кофейне открывается для покупателей, только когда кассир сел, включил терминал и повесил табличку «открыто». А когда кассу закрывают, она сначала перестаёт звать новых покупателей, обслуживает тех, кто уже стоит, и только потом выключается. Оговорка: если кассир «открылся» раньше времени (проба готовности слишком мягкая), первые покупатели получат ошибку.
Новый под получает запросы от Service не сразу после создания, а когда проба готовности (readiness, урок 5.7) скажет «да». Пока она красная, Service его не использует. При уменьшении Kubernetes одновременно начинает убирать под из списка получателей Service и посылает процессу сигнал SIGTERM («заверши работу», урок 1.4), а потом ждёт, пока запросы в полёте закончатся. Эти два действия идут параллельно, поэтому приложению нужна пауза до закрытия порта (preStop с sleep, урок 5.7). По умолчанию ждёт до 30 секунд.
HPA поднял реплики с 2 до 4. Два новых пода создаются, скачивают образ (если его нет на узле), запускаются, и через 20-40 секунд проба готовности становится зелёной. Только после этого Service начинает отправлять им запросы. Всё это время старые два пода продолжают работать под нагрузкой: поэтому цель 50-70%, а не 95%, оставляет им запас.
Прикинь сам: новый под создан, но проба готовности пока красная. Придут ли на него запросы?
Нет: Service не включает под в список получателей, пока readiness не станет зелёной.
Осторожно: не думай, что быстрый рост реплик мгновенно снижает нагрузку. Нет: между решением HPA и первым запросом на новом поде проходит время запуска, и оно полностью лежит на приложении и пробе.
Проверь понимание: новый под создан, но проба готовности пока красная. Придут ли на него запросы?
Ответ
Нет. Service не включает под в список получателей, пока проба готовности не станет зелёной. Поэтому плохо настроенная проба ломает автомасштабирование: либо под принимает запросы слишком рано, либо слишком долго ждёт.
Главное: новый под получает запросы после readiness, а убираемый сначала исключается из Service, потом получает
SIGTERM.
Остаётся один конфликт: кто хозяин числа реплик.
HPA и replicas в Deployment: два хозяина одного числа
Поле replicas в Deployment можно править вручную, через Helm или через HPA. Если хозяев двое, они бьются. Пусть в чарте replicaCount: 3, а HPA поднял до 6. Ты делаешь helm upgrade: Helm видит в шаблоне replicas: 3 и возвращает 3. Через 15 секунд HPA снова поднимает до 6. При каждой выкатке число реплик проваливается и потом растёт.
Решение простое: когда HPA включён, поле replicas в шаблоне вообще не пишут, число реплик принадлежит только HPA. В Helm это условие {{- if not .Values.hpa.enabled }} вокруг строки replicas. Тот же принцип действует в GitOps (тема 9): если в Git лежит replicas: 3, а HPA его меняет, GitOps-инструмент будет считать это «расхождением» и бороться с HPA.
Прикинь сам: зачем
replicaCountостаётся вvalues.yaml, если приhpa.enabled=trueон не используется?
Он нужен, когда HPA выключен: в окружении без автомасштабирования число реплик задаётся им. Один чарт обслуживает оба режима.
Осторожно: два хозяина числа реплик бьются: при каждой выкатке число проваливается и потом растёт.
Проверь понимание: зачем
replicaCountостаётся вvalues.yaml, если приhpa.enabled=trueон не используется?
Ответ
Он нужен, когда HPA выключен: в окружении без автомасштабирования число реплик по-прежнему задаётся им. Один чарт обслуживает оба режима.
Главное: у числа реплик должен быть один хозяин: при включённом HPA поле
replicasв шаблоне не пишут.
И наконец, границы HPA и соседние инструменты.
Что HPA не умеет и кто его соседи
- Не работает без
requests. Самая частая причина<unknown>. - Не лечит узкое место ниже по цепочке. Если все реплики упираются в одну PostgreSQL, лишние реплики только добавят нагрузку на базу и ничего не ускорят.
- Не масштабирует базы данных. PostgreSQL от лишних реплик сам не станет масштабируемым: в StatefulSet каждая реплика хранит свои данные, и «просто добавить копию» нельзя.
- Не реагирует на не-CPU причины. Если воркеры ждут очередь или ответ базы, CPU остаётся низким, HPA видит «всё спокойно».
- Не добавляет узлы. Если реплики некуда поставить, они зависнут в
Pending(ждут места), потому что планировщик не находит узел с нужным свободнымrequests. Узлы добавляют другие инструменты: Cluster Autoscaler или Karpenter (в облаке, тема 6).
Соседи, которых путают с HPA:
| Инструмент | Что меняет | Когда нужен |
|---|---|---|
| HPA | число реплик | приложения без состояния, нагрузка по CPU/памяти |
| VPA (Vertical Pod Autoscaler) | requests и limits самого пода |
приложение нельзя размножить |
| KEDA (Kubernetes Event-driven Autoscaling) | число реплик по событиям: длина очереди, метрика Prometheus, расписание | нагрузка не связана с CPU; умеет уводить в ноль реплик |
| Cluster Autoscaler, Karpenter | число узлов | подам не хватает места |
HPA и VPA на одну и ту же метрику CPU вешать нельзя: VPA меняет requests, от которых HPA считает проценты, и они начнут тянуть систему в разные стороны.
Прикинь сам: HPA поднял реплики до
maxReplicas, но часть подов вPending. Что это значит и кто должен добавить узлы?
Планировщик не находит узел с нужным свободным requests. HPA узлы не добавляет: это делает Cluster Autoscaler или Karpenter, в kind нужно освободить ресурсы.
Осторожно: HPA не добавляет узлы, не масштабирует базы данных и не лечит узкое место ниже по цепочке.
Проверь понимание: HPA поднял реплики до
maxReplicas, но часть подов вPending. Что это значит и кто должен добавить узлы?
Ответ
В кластере не хватает ресурсов под requests новых подов, планировщик не находит узел. HPA узлы не добавляет. В облаке это делает Cluster Autoscaler или Karpenter, в kind нужно освободить ресурсы или уменьшить requests.
Главное: HPA меняет число реплик, а не узлы, не лечит узкое место ниже по цепочке и не масштабирует базы; соседи VPA, KEDA и Cluster Autoscaler решают другие задачи.
Теперь пора поставить metrics-server и настроить HPA для «Заметок».
Практика
Задание 1. Ставим metrics-server и смотрим kubectl top
Цель: получить метрики ресурсов в кластере kind и научиться читать kubectl top.
Предскажи: что ответит kubectl top pods -n notes в кластере без metrics-server? А сразу после установки, до того как ты поправишь TLS?
Ответ
Без metrics-server: error: Metrics API not available. Сразу после установки без флага --kubelet-insecure-tls под metrics-server будет запущен, но не Ready (не может проверить сертификаты kubelet), и top продолжит ругаться.
Шаги:
-
Убедись, что кластер и контекст на месте.
kubectl config use-contextпереключаетkubectlна нужный кластер (kind называет контекстыkind-<имя кластера>),kubectl top pods -n notesпоказывает нагрузку подов в namespacenotes(-nуказывает namespace):kubectl config use-context kind-notes kubectl top pods -n notes -
Установи metrics-server манифестом релиза.
kubectl apply -f <адрес>создаёт в кластере всё, что описано в YAML по этому адресу. Версию подставь актуальную со страницы релизов проекта kubernetes-sigs/metrics-server (тегlatestне используем, версия закрепляется в команде).${MS_VERSION}это подстановка значения переменной оболочки:# проверь актуальную версию на странице проекта, v0.8.0 здесь пример MS_VERSION=v0.8.0 kubectl apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${MS_VERSION}/components.yaml" -
Добавь флаг для kind (только для лаборатории). Разбор:
patch deployment metrics-server -n kube-systemправит существующий Deployment;--type=jsonзначит «патч в формате JSON Patch: список операций»;-pзадаёт сам патч. Операцияaddпо пути.../containers/0/args/-добавляет элемент в конец (-) спискаargsпервого (0) контейнера, то есть новый аргумент запуска. Вторая команда ждёт, пока новая версия пода запустится:kubectl patch deployment metrics-server -n kube-system --type=json \ -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s -
Подожди 30-60 секунд (первый опрос) и смотри метрики.
top nodesпоказывает узлы,top podsподы:kubectl top nodes kubectl top pods -n notes
Что должно получиться (числа у тебя будут другими):
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
notes-control-plane 118m 2% 745Mi 9%
notes-worker 47m 1% 412Mi 5%
notes-worker2 44m 1% 398Mi 5%
NAME CPU(cores) MEMORY(bytes)
notes-6d9f8c7b5d-2xk4q 1m 31Mi
notes-6d9f8c7b5d-9tqzr 1m 30Mi
notes-6d9f8c7b5d-mw7lp 1m 31Mi
postgres-0 4m 58Mi
Как читать вывод:
NAME: узел или под. Три подаnotes-6d9f8c7b5d-...это реплики Deployment (хвост имени случайный),postgres-0это под StatefulSet.CPU(cores): сколько процессора использует сейчас.1mэто один миллиядро: «Заметки» в покое почти ничего не делают.118mу управляющего узла это 0,118 ядра.CPU(%)(только у узлов): доля от мощности узла. Это процент от узла, а не то, что считает HPA: HPA считает отrequestsподов.MEMORY(bytes): занятая память в мебибайтах (Mi, урок 5.7).- Для HPA важно сопоставить
1mсrequests50m: 1 / 50 * 100 = 2%, и это загрузка «Заметок» в покое.
Объясни себе:
- Почему CPU у «Заметок» в покое 1m, а
requests50m? Что из этого HPA возьмёт за 100%? - Почему флаг
--kubelet-insecure-tlsдопустим в kind и недопустим в проде? - Через какой API
kubectl topполучает данные?
Типичные ошибки:
error: Metrics API not available: metrics-server не установлен или ещё не Ready. Проверьkubectl get pods -n kube-system -l k8s-app=metrics-server(-lфильтрует по метке).E... scraper.go: "Failed to scrape node" err="Get \"https://172.18.0.3:10250/metrics/resource\": tls: failed to verify certificate: x509: cannot validate certificate for 172.18.0.3 because it doesn't contain any IP SANs"(в логах metrics-server): kubelet с самоподписанным сертификатом. Добавь--kubelet-insecure-tls(шаг 3).error: metrics not available yet: прошло меньше минуты с запуска. Подожди и повтори.
Задание 2. HPA руками: requests, цель и нагрузка
Цель: увидеть, как число реплик следует за нагрузкой, и посчитать результат по формуле.
Предскажи: Deployment notes (requests 50m, limits 200m) получает нагрузку, которая держит каждый под на пределе limit. HPA настроен на цель 50%, min 2, max 6. Сколько реплик будет через 2-3 минуты и почему именно столько?
Ответ
Под на пределе limit использует 200m, то есть 200 / 50 * 100 = 400% от request 50m. По формуле ceil(3 * 400 / 50) = 24, но maxReplicas равен 6, поэтому будет 6. Число упирается в потолок.
Шаги:
-
Убедись, что приложение развёрнуто (из 5.9 чартом или из 5.7 манифестом) и
requestsзаданы. Разбор:-o jsonpath='{...}'выводит только выбранное поле,{.spec.template.spec.containers[0].resources}это путь к блокуresourcesпервого контейнера,{"\n"}добавляет перевод строки:kubectl -n notes get deploy notes -o jsonpath='{.spec.template.spec.containers[0].resources}{"\n"}'Ожидаемо увидишь JSON с
requestsиlimits. Если пусто, задай ресурсы по уроку 5.7: без них дальше не получится. -
Создай HPA командой (в задании 3 он переедет в чарт).
autoscale deployment notesсоздаёт HPA для Deploymentnotes;--cpu=50%цель по CPU;--minи--maxграницы числа реплик:kubectl -n notes autoscale deployment notes --cpu=50% --min=2 --max=6 kubectl -n notes get hpa notes -
В первом терминале следи за HPA. Ключ
-w(watch) держит команду запущенной и печатает строку при каждом изменении:kubectl -n notes get hpa notes -w -
Во втором терминале создай нагрузку. Разбор:
kubectl run loadзапускает одиночный подloadиз образаbusybox:1.37.0(маленький образ с набором утилит, включаяwget);--restart=Neverзначит «не перезапускай, когда закончится»; после--идёт команда внутри пода. В скриптеfor i in 1 2 3 4запускает четыре одинаковых цикла;( ... ) &запускает цикл в фоне, чтобы четыре шли параллельно;while true; do ... doneповторяет бесконечно;wget -q -O /dev/null "..."делает запрос и выбрасывает ответ (-qбез шума). Адресhttp://notes.notes.svc:8080/burn?sec=5это Servicenotesв namespacenotes, эндпоинт/burnзанимает одно ядро на 5 секунд (урок 1.5). Последнее словоwaitне даёт скрипту завершиться:kubectl -n notes run load --image=busybox:1.37.0 --restart=Never -- /bin/sh -c ' for i in 1 2 3 4; do ( while true; do wget -q -O /dev/null "http://notes.notes.svc:8080/burn?sec=5"; done ) & done wait' -
Через 2-3 минуты в третьем терминале посмотри итог и события.
tail -15оставляет последние 15 строк:kubectl -n notes get pods -l app.kubernetes.io/name=notes kubectl -n notes describe hpa notes | tail -15 -
Останови нагрузку и наблюдай спад (займёт около 5 минут из-за окна стабилизации;
--nowудаляет под без ожидания):kubectl -n notes delete pod load --now
Что должно получиться (проценты у тебя будут другие: важно, что реплик стало 6, а после остановки нагрузки через несколько минут стало 2):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
notes Deployment/notes cpu: 2%/50% 2 6 3 20s
notes Deployment/notes cpu: 187%/50% 2 6 3 75s
notes Deployment/notes cpu: 187%/50% 2 6 6 90s
notes Deployment/notes cpu: 102%/50% 2 6 6 2m
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 95s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage of request) above target
Как читать вывод:
TARGETS:текущая/цель.cpu: 187%/50%значит «средняя загрузка 187% от requests, цель 50%». Если вместо числа<unknown>, HPA не видит метрик (см. типичные ошибки).MINPODSиMAXPODS: границы, которые ты задал.REPLICAS: сколько реплик сейчас. Строка с187%и тремя репликами: HPA уже увидел нагрузку, но новые поды ещё не созданы. Следующая строка уже с шестью.- Из 187% через формулу:
ceil(3 * 187 / 50) = ceil(11,2) = 12, но потолок 6, поэтому 6. - Загрузка упала до 102%, потому что те же запросы разошлись на шесть подов (187 * 3 / 6 = 93,5%, плюс неравномерность). Реплик всё равно 6: 102% выше цели 50%, а потолок не даёт расти дальше.
Eventsвнизуdescribeобъясняют каждое решение:SuccessfulRescaleи причину. Если HPA не работает, читай события первым делом.
Объясни себе:
- Почему после
SuccessfulRescaleзагрузка упала с 187% до 102%, но реплик всё равно 6? - Почему уменьшение занимает минуты, а увеличение секунды?
- Что бы произошло, если бы у контейнера не было
requests?
Типичные ошибки:
TARGETSпоказывает<unknown>/50%, в событияхfailed to get cpu utilization: missing request for cpu in container notes of Pod notes-...: у контейнера нетresources.requests.cpu. Задай его в Deployment (для чарта вvalues.yaml).unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io): нет metrics-server, вернись к заданию 1.Error from server (AlreadyExists): horizontalpodautoscalers.autoscaling "notes" already exists: HPA уже создан. Удали егоkubectl -n notes delete hpa notesи создай заново.
Задание 3. Шаг проекта: HPA в Helm-чарте «Заметок»
Цель: перенести HPA в чарт, включаемый флагом hpa.enabled, и поднять версию чарта до 0.1.1.
Предскажи: что произойдёт с числом реплик, если оставить в Deployment replicas: {{ .Values.replicaCount }} и при этом включить HPA, а потом сделать helm upgrade?
Ответ
Каждый helm upgrade будет сбрасывать реплики к replicaCount, а HPA будет снова их менять. Возникает борьба двух источников истины: реплики дёргаются при каждой выкатке. Поэтому при включённом HPA поле replicas в шаблоне пропускают.
Шаги:
-
Удали HPA, созданный командой (чарт создаст свой, иначе Helm упрётся в уже существующий ресурс):
kubectl -n notes delete hpa notes -
Добавь в конец
helm/notes/values.yaml(значения по умолчанию для шаблона;enabled: falseзначит «по умолчанию HPA выключен»):# автомасштабирование по CPU (включается флагом) hpa: enabled: false minReplicas: 2 maxReplicas: 6 targetCPU: 50 -
Создай
helm/notes/templates/hpa.yaml. Разбор построчно:{{- if .Values.hpa.enabled }}…{{- end }}создаёт весь ресурс только если флаг включён;apiVersion: autoscaling/v2версия API автомасштабирования;scaleTargetRefговорит, чем управляет HPA (Deployment с именем релиза);minReplicas/maxReplicasграницы;metricsперечисляет, по чему считать: типResource, ресурсcpu, цельUtilization(проценты отrequests), значение изvalues;behavior.scaleDownзадаёт окно стабилизации. Строкаinclude "notes.labels" . | nindent 4подставляет общие метки из helpers чарта (урок 5.9) с отступом в 4 пробела:{{- if .Values.hpa.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ .Release.Name }} labels: {{- include "notes.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ .Release.Name }} minReplicas: {{ .Values.hpa.minReplicas }} maxReplicas: {{ .Values.hpa.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.hpa.targetCPU }} behavior: scaleDown: # окно короче умолчания 300 с, чтобы урок шёл быстрее stabilizationWindowSeconds: 60 {{- end }} -
В
helm/notes/templates/deployment.yamlзамени строкуreplicas: ...на условие (остальное не трогай): при включённом HPA поляreplicasв результате не будет:spec: {{- if not .Values.hpa.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} -
Подними версию чарта в
helm/notes/Chart.yamlдо0.1.1(appVersionостаётся"0.4.1").sed -i 's/старое/новое/'заменяет текст в файле на месте,^и$это начало и конец строки:# GNU sed; на macOS: sed -i '' ... sed -i 's/^version: 0.1.0$/version: 0.1.1/' helm/notes/Chart.yaml grep -E '^(version|appVersion)' helm/notes/Chart.yaml -
Проверь рендер в обоих режимах.
helm lintищет ошибки в чарте;helm templateпоказывает YAML, который получится, не трогая кластер;grep -cсчитает совпавшие строки;--set hpa.enabled=trueвключает флаг для одного запуска:helm lint helm/notes helm template notes helm/notes -n notes | grep -c HorizontalPodAutoscaler helm template notes helm/notes -n notes --set hpa.enabled=true | grep -E 'kind: Horizontal|replicas:|minReplicas|maxReplicas' -
Выкати чарт с включённым HPA и проверь.
--rollback-on-failureоткатит релиз на прошлую рабочую ревизию сам, если выкатка не удалась (в Helm 3 и старых статьях этот флаг называется--atomic, в Helm 4 он устарел),--timeout 3mограничивает ожидание:helm upgrade --install notes helm/notes -n notes -f helm/notes/values-dev.yaml \ --set hpa.enabled=true --rollback-on-failure --timeout 3m kubectl -n notes get hpa,deploy notes -
Повтори нагрузку из задания 2 (шаг 4), убедись, что реплики растут до 6, затем
kubectl -n notes delete pod load --now. -
Зафиксируй в git:
git add helm/notes git commit -m "helm: HPA (hpa.enabled), chart 0.1.1"
Что должно получиться:
version: 0.1.1
appVersion: "0.4.1"
==> Linting helm/notes
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
0
kind: HorizontalPodAutoscaler
minReplicas: 2
maxReplicas: 6
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/notes Deployment/notes cpu: 3%/50% 2 6 2 40s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/notes 2/2 2 2 3h
Как читать вывод:
[INFO] Chart.yaml: icon is recommended: подсказка, не ошибка. Важна строка0 chart(s) failed.- Число
0в начале третьего блока это рендер без флага: HPA нет. Во втором рендере естьkind: HorizontalPodAutoscalerи границы, а строкиreplicas:нет: её пропустил шаблон Deployment (minReplicasиmaxReplicasпод фильтрreplicas:с маленькой буквы не подходят). - Последний блок: слева
REFERENCEпоказывает, чем управляет HPA,REPLICAS 2это минимум (в покое нагрузка 3%), у DeploymentREADY 2/2: две реплики из двух готовы.
Объясни себе:
- Почему
replicaCountвvalues.yamlостаётся, хотя приhpa.enabled=trueон не используется? - Что бы случилось при
hpa.minReplicas: 1для сервиса, который должен выдерживать падение одного узла? - Как связаны
hpa.targetCPUиresources.requests.cpuизvalues.yaml?
Типичные ошибки:
Error: UPGRADE FAILED: ... horizontalpodautoscalers.autoscaling "notes" already exists(илиinvalid ownership metadata): HPA, созданный командой, ещё жив. Удали его (шаг 1) и повтори.Error: template: notes/templates/hpa.yaml:1:14: executing "notes/templates/hpa.yaml" at <.Values.hpa.enabled>: nil pointer evaluating interface {}.enabled: вvalues.yamlнет блокаhpa. Добавь его (шаг 2).Error: INSTALLATION FAILED: ... hpa.yaml: yaml: line 12: did not find expected key: неверныйnindentвinclude. Смотриhelm template --debug.
Нейросеть не видит реальную нагрузку: числа
requestsи цель проверяй измерениемkubectl top, а не доверяй расчёту вслепую.
Сломай и почини
Скачай скрипт и запусти сценарий, не читая его. Скрипту нужен работающий кластер kind, установленный metrics-server (задание 1) и HPA notes (задание 2 или 3):
curl -fsSL -o /tmp/break-5.11.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/5.11/break.sh
bash /tmp/break-5.11.sh 1
Номера сценариев 1-3. Вернуть как было: bash /tmp/break-5.11.sh fix. Запускай без sudo: скрипт работает через твой kubectl.
Симптом
Тебе пишут: «после нагрузочного теста реплики не выросли, хотя сервис тормозит» или «реплики то растут, то падают каждые пару минут». Начни с kubectl -n notes get hpa и kubectl -n notes get pods.
Пока мониторишь HPA, создай нагрузку из задания 2 (шаг 4), иначе симптом не проявится.
Гипотезы
Составь список причин до проверок: нет метрик (metrics-server, requests), цель выставлена неверно, слишком короткое окно уменьшения, упёрлись в maxReplicas, нет места для подов.
Проверки
kubectl -n notes get hpa notes
kubectl -n notes describe hpa notes | tail -20
kubectl -n notes get deploy notes -o jsonpath='{.spec.template.spec.containers[0].resources}{"\n"}'
kubectl get pods -n kube-system -l k8s-app=metrics-server
kubectl top pods -n notes
kubectl get apiservice v1beta1.metrics.k8s.io
Последняя команда спрашивает у кластера, доступен ли раздел API metrics.k8s.io: столбец AVAILABLE должен быть True.
Исправление
Разбор трёх сценариев
Сценарий 1: <unknown>/50% из-за отсутствия requests. В describe hpa событие failed to get cpu utilization: missing request for cpu in container notes of Pod .... Из Deployment убрали блок resources целиком (если оставить только limits, Kubernetes подставит requests = limits, и проценты посчитаются). Исправление: вернуть requests.cpu: 50m (в чарте: блок resources в values.yaml) и выкатить. Через минуту TARGETS покажет число. Урок: HPA считает проценты от requests, без них считать нечего.
Сценарий 2: нет metrics-server. kubectl top отвечает Metrics API not available, get apiservice показывает False (MissingEndpoints) или FailedDiscoveryCheck. Deployment metrics-server остановлен (0 реплик) или не Ready. Исправление: вернуть 1 реплику (kubectl -n kube-system scale deploy metrics-server --replicas=1) и дождаться rollout status. Урок: HPA зависит от системного компонента, и его нужно мониторить так же, как приложение.
Сценарий 3: реплики дёргаются. В HPA цель занижена до 10% (у «Заметок» в покое 2%, при малейшей нагрузке цифра сразу сильно выше цели, и HPA поднимает реплики) и убрано окно стабилизации (stabilizationWindowSeconds: 0): как только нагрузка спадает, реплики тут же убираются. В describe hpa события SuccessfulRescale идут вверх и вниз одно за другим. Исправление: вернуть behavior.scaleDown.stabilizationWindowSeconds (60 в лаборатории, 300 в проде) и цель 50-70%, при необходимости ограничить скорость уменьшения политикой policies. Урок: медленно вниз, быстро вверх.
ИИ в помощь
Нейросеть хорошо разбирает вывод describe hpa и пересчитывает формулу, но не знает реальную нагрузку твоего сервиса. Общие правила: ИИ-помощник.
Задача: понять, почему HPA ничего не делает.
Я учу Kubernetes. HPA не масштабирует сервис. Вот вывод kubectl get hpa и kubectl describe hpa:
<вставь вывод>
Объясни по порядку Metrics, Conditions и Events, что из них видно и что проверить первым.
Проверь ответ: сверь с колонкой TARGETS и состоянием ScalingActive: при <unknown> причина в requests или metrics-server. Типичная ошибка: совет «увеличить maxReplicas», хотя метрик нет вообще.
Задача: подобрать requests, цель и границы.
Сервис в покое использует <вставь m> CPU, под обычной нагрузкой <вставь m>, в пике <вставь m>.
Предложи requests.cpu, цель HPA, minReplicas и maxReplicas. Покажи расчёт загрузки в обычное время.
Проверь ответ: пересчитай сам: обычное потребление, делённое на requests, должно быть около цели 50-70%. Типичная ошибка: maxReplicas: 100 без учёта ёмкости кластера и базы.
Задача: разобраться с флаппингом.
Реплики HPA то растут, то падают каждые несколько минут. Вот манифест HPA и график нагрузки:
<вставь данные>
Объясни, при чём тут окно стабилизации и как поправить блок behavior.
Проверь ответ: убедись, что в ответе есть scaleDown с окном стабилизации и что формула не обещает мгновенной реакции. Типичная ошибка: совет убрать minReplicas.
Словарик урока
| Термин | Простыми словами |
|---|---|
| HPA (Horizontal Pod Autoscaler) | контроллер, который сам меняет число реплик Deployment по нагрузке |
| Реплика | одна из одинаковых копий пода |
| Stateless | приложение без собственного состояния: все реплики взаимозаменяемы |
| Горизонтальное / вертикальное масштабирование | добавить копии / дать одной копии больше ресурсов |
Миллиядро (m) |
тысячная доля ядра процессора: 50m это 5% одного ядра |
requests |
сколько ресурсов под просит у кластера как минимум; база для процентов HPA |
limits |
потолок ресурсов, выше которого контейнеру не дают |
| Metrics-server | компонент, который собирает у kubelet текущее потребление CPU и памяти и отдаёт через API |
| kubelet | агент Kubernetes на узле, запускающий поды |
kubectl top |
команда, показывающая текущее потребление узлов и подов |
| Целевая загрузка | процент от requests, который HPA старается удерживать |
| Допуск (tolerance) | отклонение до 10% от цели, при котором HPA ничего не меняет |
ceil |
округление вверх до целого |
| Окно стабилизации | время, в течение которого HPA не уменьшает реплики ниже максимума недавних расчётов |
| Флаппинг (flapping) | быстрые качели: реплики то растут, то падают |
behavior |
блок HPA с правилами скорости роста и сокращения |
| VPA | автоскейлер, меняющий requests и limits самого пода |
| KEDA | автоскейлер по событиям (очередь, расписание), умеет уводить в ноль |
| Cluster Autoscaler, Karpenter | инструменты, добавляющие узлы, когда подам не хватает места |
Pending |
состояние пода, которому пока не нашёлся узел |
scaleTargetRef |
поле HPA: какой объект (Deployment) он масштабирует |
Utilization |
тип цели: проценты от requests |
--rollback-on-failure |
флаг Helm 4: при неудаче вернуть прошлую рабочую ревизию (раньше --atomic) |
| Rolling update | постепенная замена старых подов новыми без остановки сервиса |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Как HPA решает, сколько реплик нужно?
Ответ
Раз в 15 секунд сравнивает среднюю метрику по подам с целевой: ceil(реплики * текущее / цель). Результат зажимается между min и max. Для CPU процент считается от requests. Если отклонение меньше 10%, реплики не меняются.
Что хотят услышать: формула, requests как база процента, min и max, допуск 10%.
Красный флаг: «считает от limits» или «смотрит на нагрузку узла».
2. [junior] [часто] Зачем нужен metrics-server и чем он отличается от Prometheus?
Ответ
Он даёт текущий снимок CPU и памяти подов и узлов через API metrics.k8s.io для kubectl top и HPA. Истории и алертов в нём нет. Prometheus хранит временные ряды (историю значений), строит графики и алерты, но это отдельная система.
Что хотят услышать: снимок без истории, источник для HPA, Prometheus для мониторинга.
Красный флаг: «metrics-server это замена Prometheus».
3. [junior] [часто] Чем горизонтальное масштабирование отличается от вертикального?
Ответ
Горизонтальное добавляет реплики (HPA), вертикальное увеличивает ресурсы одного пода (VPA). Горизонтальное подходит приложениям без состояния вроде «Заметок», вертикальное для того, что нельзя размножить.
Что хотят услышать: HPA и VPA, применимость, невозможность вешать оба на CPU.
Красный флаг: «vertical это когда больше узлов».
4. [junior] [на скорость] kubectl get hpa показывает <unknown>/50% в колонке TARGETS. Что проверишь?
Ответ
Смотрю kubectl describe hpa: в событиях будет причина. Обычно две: у контейнера нет requests.cpu (нечего делить на процент) или не работает metrics-server (kubectl top pods не отвечает). Проверяю resources в Deployment, состояние пода metrics-server и apiservice v1beta1.metrics.k8s.io.
Что хотят услышать: describe hpa, requests, metrics-server, kubectl top как быстрый тест.
Красный флаг: «перезапущу HPA» или «подожду», без чтения событий.
5. [junior] [на скорость] Почему HPA не работает без requests?
Ответ
Загрузка считается как использование, делённое на requests. Без requests делить не на что, и HPA показывает <unknown> и ничего не делает. Если задан только limits, Kubernetes сам подставляет requests = limits.
Что хотят услышать: процент от requests, а не от limits.
Красный флаг: «HPA возьмёт проценты от узла».
6. [middle] Нагрузка выросла в 5 раз, а HPA держит 2 реплики и ничего не делает. Твои действия?
Ответ
Начинаю с kubectl describe hpa: в Conditions видно ScalingActive и причину. Проверяю TARGETS: если <unknown>, чиню метрики (requests, metrics-server). Если число есть, сверяю с целью и допуском 10%. Потом смотрю, не упёрлись ли в maxReplicas (ScalingLimited). Если метрика честная, а нагрузка идёт не через CPU (например, ждём БД), CPU не вырос и HPA прав: нужна другая метрика.
Что хотят услышать: Conditions, ScalingActive, ScalingLimited, метрика не всегда CPU.
Красный флаг: «увеличу replicas в Deployment руками» без выяснения причины.
7. [middle] HPA поднял реплики до максимума, но часть подов в Pending. Что дальше?
Ответ
Планировщику не хватает ресурсов узлов под requests новых подов. kubectl describe pod покажет 0/3 nodes are available: 3 Insufficient cpu. HPA узлов не добавляет. В облаке нужен Cluster Autoscaler или Karpenter, локально освобождаю ресурсы или снижаю requests. Заодно проверяю, что requests не завышены.
Что хотят услышать: Insufficient cpu, разница между HPA и Cluster Autoscaler, правильные requests.
Красный флаг: «HPA сам создаст узлы».
8. [middle] Реплики то растут, то падают каждые несколько минут. В чём дело и как чинить?
Ответ
Флаппинг: нагрузка около границы цели, а уменьшение слишком быстрое. Проверяю describe hpa, чередование SuccessfulRescale. Лечу behavior.scaleDown.stabilizationWindowSeconds (по умолчанию 300 с), ограничением скорости уменьшения и более осторожной целью (50-70%). Проверяю, что под быстро становится Ready, иначе новые реплики не успевают взять нагрузку.
Что хотят услышать: окно стабилизации, behavior, цель с запасом, время старта пода.
Красный флаг: «выключу автоскейлинг и поставлю 6 реплик навсегда».
9. [middle] Deployment описан в Git с replicas: 3 и есть HPA. Что не так и как исправить?
Ответ
Два источника истины. Каждый деплой из Git вернёт 3, HPA будет менять обратно, и реплики дёргаются при выкатке. Исправление: убрать replicas из манифеста при включённом HPA (в Helm под условие). В GitOps (тема 9) это ещё и постоянный дрейф (расхождение Git и кластера).
Что хотят услышать: борьба HPA и Git, удаление replicas, ссылка на GitOps.
Красный флаг: «оставлю replicas и буду перезапускать деплой».
10. [middle] Как выбрать requests.cpu и цель для HPA у нового сервиса?
Ответ
Сначала измеряю: нагрузочный тест, kubectl top или метрики Prometheus, смотрю обычное потребление и пик. requests ставлю около типичной нагрузки, цель HPA 50-70% от них, чтобы был запас на время старта новых подов. Слишком маленькие requests дают постоянные скачки процента и лишние реплики, слишком большие ведут к недогрузке узлов и Pending.
Что хотят услышать: измерение вместо угадывания, запас, влияние на планировщик.
Красный флаг: «ставлю 1 CPU везде, чтобы не думать».
11. [middle] Очередь заданий растёт, а CPU воркеров низкий. Подойдёт ли HPA по CPU?
Ответ
Нет: воркеры ждут ввода-вывода, CPU не растёт, и HPA не сработает. Нужна метрика очереди: KEDA либо пользовательские метрики через Prometheus Adapter. KEDA умеет и уводить в ноль реплик.
Что хотят услышать: ограничение CPU-метрики, KEDA, событийное масштабирование.
Красный флаг: «увеличу limits CPU».
12. [middle] Какие метрики, кроме CPU, может использовать HPA?
Ответ
В autoscaling/v2 есть несколько типов метрик. Resource: CPU и память из metrics-server. Pods: своя метрика на под, например число запросов в секунду. Object: метрика объекта, например Ingress. External: внешняя, скажем длина очереди. Для трёх последних нужен адаптер метрик (например, для Prometheus), который отдаёт их через API метрик. Для очередей часто удобнее KEDA. Память для HPA ненадёжна: многие приложения не освобождают её после пика.
Что хотят услышать: autoscaling/v2, типы Resource, Pods, Object, External, нужен адаптер, оговорка про память.
Красный флаг: Думать, что HPA умеет только CPU.
13. [middle] Почему minReplicas: 1 для продакшена обычно плохая идея?
Ответ
При одной реплике перезапуск или потеря узла даёт простой (обновление можно пройти без простоя через maxUnavailable: 0, maxSurge: 1 и рабочую readiness), а HPA не поможет, потому что масштабирование требует времени. Ставлю минимум две реплики, разнесённые по узлам через topologySpreadConstraints или anti-affinity, и добавляю PodDisruptionBudget. Так плановые работы на узлах не останавливают сервис. Для малонагруженных внутренних сервисов один под допустим, если простой можно пережить.
Что хотят услышать: минимум 2 реплики, разные узлы, PDB, цена против доступности.
Красный флаг: Ставить 1 ради экономии для клиентского сервиса.
14. [junior] [на скорость] Как проверить, что HPA реально работает?
Ответ
Проверяю kubectl get hpa (TARGETS должны быть числами, не <unknown>) и kubectl describe hpa: там события и условия. Затем даю нагрузку, например командой hey или циклом запросов в отдельном поде, и слежу за kubectl get hpa -w и kubectl get pods -w. Должно быть: рост метрики, рост реплик, после спада нагрузки через время уменьшение. Если реплик не прибавилось, смотрю requests, metrics-server и maxReplicas.
Что хотят услышать: get hpa и describe, нагрузочный тест, наблюдение в watch, задержка при снижении.
Красный флаг: Проверять только по манифесту, без нагрузки.
Проверено на версиях
- Проверено командами:
helm lintиhelm template(Helm v4.3.0) на учебном чарте с шаблонамиhpa.yamlиdeployment.yamlиз урока, в обоих режимах (hpa.enabledвыключен и включён), вывод совпадает с приведённым; результатhelm templateпрошёлkubeconform -strict -summary(v0.8.0), без замечаний;shellcheckдляbreak.shбез замечаний. - Не прогонялось: кластер kind не запускался. Вывод
kubectl top,kubectl get hpa, событий HPA и метрики нагрузки взяты из предыдущей редакции урока, числа примерные. Скриптbreak.shпроверен толькоshellcheckи чтением, на кластере не запускался. - Kubernetes: 1.37.1, kubectl 1.37.1, kind: v0.33.0 (версии курса, не проверялись заново)
- metrics-server: v0.8.0 указан примером, актуальную версию проверь на странице проекта
- busybox (образ): 1.37.0
Итог урока: ты умеешь
- умею объяснить, почему HPA считает загрузку от
requestsи что такое миллиядро - умею поставить metrics-server в kind и проверить его через
kubectl top - умею объяснить, почему HPA без
requestsпоказывает<unknown> - умею посчитать по формуле, сколько реплик запросит HPA, с учётом min, max и допуска
- умею создать нагрузку через
/burnи наблюдать масштабирование - умею читать
kubectl describe hpa: события,Conditions, текущую и целевую метрику - умею добавить HPA в Helm-чарт под флаг
hpa.enabledи убратьreplicasпри включённом HPA - умею объяснить флаппинг и настроить
behavior.scaleDown - умею отличить HPA, VPA, KEDA и Cluster Autoscaler по задачам
Дальше: Урок 5.12: Безопасность кластера: RBAC, NetworkPolicy, PSS
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.