Kubernetes 1.37 Memory QoS: почему cgroups v2 не защищают SaaS от давления памяти автоматически

23 сентября 2026 г.

После обновления кластера до Kubernetes 1.37 можно увидеть включённый флаг функции MemoryQoS и сделать опасный вывод: теперь kubelet автоматически защищает критичные сервисы от давления памяти.

Это не так. В стандартной конфигурации Kubernetes 1.37 kubelet не записывает для контейнеров memory.high, memory.min или memory.low. Обычный лимит памяти по-прежнему превращается в memory.max, а новые механизмы начинают влиять на работу узла только после явной настройки memoryThrottlingFactor или memoryReservationPolicy: TieredReservation.

И даже после включения Memory QoS не устраняет отказы из-за памяти. Он меняет их форму:

  • вместо быстрого OOM приложение может надолго перейти к прямому освобождению памяти (direct reclaim) и потерять P99;

  • защита Guaranteed Pod через memory.min может сделать его файловый кеш недоступным для вытеснения;

  • защита критичного сервиса может уменьшить эластичность соседних нагрузок;

  • снижение числа OOMKilled не обязательно означает улучшение SLO.

Поэтому Memory QoS — не безопасный переключатель, а политика распределения дефицитного ресурса. Её нужно оценивать на уровне пула узлов, профиля нагрузки и пользовательских задержек.

Что действительно меняется в Kubernetes 1.37

Kubernetes 1.37 был выпущен 26 августа 2026 года, а в сентябре проект объявил перевод Memory QoS в Beta. Флаг функции MemoryQoS в kubelet 1.37 включён по умолчанию — это подтверждают анонс выпуска и отдельная публикация о Memory QoS.

Но доступность функции и активная политика — разные вещи. Значения по умолчанию таковы:

  • memoryThrottlingFactor: null;

  • memoryReservationPolicy: None.

При такой конфигурации апгрейд сам по себе не включает ограничение через memory.high и не создаёт защиту через memory.min или memory.low. Поведение контейнеров относительно памяти в основном остаётся прежним.

Есть важное исключение. Если memoryThrottlingFactor был задан явно до обновления, это значение сохраняется, а ограничение через memory.high продолжает работать. Следовательно, два внешне одинаковых кластера после обновления до 1.37 могут вести себя по-разному из-за истории их конфигурации.

Откат версии также не гарантирует мгновенного возвращения к прежнему состоянию. Уже записанное контейнеру значение memory.high может сохраняться до перезапуска, изменения ресурсов или применения конфигурации ресурсов средой выполнения. Поэтому проверять нужно не только декларативную конфигурацию kubelet, но и реальные cgroup-файлы работающих контейнеров. Подробности этого поведения описаны в KEP-2570.

Базовая модель: четыре разных механизма cgroup v2

Путаница вокруг Memory QoS часто начинается с попытки трактовать все параметры cgroup как разновидности лимита. На практике они решают разные задачи.

Параметр Назначение Что происходит под давлением памяти
memory.max Жёсткий верхний предел Если reclaim не освобождает достаточно памяти, cgroup получает OOM
memory.high Порог ограничения Процессы переходят в интенсивный direct reclaim и замедляются, но пересечение порога само по себе не вызывает OOM
memory.min Жёсткая защита используемой памяти Память в пределах эффективного минимума нельзя вытеснить ради других cgroup
memory.low Мягкая защита используемой памяти Ядро старается сначала вытеснять память из менее защищённых cgroup

Подробная семантика этих параметров описана в документации контроллера памяти cgroup v2.

Лимит остаётся жёстким лимитом

Обычный Kubernetes memory limit отображается в memory.max. Memory QoS его не заменяет. Если контейнер достигает memory.max, а ядро не может освободить достаточно памяти, запускается OOM killer внутри cgroup.

Это принципиально отличает лимит от memory.high. Первый определяет, сколько памяти контейнеру разрешено удерживать. Второй заставляет контейнер платить задержкой за дальнейшие выделения памяти ещё до достижения жёсткого предела.

Requests становятся защитой только после включения TieredReservation

Без соответствующей политики memory request прежде всего участвует в планировании и вытеснении Pod. Он не означает, что ядро Linux зарезервировало для контейнера неотчуждаемую область RAM.

При memoryReservationPolicy: TieredReservation kubelet начинает отображать requests на механизмы cgroup v2:

  • Guaranteed получает memory.min;

  • Burstable получает memory.low;

  • BestEffort не получает защиты.

