АйТи Фреш
Главная / Статьи / Linux, Docker и DevOps
Linux, Docker и DevOps

После sidecar HPA перестал масштабировать по CPU: где нужен ContainerResource

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~19 мин чтения
Sidecar-контейнер искажает расчёт CPU utilization в HPA Kubernetes, и новые реплики не создаются
HPA делит сумму на сумму — и лёгкий sidecar прячет перегруженное приложение.

Если после добавления sidecar HPA перестал добавлять реплики, дело в формуле: Utilization считается как сумма потребления всех контейнеров пода к сумме их requests. Sidecar без request выключает метрику, а «лёгкий» sidecar занижает её. Лечится метрикой ContainerResource по основному контейнеру — ниже арифметика, кейс, манифест и грабли.

Почему HPA перестал масштабировать после добавления sidecar

Сценарий, который я видел уже раз пять, и каждый раз он выглядит одинаково — чаще всего его приносят клиенты, которым мы помогаем с внедрением Kubernetes и open-source. В пятницу в Deployment добавили sidecar — прокси авторизации, sidecar сервис-меша, агент трейсинга, лог-шиппер, неважно. В понедельник приходит нагрузка, приложение задыхается, p95 уезжает в потолок, а количество реплик стоит колом. Дежурный смотрит kubectl get hpa, видит там либо <unknown>/75%, либо вполне приличные 62%/75% — и уходит копать в базу, в сеть, в ingress. Теряет день. А сломалось ровно в момент добавления sidecar.

Отказов тут на самом деле два, и они разные. Первый — жёсткий: sidecar приехал вообще без resources.requests.cpu. Тогда утилизация пода не определена в принципе, и документация про это говорит прямым текстом: «if some of the Pod's containers do not have the relevant resource request set, CPU utilization for the Pod will not be defined and the autoscaler will not take any action for that metric». В kubectl describe hpa вы увидите условие ScalingActive=False с причиной вида FailedGetResourceMetric и текстом про missing request for cpu. HPA просто выключается по этой метрике. Хорошая новость — это заметно.

Второй отказ — тихий, и он опаснее. Sidecar приехал с корректным request, всё формально валидно, HPA живой, проценты показывает. Только эти проценты больше не про ваше приложение. Sidecar добавил свой request в знаменатель, а потребляет он при этом копейки — и общая цифра поехала вниз ровно тогда, когда основной контейнер начал упираться. Ничего не падает, алертов нет, просто автомасштабирование стало отвечать с опозданием или не отвечать вовсе. Такое живёт в проде месяцами.

Диагностика занимает две минуты и делается вот так:

kubectl get hpa donate-api -n prod -o wide
kubectl describe hpa donate-api -n prod | sed -n '/Conditions/,$p'
# главное — разложить потребление по контейнерам, а не по подам
kubectl top pod -n prod -l app=donate-api --containers
# и сверить с тем, что реально запрошено
kubectl get deploy donate-api -n prod -o jsonpath=\
'{range .spec.template.spec.containers[*]}{.name}{"\t"}{.resources.requests.cpu}{"\n"}{end}'
`<unknown>` — это отказ, который видно. Честные 62 % при основном контейнере на 94 % — тоже отказ, но его никто не замечает, пока не приедет распродажа. Второй случай встречается чаще.
Памятка: Почему HPA перестал масштабировать после добавления sidecar — схема
Памятка: Почему HPA перестал масштабировать после добавления sidecar. Открыть схему в полном размере

Как HPA считает CPU Utilization для пода из нескольких контейнеров

Метрика типа Resource с target.type: Utilization считается не так, как думает большинство. Во-первых, знаменатель — это **requests**, а не limits. Во-вторых, при нескольких контейнерах в поде это не «максимум по контейнерам» и не «средняя по контейнерам», а отношение суммы к сумме: суммарное потребление всех контейнеров пода делится на суммарный request всех контейнеров пода. Дальше HPA усредняет это по всем готовым подам и подставляет в формулу desiredReplicas = ceil(currentReplicas × currentMetricValue / desiredMetricValue), а если отношение достаточно близко к единице — в пределах допуска, по умолчанию 10 % — не делает вообще ничего.

