Механика CPU limits и устранение CPU throttling в Kubernetes
Настройка вычислительных ресурсов в Kubernetes остаётся одной из самых обманчивых задач при эксплуатации продакшен-кластеров. Инженеры часто исходят из интуитивной гипотезы: если узел загружен лишь на треть, а контейнеру выделили лимит в 1 CPU, приложение должно работать без задержек. Однако в реальной эксплуатации сервисы сталкиваются с неожиданным замедлением ответов, спайками хвостовой задержки (p99 latency) и падением пропускной способности. Причиной выступает не дефицит аппаратных ресурсов сервера, а встроенная механика квотирования CPU Completely Fair Scheduler (CFS) в ядре Linux.
Понимание того, как Kubernetes управляет процессором, критически важно для предотвращения скрытых аварий. В отличие от оперативной памяти, где превышение limits приводит к немедленному уничтожению контейнера сигналом OOMKilled, процессорные ресурсы распределяются во времени. Некорректная конфигурация процессорных лимитов приводит к так называемому CPU throttling — искусственному замораживанию выполнения потоков приложения планировщиком ядра.
Как работает квотирование CFS и математика периода
Планировщик Linux CFS управляет ограничениями cgroups через фиксированные временные интервалы. По умолчанию базовый период квотирования в Kubernetes составляет 100 миллисекунд (настраивается параметром ядра kernel.sched_cfs_period_us). Когда инвестор или DevOps-инженер указывает в манифесте Пода значение limits.cpu: 1 (или 1000m), ядро пересчитывает этот показатель в доступное процессорное время за один период. Формула расчёта квоты выглядит следующим образом:
[ \text{Quota} = \left( \frac{\text{CPU Limit}}{1000} \right) \times 100\text{ мс} ]
При значении limits.cpu: 1 контейнер получает 100 миллисекунд процессорного времени на каждые 100 миллисекунд реального времени. Если лимит ограничен показателем 200m (0.2 CPU), доступная квота составляет всего 20 миллисекунд за каждые 100 миллисекунд.
Ключевая проблема возникает в многопоточных приложениях, написанных на Java, Go, Node.js или Python. Потоки исполнения расходуют процессорную квоту параллельно. Если приложение запускает 8 рабочих потоков на 8-ядерном узле, и каждый поток выполняет вычисления в течение 5 миллисекунд реального времени, суммарное израсходованное процессорное время за этот отрезок составит:
[ 8 \text{ потоков} \times 5\text{ мс} = 40\text{ мс CPU time} ]
Если контейнер имел ограничение limits.cpu: 300m (квота 30 мс на 100 мс период), выделив 40 мс всего за 5 миллисекунд реального времени, он полностью исчерпает свою квоту. В результате планировщик Linux CFS мгновенно замораживает все потоки данного контейнера на оставшиеся 95 миллисекунд текущего периода. Приложение буквально «замирает», не обрабатывая входящие сетевые пакеты и не отвечая на проверки готовности (readiness probes), хотя сам сервер может простаивать.
Регрессия ядра Linux 5.4 и исторический контекст bug 512ac999
Проблема троттлинга долгое время усугублялась критической ошибкой в самом ядре Linux. До версии 5.4 в коде cgroup CFS существовал баг (зафиксированный в патче ядра commit 512ac999), связанный с некорректным сбросом неиспользованных локальных квантов времени на многоядерных системах.
В баг-трекере ядра выяснилось, что при наличии десятков процессорных ядер (например, на двухсокетных серверах с 88 vCPU) планировщик периодически ошибочно считал квоту исчерпанной и включал троттлинг даже для Подов, которые израсходовали лишь 20–30% от заданного CPU limit. Потоки принудительно списывались в ожидание из-за гонки при возврате неиспользованного времени ядра из локального буфера CPU в глобальный пул cgroup.
Обновление ядра Linux до версии 5.4+ (а также соответствующие бэкпорты в дистрибутивы RHEL, Ubuntu и Debian) полностью устранило эту проблему. Однако даже на современном ядре естественный троттлинг, вызванный всплесками параллельной нагрузки, остаётся штатным поведением CFS bandwidth control.
Диагностика через метрики Prometheus
Для выявления скрытого замедления нельзя опираться только на стандартный график container_cpu_usage_seconds_total. Среднее потребление процессора за 1 или 5 минут часто показывает низкую загрузку, маскируя регулярные 100-миллисекундные микро-заморозки.
В контроллере cgroups экспортируются две специализированные метрики, доступные в Prometheus через cAdvisor:
container_cpu_cfs_periods_total— общее количество отработанных 100-миллисекундных интервалов квотирования.container_cpu_cfs_throttled_periods_total— число интервалов, в которых контейнер подвергся принудительной остановке из-за превышения квоты.
Для расчёта относительного уровня троттлинга в процентах применяется PromQL-запрос:
sum(increase(container_cpu_cfs_throttled_periods_total[5m])) by (pod, container)
/
sum(increase(container_cpu_cfs_periods_total[5m])) by (pod, container) * 100
Если полученное значение превышает 1–5%, приложение испытывает систематические задержки execution. Значения выше 20% указывают на критическую деградацию производительности.
Практические стратегии оптимизации ресурсов
Для исключения CPU throttling в продакшене рекомендуется применять комбинированный инженерный подход:
- Обновление ядра системы: Убедитесь, что ноды Kubernetes работают под управлением ядром Linux 5.4 или более свежим, чтобы исключить исторические баги CFS.
- Использование целочисленных лимитов: Задавайте
limits.cpuкратным целому числу ядер (например,2000mили4000m), особенно для приложений с большим числом параллельных worker-потоков. - Отказ от CPU limits для чувствительных микросервисов: Многие высоконагруженные IT-компании (включая Zalando и Datadog) официально рекомендуют настраивать точный
requests.cpu, но оставлятьlimits.cpuне заданным (unset). В этом случае Поды свободно используют доступный простаивающий процессор узла без принудительной заморозки CFS. Безопасность узла при этом обеспечивается контролемrequestsи автомасштабированием HPA. - Настройка CPU Governor: Переключите регулятор частоты процессора на нодах из режима
powersaveвperformance, чтобы исключить задержки при переключении P-states ядер.
Правильная настройка квотирования процессора позволяет сохранить предсказуемое время отклика сервисов и исключить аномальные спайки задержек в Kubernetes.