Kubelet также должен настроить предковые QoS-cgroup, иначе защита дочернего контейнера не будет эффективно работать во всей иерархии. Сопоставление классов QoS с параметрами памяти описано в документации Kubernetes.

memoryThrottlingFactor: пространство между нормальной работой и OOM

После явной настройки memoryThrottlingFactor kubelet устанавливает memory.high для контейнеров классов Burstable и BestEffort. Для класса Guaranteed этот механизм не применяется.

Для Burstable порог рассчитывается так:

memory.high = request + factor × (limit − request)

Результат округляется до размера страницы памяти. Если лимита нет, вместо него в расчёте используется доступная для размещения Pod память узла.

Например, при request 256 MiB, limit 512 MiB и коэффициенте 0,9 порог будет около 486 MiB. Контейнер сможет двигаться к memory.max, но после пересечения memory.high новые аллокации станут существенно дороже.

Пересечение memory.high — это не OOM

Ядро не завершает процесс сразу после пересечения memory.high. Оно переводит процессы cgroup в режим интенсивного прямого освобождения памяти: поток, запросивший память, сам участвует в её освобождении. Это снижает скорость роста потребления и может дать оператору или внешнему контроллеру время:

  • уменьшить входящую нагрузку;

  • увеличить лимит;

  • управляемо завершить задачу;

  • перенести работу на другой узел.

Для пакетной или повторяемой задачи это может быть лучше немедленного OOM. Для API, чувствительного к задержкам, результат может оказаться хуже: процесс остаётся живым и формально готовым, но запросы начинают зависать на выделении памяти.

Пробы готовности и живости не обязаны заметить такую деградацию. Они могут оставаться успешными, пока пользовательский P99 уже вышел за пределы SLO.

Что показывает синтетический тест

В эксперименте, приведённом в репозитории, Pod класса Burstable с запросом памяти 256 MiB, лимитом 512 MiB и коэффициентом 0,9 получил memory.high около 486 MiB. До OOMKilled примерно через 71 секунду cgroup накопил 3 596 событий high.

Измеряемая операция занимала около 0,5 мс до давления памяти, а затем выросла:

  • до 27 мс при 490 MiB;

  • до 119 мс при 495 MiB;

  • до 448 мс при 500 MiB.

Этот тест полезен как демонстрация механизма: memory.high действительно способен заменить немедленный отказ на период резкой деградации. Но из него нельзя выводить ожидаемую потерю производительности для JVM, Go-сервиса, базы данных или файловой нагрузки. Публичных данных, позволяющих переносить такие числа на типичный SaaS, нет.

Требование к ядру — не формальность

Ранее перевод Memory QoS в Beta для Kubernetes 1.28 отменили из-за риска livelock при использовании memory.high на ядрах Linux ниже 5.9. В более новых ядрах исправление должно обеспечивать продвижение процессов даже под давлением памяти.

Kubelet 1.36 и новее предупреждает о слишком старом ядре, но предупреждение не заменяет инвентаризацию образов узлов. Особенно это важно в управляемом Kubernetes, где версия Kubernetes, образ узла, ядро, среда выполнения контейнеров и доступность конфигурации kubelet могут обновляться независимо.

TieredReservation: реальная защита с ценой для эластичности

memory.min и memory.low не резервируют физическую память заранее. Если cgroup использует 200 MiB при memory.min 2 GiB, оставшиеся 1,8 GiB не вычитаются из свободной RAM.

Защита распространяется только на фактически используемую память. Поэтому включение TieredReservation не обязательно немедленно снижает плотность размещения: планировщик и до этого учитывал requests, а не полную физическую предаллокацию.

Проблема возникает позже, когда защищённый рабочий набор или файловый кеш уже занят. Используемая память в пределах эффективного memory.min не может быть вытеснена ради соседей. При memory.low защита мягкая: ядро может нарушить её при отсутствии лучшего варианта. При memory.min защита жёсткая.

Почему Pod класса Guaranteed может неожиданно удерживать кеш

В cgroup учитывается не только анонимная память процесса, но и файловый кеш. Для Pod класса Guaranteed запросы памяти равны лимитам. При TieredReservation это приводит к сочетанию:

memory.min = memory.max