Разберём на числах, потому что так нагляднее. Был один контейнер app: request 500m, под нагрузкой ест 450m. Утилизация 90 %, порог 75 % — HPA бодро добавляет реплики. Приехал sidecar auth-proxy с request 200m, который при той же нагрузке ест 40m. Считаем: (450 + 40) / (500 + 200) = 490/700 = 70 %. Приложение как упиралось в свой request, так и упирается, а HPA видит 70 % при пороге 75 % и спокойно спит. Мы не поменяли ни строчки в коде приложения и ни цифры в его requests — но автомасштабирование выключилось.

Эффект тем сильнее, чем «легче» sidecar относительно своего request. И он двусторонний: тот же sidecar, который занижает метрику под нагрузкой, будет завышать её на простое, если у него есть фоновая активность — ротация логов, периодический скрейп, переподключение к control plane меша. Я видел стенд, где ночью на нулевом трафике HPA держал на две реплики больше положенного просто потому, что fluent-bit раз в минуту дожимал буфер.

И третья мелочь, про которую забывают: допуск в 10 % работает поверх уже искажённой цифры. То есть даже если вы аккуратно подкрутите порог с 75 % на 68 %, чтобы «скомпенсировать sidecar», компенсация будет верной ровно для одного профиля нагрузки. Изменилось соотношение работы app и sidecar — расчёт снова врёт. Подкручивание порога здесь не решение, а отсрочка. Кстати, если хочется поменять requests без пересоздания подов, в свежих версиях для этого есть изменение ресурсов работающего Pod — но на арифметику HPA это никак не влияет: знаменатель просто станет другим.

Знаменатель — requests всех контейнеров пода. Любой контейнер, добавленный в под, меняет метрику HPA, даже если приложение не трогали.
После sidecar HPA перестал масштабировать по CPU: где нужен ContainerResource — схема
Схема к статье. Открыть схему в полном размере
Расчёт CPU utilization HPA: sidecar снижает процент пода с 90 до 70 при той же нагрузке на приложение
Приложение загружено одинаково, но HPA видит разные цифры — всё решает знаменатель.

Кейс: платёжный сервис и 6 реплик при трафике ×3,5

Условный клиент — краудфандинговая платформа «Народный проект», 41 рабочее место в офисе, прод в managed-кластере Kubernetes 1.36, три воркера по 8 vCPU / 32 ГБ. Ключевой сервис — donate-api, через который проходят платежи жертвователей: Deployment на 6 реплик, HPA с minReplicas: 3, maxReplicas: 20, метрика Resource cpu, averageUtilization: 80. Контейнер app — request 1000m, limit 2000m. Так он прожил больше года без претензий.

В пятницу разработчики выкатили два sidecar: auth-proxy (request 300m, limit 500m) и fluent-bit для отгрузки логов — его выкатили вообще без секции resources. Хронология дальше. Выходные: HPA показывает <unknown>/80%, никто не смотрит. Понедельник, финал крупного сбора и рассылка по всей базе, трафик вырос в 3,5 раза: p95 на странице оплаты улетает со 180 мс до 2,4 с, реплик по-прежнему 6 при потолке 20. Меня подключили примерно через час.

Первое, что я сделал, — kubectl top pod --containers. Картина: app ест 940–960m при request 1000m, auth-proxy — 55–70m при request 300m, fluent-bit — 20–30m при отсутствующем request. describe hpa подтвердил: ScalingActive=False, причина FailedGetResourceMetric, в тексте — missing request for cpu в контейнере fluent-bit. Прописали fluent-bit request 100m — HPA ожил через полторы минуты и показал… 71–74 % при пороге 80 %. Жёсткий отказ починили и сразу въехали в тихий: (950 + 60 + 25) / (1000 + 300 + 100) = 1035/1400 ≈ 74 %. Приложение на 95 % от своего request, а HPA считает, что всё в порядке.

Крутить порог я не стал, а перевёл HPA на ContainerResource по контейнеру app с целью 75 %. В следующий пик того же дня: 6 реплик → 14 за четыре минуты (упёрлись в policy scale-up, а не в потолок), p95 вернулся к 210 мс. Плюс побочный эффект, которого не ждали: пропали лишние ночные реплики. Оказалось, HPA месяцами держал ночью 4–5 реплик вместо трёх, потому что fluent-bit на ротации логов подтягивал общую метрику. За следующий месяц счёт за вычислительные ресурсы снизился примерно на 11 % — не главный результат, но директору он понравился больше, чем починенный p95.

