Cgroups и лимиты памяти: как контейнер получает OOM
Контейнер может получить OOM, хотя на хосте ещё остаётся свободная память: ядро учитывает лимит его cgroup, а не только общий запас RAM. Разбираем путь от учёта памяти до убийства процесса и показываем, что смотреть при расследовании.

Свободная память на хосте не спасает контейнер от OOM
Контейнер ограничен 2 ГБ памяти и падает с OOM, хотя на сервере свободно ещё несколько гигабайт. Противоречия нет: cgroups учитывают память отдельно для группы процессов, и достижение её лимита может запустить OOM-killer внутри этой группы. Чтобы найти причину, нужно смотреть не только на RAM хоста, но и на события и счётчики конкретной cgroup.
Ниже — разбор для Linux с cgroups v2. Контейнерные платформы и версии ядра влияют на детали: сначала проверьте, какая иерархия cgroups включена на вашем узле. Интерфейс cgroups v1 устроен иначе, поэтому названия файлов и трактовка лимитов в этой статье к нему не относятся.
Что происходит, когда процесс выделяет память
В cgroups память учитывается на уровне группы процессов и её потомков. Когда процесс контейнера получает страницы памяти, ядро учитывает их в соответствующей cgroup. В счётчик входят не только анонимные страницы процесса: учитываются и некоторые виды файлового кеша и ядровой памяти. Поэтому сумма RSS процессов не обязана совпадать со значением, которое показывает cgroup.
В cgroups v2 ключевые настройки и счётчики лежат в файловой системе cgroup. memory.max задаёт жёсткий предел учёта памяти. Если потребление приближается к нему, ядро пытается освободить память, например за счёт reclaim — возврата страниц, которые можно освободить без завершения процесса.1 Если освободить достаточно не удаётся, в этой cgroup может начаться OOM-kill: ядро выбирает процесс и завершает его, чтобы уложиться в лимит.
OOM в контейнере — не обязательно признак нехватки RAM на сервере. Это может быть нехватка памяти именно в его cgroup.
Важны три разных результата. Процесс может получить ошибку выделения памяти; ядро может убить один процесс в cgroup; либо, если включена соответствующая настройка, группа может быть уничтожена целиком. Какой вариант вы увидите, зависит от обстоятельств, настроек cgroup и среды запуска. Не следует считать любой выход процесса с кодом 137 доказательством OOM: этот код обычно означает завершение сигналом SIGKILL, а причину нужно подтверждать по событиям и журналам.
Какие лимиты и счётчики читать
В cgroups v2 есть не только жёсткий предел. memory.high задаёт уровень, после которого ядро сильнее ограничивает потребление через reclaim; это механизм давления на процессы, а не эквивалент немедленного убийства. memory.max — предел, при достижении которого освобождение памяти может закончиться OOM-kill. memory.events помогает увидеть, происходило ли превышение порогов и запускался ли OOM.
| Файл | Что показывает | Как использовать при разборе |
|---|---|---|
memory.current | Текущий учёт памяти cgroup | Сравнить с лимитом и проверить динамику |
memory.max | Жёсткий лимит; значение max означает отсутствие такого предела на этом уровне | Узнать, мог ли сработать лимит группы |
memory.high | Порог давления на память | Искать задержки reclaim и замедление приложения |
memory.events | Счётчики событий, включая high, max, oom и oom_kill | Проверить, росли ли счётчики во время инцидента |
Счётчики в memory.events накопительные: само по себе ненулевое значение не доказывает, что событие произошло именно во время текущего падения. Снимите значения до нагрузки и после неё или сравните с предыдущими наблюдениями. Также проверьте родительские cgroup: дочерняя группа может не достигнуть собственного memory.max, но упереться в лимит выше по дереву.
Как найти cgroup процесса
На узле с cgroups v2 путь процесса к cgroup можно посмотреть в /proc. Нужны права чтения файлов процесса; путь ниже предполагает, что иерархия cgroups смонтирована в стандартное место /sys/fs/cgroup.
PID=1234
cat /proc/$PID/cgroup
В выводе cgroups v2 обычно есть строка с форматом 0::/путь. Сопоставьте путь после двойного двоеточия с каталогом под /sys/fs/cgroup, затем прочитайте нужные файлы. Например, если путь процесса — /system.slice/example.service, каталогом будет /sys/fs/cgroup/system.slice/example.service.
CGROUP=/sys/fs/cgroup/system.slice/example.service
cat "$CGROUP/memory.current"
cat "$CGROUP/memory.max"
cat "$CGROUP/memory.high"
cat "$CGROUP/memory.events"
PID здесь замените на PID процесса на хосте, а путь cgroup — на тот, который увидели в его записи. В контейнере PID и пути могут отличаться от видимых на хосте. Если файл не найден, сначала проверьте версию cgroups, точки монтирования и то, что процесс ещё существует.
Как отделить OOM cgroup от нехватки памяти на узле
У cgroup OOM и глобального OOM разные масштабы. В первом случае ядро пытается справиться с нехваткой внутри ограниченной группы; хост при этом может сохранять свободную память. При глобальном OOM проблема касается системы в целом. Не делайте вывод по одной строке в логе: сопоставьте время падения с memory.events, журналом ядра и статусом контейнера.
- Если увеличился
oom_killв нужной cgroup, это сильный признак, что ядро убило там процесс из-за OOM. Уточните, какой именно процесс завершился. - Если растёт
high, ноoom_killне меняется, группа могла испытывать давление и замедляться без убийства процесса. - Если растёт
max, группа сталкивалась с пределомmemory.max; это не всегда означает, что процесс уже убит. - Если счётчики cgroup не изменились, проверьте журналы узла, родительские лимиты, рестарты оркестратора и действия внешнего менеджера.
В Kubernetes статус контейнера OOMKilled помогает обнаружить завершение по OOM, но сам по себе не объясняет, какой именно лимит сработал и сколько памяти использовало приложение. Сверьте событие с лимитами Pod и контейнера, метриками узла и счётчиками cgroup, пока данные ещё доступны. После перезапуска часть диагностического контекста можно потерять.
Где диагностика даёт сбой
Смотрят только на RSS процесса
RSS полезен, но это не полный учёт cgroup. Файловый кеш и другие учитываемые ядром расходы могут изменить картину. Для ответа на вопрос «упёрся ли контейнер в лимит» сначала смотрите на memory.current, затем разбирайте состав потребления.
Принимают код 137 за доказательство OOM
Процесс с кодом 137 мог получить SIGKILL по другой причине — например, его остановил администратор или менеджер контейнеров. Ищите подтверждение в событиях cgroup и системных журналах; не стройте диагноз только на коде завершения.
Сравнивают с лимитом только одной группы
Лимит может задаваться родителем, а настройки контейнера не всегда отражают всю цепочку ограничений. Проверяйте cgroup процесса и её предков. Также учитывайте резерв памяти для узла: повышение лимита контейнера не добавляет физическую RAM и может увеличить риск глобального OOM.
Путают swap с RAM
Для cgroups v2 учёт swap связан с отдельными настройками и файлами. Возможность использовать swap зависит от конфигурации ядра и среды запуска. Не выводите её наличие только из значения memory.max: проверьте фактические настройки и документацию вашей версии платформы.
Что делать дальше. Настройте сбор дельты memory.events вместе с memory.current, лимитами контейнера и метриками узла. Воспроизведите типичную нагрузку на тестовом окружении и зафиксируйте, при каком объёме памяти растут high и max, а также когда появляется oom_kill. Это даст основу для решения: оптимизировать расход, изменить лимит или оставить запас памяти хосту.
1 Reclaim — механизм ядра, который пытается освободить используемую память, например вытесняя страницы кеша или возвращая их в доступный пул, прежде чем прибегнуть к убийству процесса.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Control Group v2 — документация ядра Linux, Resource constraints — Docker documentation
Частые вопросы
Почему контейнер получил OOM, если на хосте была свободная память?
Код завершения 137 означает OOM?
Что смотреть в cgroups v2 в первую очередь?
Если `memory.events` показывает `oom_kill`, процесс убит именно из-за лимита контейнера?
Чем отличается `memory.high` от `memory.max`?
Почему показания RSS не совпадают с `memory.current`?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.