Следовательно, память, уже учтённая в этом cgroup, включая файловый кеш, получает жёсткую защиту вплоть до лимита. Если приложению потребуется место под новые анонимные аллокации, вытеснение собственного кеша может быть ограничено защитой, а итогом станет cgroup OOM.

Это особенно существенно для сервисов с интенсивным чтением файлов, tmpfs, общими файлами или большими кешами. Фактический эффект зависит от того, как ядро учитывает страницы, кто первым обращается к файлам и как устроен ввод-вывод. Поэтому оценивать риск только по RSS недостаточно: нужны как минимум поля anon и file из memory.stat.

Конфликт с плотным смешанным размещением

Некоторые команды назначают Pod класс Guaranteed не потому, что приложению нужна неотчуждаемая память, а ради предсказуемой семантики вытеснения: запрос памяти приравнивают к лимиту, оставляя реальное потребление существенно ниже.

На смешанном узле TieredReservation меняет смысл такой настройки. Неиспользованный запрос памяти по-прежнему не занимает RAM, но накопленный неиспользуемый файловый кеш Pod класса Guaranteed становится жёстко защищённым. Соседние эластичные задачи теряют возможность использовать эту память при всплеске.

Это известное архитектурное ограничение: TieredReservation является политикой всего узла. Нельзя включить memory.min только для нескольких выбранных Pod. В принятой SIG Node issue зафиксирован конфликт между жёсткой защитой Guaranteed и плотным смешанным размещением; среди направлений развития обсуждаются мягкая защита и будущая настройка на уровне Pod.

На текущем этапе отдельный пул узлов остаётся более управляемой границей для нагрузок, которым действительно нужна жёсткая защита.

Что происходит при нехватке памяти

Memory QoS не создаёт единого последовательного контура отказа. На узле одновременно действуют несколько механизмов.

1. Освобождение памяти и ограничение через cgroup

Ядро пытается освободить память. memory.low влияет на порядок освобождения памяти, memory.min запрещает вытеснение защищённой части, а memory.high заставляет процессы конкретной cgroup выполнять прямое освобождение памяти.

2. Eviction со стороны kubelet

При состоянии MemoryPressure kubelet может вытеснять Pod. Memory QoS не отменяет этот механизм. При ранжировании учитываются:

  1. превышает ли фактическое потребление request;

  2. Priority Pod;

  3. величина превышения над request.

Подробный порядок описан в документации о вытеснении при давлении на узел.

Это означает, что request одновременно влияет на планирование, защиту при TieredReservation и вероятность вытеснения. Ошибка в request перестаёт быть только вопросом плотности размещения.

3. OOM со стороны ядра

Ядро может запустить OOM killer до того, как kubelet успеет отреагировать. Выбор процесса зависит в том числе от oom_score_adj, связанного с классом QoS. Возможны как OOM внутри cgroup при достижении memory.max, так и отказ на уровне узла.

Поэтому отсутствие eviction не доказывает, что политика работает правильно, а отсутствие OOM не доказывает отсутствие пользовательского воздействия. Приложение может оставаться живым, но проводить значительную часть времени в reclaim.

Какие данные нужны для диагностики

Наблюдать только kubectl top и число OOMKilled недостаточно. Для каждого тестируемого контейнера нужны данные как минимум из четырёх групп.

Состояние cgroup

  • memory.current;

  • memory.high;

  • memory.min;

  • memory.low;

  • memory.max.

Они показывают не предполагаемую политику, а значения, действительно применённые ядром.

События контроллера памяти

В memory.events следует наблюдать:

  • high;

  • low;

  • max;

  • oom;

  • oom_kill.

Рост high без OOM может быть не успехом, а признаком скрытого ограничения приложения.

Состав памяти и давление

Нужны:

  • memory.stat, особенно соотношение file и anon;

  • PSI для памяти;

  • показатели ввода-вывода, если нагрузка активно работает с файлами;

  • метрики защищённой памяти со стороны kubelet, если они доступны в конкретной поставке.

Метрики приложения и Kubernetes

На том же временном графике должны находиться:

  • P50, P95 и P99 пользовательских операций;

  • пропускная способность и RPS;

  • доля ошибок и тайм-аутов;

  • container_oom_events_total;

  • OOMKilled и перезапуски;

  • eviction;

  • состояние MemoryPressure;

  • результаты проб готовности и живости.

Иначе команда рискует объявить успехом уменьшение OOM, которое было куплено длительными задержками.

Как проверить политику до внедрения