Честно про спорный момент: minReplicas мы после этого подняли с 3 до 4. Формально ContainerResource решил задачу и без этого, но реакция HPA всё равно измеряется десятками секунд (об этом ниже), и запас в одну реплику на старте пика дешевле, чем минута деградации на странице оплаты. Это компромисс, а не «правильная настройка».

Обратите внимание на последовательность: пока `fluent-bit` был без request, тихий отказ был не виден — его закрывал жёсткий. Починили один — вылез второй. Если чините HPA по чек-листу, не останавливайтесь на том, что цифры «появились».
Цифры и версии: Кейс: платёжный сервис и 6 реплик при трафике ×3,5 — схема
Цифры и версии: Кейс: платёжный сервис и 6 реплик при трафике ×3,5. Открыть схему в полном размере
До и после перехода HPA на ContainerResource: 6 против 14 реплик в пик, p95 2,4 с против 210 мс
Правильная метрика одновременно чинит пик и убирает лишние реплики ночью.

ContainerResource: рабочий манифест и проверка

Тип метрики ContainerResource делает ровно то, чего не хватает: позволяет назвать конкретный контейнер и считать утилизацию только по нему — его потребление к его request. Sidecar в расчёт не попадает вообще. В документации это описано прямым текстом: «if you have a web application and a sidecar container that provides logging, you can scale based on the resource use of the web application, ignoring the sidecar container and its resource use».

Манифест целиком — вот он, autoscaling/v2:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: donate-api
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: donate-api
  minReplicas: 4
  maxReplicas: 20
  metrics:
    - type: ContainerResource
      containerResource:
        name: cpu
        container: app          # ИМЯ КОНТЕЙНЕРА, а не Deployment
        target:
          type: Utilization
          averageUtilization: 75
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60

Про версии и доступность. Тип ContainerResource появился как alpha в Kubernetes 1.20 под feature gate HPAContainerMetrics, в 1.27 стал beta и включённым по умолчанию, а в 1.30 перешёл в stable; сам feature gate после этого убрали из кода (в 1.32 его уже нет). То есть в любой поддерживаемой сейчас ветке — 1.35, 1.36 и вышедшей 26 августа 2026 года 1.37 — он работает из коробки, никаких флагов включать не нужно. Если у вас managed-кластер на старой версии ниже 1.27, начните с обновления, а не с HPA.

Проверка занимает один apply — сначала в dev-namespace, потом в проде:

kubectl apply -f hpa-containerresource.yaml
sleep 60
kubectl describe hpa donate-api -n dev | grep -A3 Metrics
# ожидаемая строка:
#   resource cpu of container "app" on pods (as a percentage of request): 74% (740m) / 75%
kubectl get hpa donate-api -n dev -o jsonpath='{.status.conditions}' | jq

Если имя контейнера указано с ошибкой или у него нет request, вы это увидите сразу: HPA повиснет с ScalingActive=False и внятной причиной в conditions.

И ещё: метрики в spec.metrics можно комбинировать. HPA считает рекомендацию по каждой и берёт максимальную. Так что нормальная схема — ContainerResource по app как основная плюс, если очень хочется, ContainerResource по sidecar со своим порогом. Только не смешивайте Resource и ContainerResource по одному и тому же ресурсу: получите два расчёта, один из которых заведомо искажён sidecar, и когда он окажется выше, именно он и будет решать, сколько реплик держать.

Прежде чем катить в прод, проверьте, что строка метрики в describe hpa показывает именно контейнер app и реальный процент, а не <unknown>.

Грабли после перехода на ContainerResource

**Переименование контейнера убивает HPA молча.** Это самая неприятная из всех. Имя контейнера в containerResource.container — обычная строка, никакой связи с Deployment у неё нет. Переименовали app в api при рефакторинге чарта — метрика исчезла, HPA замер на текущем количестве реплик. Документация даёт правильный порядок действий: сначала обновите HPA так, чтобы он отслеживал и старое, и новое имя (две метрики в spec.metrics), потом катите Deployment с новым именем, и только после этого убирайте старую метрику из HPA. Тот же порядок работает и когда контейнер временно убирают из пода.

