Конвертация CPU лимитов в cgroups v2: новая формула в Kubernetes и OCI
Переход современной инфраструктуры контейнеризации на вторую версию контрольных групп Linux (cgroups v2) стал важным этапом в повышении безопасности и стабильности операционных систем. Контрольные группы (cgroups) отвечают за ограничение, учет и изоляцию системных ресурсов (процессорного времени, оперативной памяти, дискового ввода-вывода) для групп процессов. В контейнерных средах вроде Kubernetes именно cgroups обеспечивают выполнение лимитов, указанных разработчиками в спецификациях подов.
Однако переход с cgroups v1 на v2 вскрыл серьезные архитектурные проблемы в распределении приоритетов процессора (CPU) для контейнеров. Для их устранения разработчик Итамар Холдер (Itamar Holder) из Red Hat разработал новую квадратичную формулу конвертации лимитов, которая была внедрена на уровне спецификаций OCI (Open Container Initiative) и поддержана в популярных средах выполнения контейнеров.
Как работало распределение CPU: cgroups v1 против cgroups v2
В первой версии cgroups v1 приоритет выделения процессорного времени регулировался параметром cpu.shares. Значение этого параметра вычислялось на основе запросов контейнера (CPU requests) по следующей формуле:
cpu.shares = (milliCPU * 1024) / 1000
Единица измерения milliCPU (или миллиядра) — это виртуальная метрика Kubernetes. Одно физическое ядро процессора эквивалентно 1000m. Значение 1024 в формуле представляет собой стандартный вес по умолчанию для системного процесса в Linux. Таким образом, контейнер, запрашивающий ровно 1 CPU (1000m), получал значение cpu.shares = 1024. Если контейнер запрашивал минимальные ресурсы, например 100m, ему присваивалось значение cpu.shares = 102. Общий диапазон допустимых значений в v1 составлял от 2 до 262 144.
Во второй версии cgroups v2 концепция «shares» была полностью переработана и заменена на CPU вес (cpu.weight). Диапазон значений изменился на [1, 10000], а стандартный вес по умолчанию для любого процесса стал равен 100.
Для обеспечения плавного перехода на cgroups v2 в рамках предложения по улучшению Kubernetes KEP-2254 была принята простая линейная формула конвертации старых значений shares в новые веса weight:
cpu.weight = 1 + ((cpu.shares - 2) * 9999) / 262142
Эта формула пропорционально проецировала старый диапазон [2, 262144] на новый [1, 10000].
Проблемы старой линейной формулы конвертации
Несмотря на простоту математических расчетов, линейная модель маппинга вызвала две критические проблемы в реальных инфраструктурах.
1. Занижение приоритета контейнеров перед системными процессами ОС
В cgroups v1 контейнер с запросом в 1 CPU получал приоритет 1024, что делало его равным по правам любым системным демонам операционной системы хоста. Однако при использовании старой формулы в cgroups v2 значение cpu.shares = 1024 конвертировалось в cpu.weight = 39.
Поскольку стандартный вес системных процессов хоста равен 100, контейнеры Kubernetes де-факто получали менее 40% от положенного им приоритета. Это приводило к опасным ситуациям: в моменты нехватки процессорных ресурсов (resource starvation) фоновые процессы операционной системы хоста легко вытесняли рабочие контейнеры Kubernetes с ядер процессора, вызывая деградацию работы бизнес-приложений.
2. Неприемлемая потеря гранулярности при малых лимитах
При малых лимитах линейная формула выдавала экстремально низкие значения весов. Так, контейнер с запросом 100m (что соответствует 102 shares в v1) получал в cgroups v2 значение cpu.weight = 4.
Такой низкий вес делал невозможным дальнейшее разделение ресурсов внутри самого контейнера. В Kubernetes развивается поддержка вложенных контрольных групп (sub-cgroups) для разделения ресурсов между процессами внутри одного пода (например, KEP #5474 для непривилегированных контейнеров). Распределить приоритеты между внутренними процессами контейнера, имея на входе общую емкость всего в 4 единицы, технически невозможно из-за отсутствия необходимой точности (гранулярности).
Новое решение: нелинейная квадратичная формула
Чтобы решить эти проблемы, разработчики отказались от линейной модели в пользу более сложного квадратичного уравнения. Новая формула имеет следующий вид:
cpu.weight = ceil(10 ^ (L^2 / 612 + 125 * L / 612 - 7 / 34))
где L = log2(cpu.shares).
Главная идея новой формулы — построить нелинейную кривую, которая обязательно проходит через три ключевые «опорные точки», связывающие логику cgroups v1 и v2:
- Минимум: Точка
(2, 1)— минимальные значения shares и weight; - Стандарт хоста: Точка
(1024, 100)— стандартный вес по умолчанию (1 CPU = 100 weight); - Максимум: Точка
(262144, 10000)— пиковые лимиты производительности.
Благодаря квадратичной зависимости, в диапазоне малых и средних значений лимитов кривая идет выше линейной траектории, обеспечивая правильный маппинг приоритетов.
Как это решает проблемы на практике:
- Восстановление приоритета: Контейнер с запросом в 1 CPU (1000m) теперь получает
cpu.weight = 102. Это значение практически равно стандартному системному весу хоста (100), что возвращает паритет между контейнерами и процессами операционной системы. - Повышение точности: Контейнер с запросом 100m получает значение
cpu.weight = 17(вместо 4). Это дает инженерам достаточную свободу для тонкой настройки приоритетов суб-cgroups внутри контейнера.
Сфера применения: OCI-слой (runc и crun)
Важно понимать, что данная математика конвертации реализована не в самом Kubernetes. Расчет весов происходит на более низком уровне — в рантаймах контейнеров OCI (Open Container Initiative), которые непосредственно взаимодействуют с ядром Linux.
Поэтому внедрение новой формулы полностью зависит от обновления версий используемых OCI-совместимых сред выполнения:
- В низкоуровневом рантайме runc новая формула включена по умолчанию начиная с версии 1.3.2.
- В рантайме crun поддержка новой формулы появилась в версии 1.23.
Влияние на инфраструктуру и потенциальные риски
Перед обновлением OCI-рантаймов на серверах системным администраторам необходимо учесть несколько важных нюансов:
- Сломы систем мониторинга: Инструменты мониторинга, дашборды и кастомные скрипты управления ресурсами, которые программно вычисляли ожидаемый вес CPU на основе старой линейной формулы KEP-2254, могут выдать ошибки или ложные алерты. Их необходимо обновить с учетом квадратичного алгоритма.
- Потеря точности при обратной конвертации: Восстановить исходное значение
milliCPUиз текущегоcpu.weightбез потери данных невозможно. Во-первых, при переводе milliCPU в shares происходит округление (100m дает 102 shares). Во-вторых, новая формула сопоставляет диапазоны по принципу «многие к одному» — например, значения milliCPU от 90m до 109m будут одинаково преобразованы вcpu.weight = 17. Системы сбора метрик должны читать исходные лимиты из спецификаций подов (pod spec), а не пытаться вычислить их на основе показателей cgroups в ОС.
Команда Kubernetes рекомендует провести обязательное тестирование новой формулы в тестовых (non-production) средах перед обновлением OCI-рантаймов на продуктовых кластерах.