Испытание имеет смысл проводить на отдельном пуле узлов с тем же ядром, средой выполнения и образом ОС, которые планируются для рабочей среды. Полезно сравнить четыре режима:

  1. исходная конфигурация без активных политик Memory QoS;

  2. только memoryThrottlingFactor;

  3. только TieredReservation;

  4. обе политики одновременно.

Для каждого режима нужна одна и та же воспроизводимая нагрузка. Синтетическое выделение памяти проверяет, что значения записываются в cgroup и события действительно возникают. Решение о внедрении следует принимать по воспроизведению реального профиля сервиса: запросов, фоновых задач, кеша, работы с хранилищем и характерных всплесков.

Минимальный сценарий испытания должен включать:

  • устойчивую нормальную нагрузку;

  • постепенный рост потребления памяти;

  • кратковременный всплеск выше request;

  • давление со стороны соседнего контейнера;

  • профиль с большим анонимным рабочим набором;

  • профиль с заметным файловым кешем;

  • достижение memory.high и memory.max;

  • проверку реакции kubelet на MemoryPressure.

Критерии успеха и остановки нужно определить до теста. Их нельзя сводить к «стало меньше OOM». Политика приемлема, только если задержки, пропускная способность, ошибки, eviction и время восстановления остаются в пределах SLO команды.

Практическая стратегия внедрения

Перед включением политики стоит пройти последовательность из четырёх этапов.

1. Провести инвентаризацию

Для каждого пула узлов проверить:

  • действительно ли используется cgroup v2;

  • версию ядра и соответствие требованию 5.9 или новее;

  • образ узла и среду выполнения контейнеров;

  • фактически загруженную конфигурацию kubelet;

  • текущие значения cgroup у работающих контейнеров;

  • наличие ранее заданного memoryThrottlingFactor;

  • возможность управлять этими параметрами у поставщика Kubernetes.

Наличие Kubernetes 1.37 само по себе не отвечает ни на один из этих вопросов.

2. Разделить нагрузки по требуемой форме отказа

memory.high разумен там, где замедление предпочтительнее немедленного завершения: например, для повторяемых фоновых задач или нагрузки, которую внешний контроллер способен ограничить.

Для API с жёстким SLO нужно отдельно решить, что хуже: быстрый отказ с перезапуском или продолжительный рост P99. Универсального ответа нет.

TieredReservation лучше соответствует выделенному пулу с хорошо измеренными запросами памяти и критичными сервисами класса Guaranteed. На узле со смешанными API, пакетными задачами и файловыми кешами единая жёсткая политика заметно рискованнее.

3. Вводить изменения по одному

Если одновременно включить ограничение и резервирование, при деградации будет трудно отделить эффект прямого освобождения памяти от нехватки вытесняемой памяти. Сначала полезно проверить каждую политику отдельно, затем их сочетание.

Откат должен учитывать, что удаление параметра из конфигурации не обязательно немедленно очистит memory.high у уже запущенных контейнеров. План возврата должен включать проверку cgroup и контролируемое обновление затронутых Pod.

4. Строить защиту как систему, а не как один параметр

Опыт Meta с cgroup v2 показывает ценность мягкой защиты memory.low, но также её границы. В модели fbtax2 защита критичной нагрузки сочеталась с PSI, oomd, swap и управлением вводом-выводом. Даже при таком дизайне сохранялись временные просадки RPS и эффекты неполной изоляции. Описание результатов доступно в документации fbtax2.

Этот пример не является готовой политикой для Kubernetes, но показывает правильный уровень мышления: cgroup задаёт приоритеты при дефиците, а не устраняет сам дефицит. Для эластичного SaaS мягкая защита, наблюдение за PSI и управляемая реакция на давление могут быть полезнее, чем жёсткий memory.min для всех Guaranteed Pod.

Практическая позиция для Kubernetes 1.37 проста: не считать Memory QoS результатом обновления. Сначала проверить реальные cgroup-настройки. Затем выбрать желаемую форму отказа для каждого класса нагрузки. И только после измерения задержек, освобождения памяти, файлового кеша, вытеснений и OOM решать, стоит ли включать политику на всём пуле узлов.

— мы обсудим проект, оценим сроки и предложим формат.
Нужна помощь с проектом?

Разберёмся вместе —
подскажем, как решить
задачу быстро и эффективно

Kubernetes 1.37 Memory QoS: почему cgroups v2 не защищают SaaS от давления памяти автоматически