**Требование request никуда не делось — оно просто сузилось.** ContainerResource не отменяет необходимость resources.requests.cpu, он лишь перестаёт требовать его от всех контейнеров сразу. У целевого контейнера request обязан быть, иначе получите ровно ту же ошибку про missing request. Зато sidecar теперь можно жить без CPU request — хотя я бы всё равно проставил, из-за QoS-класса и вытеснения.

**Sidecar больше не под наблюдением HPA — вообще.** Мы сознательно выкинули его из расчёта, а значит, если он сам упрётся в свой limit и начнёт троттлиться, автомасштабирование этого не увидит. У auth-proxy в примере выше limit был 500m — на утроенном трафике он подходил к 380m, и это стоило отдельного алерта в мониторинге (container_cpu_cfs_throttled_seconds_total). Не заведёте такой алерт — рано или поздно получите латентность, происхождение которой будете искать неделю.

**Нативные sidecar-контейнеры устроены иначе.** С Kubernetes 1.33 нативные sidecar (init-контейнеры с restartPolicy: Always) стабильны, и живут они не в spec.containers, а в spec.initContainers. Правила расчёта эффективного request пода для init-контейнеров отдельные, а что именно попадёт в расчёт HPA — зависит от версии кластера. Универсальный ответ давать не буду: проверяйте на своём кластере kubectl top pod --containers и kubectl describe hpa. Если по условиям HPA видно, что нативный sidecar учитывается в метрике пода, — работает то же правило суммы, и ContainerResource нужен. Если нет — половина проблемы решилась сама.

**Скорость реакции.** Тут любят завышать ожидания. HPA-контроллер пересчитывает раз в 15 секунд, а metrics-server по умолчанию скрейпит kubelet раз в 60 секунд (--metric-resolution, снижать ниже 15 секунд разработчики не рекомендуют — это разрешение самих метрик kubelet). Плюс усреднение, плюс допуск, плюс время старта пода. Реальная задержка от «нагрузка приехала» до «новый под принял трафик» — полторы-три минуты. Если вам нужно быстрее, ContainerResource не поможет: нужен запас по minReplicas, прогретые ноды или проактивное масштабирование по внешней метрике вроде длины очереди. Про базовые вещи вроде этой я подробнее писал в заметках о том, как Kubernetes ведёт себя в продакшене в первый год.

Порядок при переименовании контейнера: HPA (обе метрики) → Deployment → HPA (убрать старую). В обратном порядке вы получите окно, в котором автомасштабирование мертво, а вы об этом не знаете.
Дерево решений для HPA Kubernetes: unknown, заниженный процент, переименованный контейнер, metrics-server
Каждый симптом HPA ведёт к своему действию — порог крутить не нужно ни в одной ветке.

Когда ContainerResource не нужен и что делать вместо

Не надо переводить на ContainerResource всё подряд. Если sidecar реально масштабируется вместе с приложением — например, это прокси, через который идёт весь трафик, и его потребление пропорционально нагрузке, — обычный Resource считает правильно, и трогать ничего не нужно. Проблема возникает именно там, где потребление sidecar живёт своей жизнью относительно нагрузки на приложение. Простой тест: постройте два графика — CPU app и CPU sidecar за неделю. Если они коррелируют, забейте.

Второй вариант обхода — метрика с target.type: AverageValue вместо Utilization. Она сравнивает абсолютное потребление на под с заданным значением и вообще не требует requests. Это законный способ, но вы теряете относительность: поменяли размер пода — надо руками пересчитывать целевое значение. Я использую его только там, где requests по каким-то причинам не проставить.

Отдельно скажу про память, потому что вопрос задают всегда. Масштабирование по памяти в большинстве случаев — ловушка, и ContainerResource её не чинит. У JVM, Go, .NET и Node потребление памяти определяется поведением сборщика мусора, а не текущей нагрузкой: вверх метрика едет охотно, вниз — почти никогда. Получите кластер, который вырос под пик и больше не сжался. По памяти имеет смысл масштабировать только приложения с честной корреляцией «запрос → выделенная память», и таких мало.

Если же вам важна не столько метрика CPU, сколько предсказуемость, смотрите в сторону масштабирования по прикладным показателям — глубина очереди, RPS, время в очереди, количество активных соединений. Через custom/external metrics или KEDA это делается штатно и почти всегда честнее, чем любой CPU-based автоскейл. CPU — это прокси-метрика, и она врёт тем сильнее, чем сложнее устроен под.

И банальность, которую всё равно приходится повторять: уберите spec.replicas из манифеста Deployment, если им управляет HPA. Иначе GitOps-инструмент будет каждый цикл синхронизации возвращать реплики к значению из репозитория, а вы будете думать, что это HPA сходит с ума. Такие же «невидимые» изменения поведения после обновлений встречаются и в RBAC — например, когда после 1.35 отвалились exec и port-forward: формально ничего не сломано, а работа встала.

CPU — прокси-метрика. Если есть честный прикладной показатель вроде длины очереди, масштабироваться по нему почти всегда надёжнее.
Порядок действий: Когда ContainerResource не нужен и что делать вместо — схема
Порядок действий: Когда ContainerResource не нужен и что делать вместо. Открыть схему в полном размере

Чек-лист: что делать в первую очередь

Порядок, по которому я разбираю такие инциденты. Он выстроен по принципу «сначала то, что даёт ответ за минуту, потом то, что требует выкатки».

Первые четыре пункта — диагностика, она бесплатная и делается прямо в проде. Пятый и шестой — правки, их катите в dev-namespace и смотрите на describe hpa в течение хотя бы двух минут: раньше метрика просто не успеет обновиться, и вы решите, что не работает. Седьмой и восьмой — гигиена, без которой вы вернётесь к этой же статье через полгода.

На что можно забить, если времени в обрез: на тонкую настройку behavior, на подбор идеального averageUtilization (75 % — нормальная отправная точка для CPU) и на масштабирование по памяти. На что забивать нельзя ни при каких обстоятельствах: на алерт по условию ScalingActive и на порядок действий при переименовании контейнера.

Если после всех правок HPA всё ещё показывает `<unknown>` — проверьте metrics-server: `kubectl get apiservice v1beta1.metrics.k8s.io`. Без него не работает ни `Resource`, ни `ContainerResource`, и симптом будет ровно тот же.

Частые вопросы

HPA пишет «missing request for cpu» — это точно из-за sidecar?

Причина всегда одна: у какого-то контейнера пода нет resources.requests.cpu. Имя контейнера указано в блоке Conditions вывода kubectl describe hpa. Sidecar — самый частый виновник, потому что его добавляют вебхуком или патчем чарта без resources.

Можно ли просто снизить порог averageUtilization и не трогать тип метрики?

Можно, но это отсрочка. Компенсация порогом верна для одного соотношения нагрузки app и sidecar: поменялся трафик или обновился sidecar — расчёт снова врёт. Если sidecar не масштабируется вместе с приложением, нужен ContainerResource.

ContainerResource доступен в моей версии Kubernetes?

Да, если у вас Kubernetes 1.27 и новее: с 1.27 тип включён по умолчанию (beta), с 1.30 — stable, feature gate HPAContainerMetrics удалён. В ветках 1.35–1.37 ничего включать не нужно.

Что будет, если контейнер, указанный в containerResource.container, исчезнет или его переименуют?

HPA перестанет получать метрику и замрёт на текущем числе реплик. Порядок: добавить в HPA метрику и по старому, и по новому имени, выкатить Deployment, затем убрать старую метрику. И заведите алерт на ScalingActive=False.

Насколько быстро HPA среагирует после перехода на ContainerResource?

Не станет быстрее: контроллер HPA пересчитывает раз в 15 секунд, metrics-server по умолчанию собирает данные раз в 60 секунд, плюс допуск и старт пода. Реально — полторы-три минуты; для быстрой реакции нужен запас по minReplicas.

Нужно ли теперь вообще ставить requests sidecar-контейнерам?

Для ContainerResource — не нужно, он их игнорирует. Для планировщика, QoS-класса и порядка вытеснения — нужно. Ставьте скромные, но осмысленные значения.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи