После ночного отчёта контейнер с PostgreSQL перезапустился со статусом OOMKilled. На физическом узле при этом оставалась доступная RAM. Команда готовит закупку более крупных серверов, хотя новый узел с тем же лимитом контейнера воспроизведёт ту же аварию. Похожая запись в истории способна появиться и при настоящем дефиците памяти всего узла. Есть и третье событие: kubelet заранее выселяет Pod, чтобы не довести сервер до полного истощения. Эти случаи выглядят как «базу завершили из-за памяти», но происходят на разных уровнях и требуют разных решений. Pod — единица запуска Kubernetes, в которой находятся один или несколько контейнеров. На Linux их ресурсы ограничиваются через cgroup — группу процессов с общим учётом и правилами памяти. Поэтому сначала нужно понять, чья именно граница была достигнута: контейнера, Pod или физического узла.
Один перезапуск может означать три разных события
Первый случай — OOM внутри cgroup контейнера. Процессы достигли собственного memory limit, ядро не смогло освободить достаточно памяти в этой группе и завершило одного из них. За пределами группы на узле ещё может оставаться большой запас. Второй случай — node OOM, то есть нехватка физической памяти всему узлу. Ядру нужно освободить память для продолжения работы, и оно выбирает процесс-жертву среди доступных кандидатов. Здесь проблема действительно затрагивает общий сервер и соседние Pod. Третий случай — node-pressure eviction. Kubelet, агент Kubernetes на узле, следит за доступной памятью и может заранее завершить Pod при достижении порога. Это управляемая попытка вернуть ресурс до неконтролируемого OOM, а не другое название OOMKilled.
Даже код выхода 137 не закрывает вопрос. Обычно он говорит, что процесс завершён сигналом SIGKILL — принудительным завершением без возможности обработать сигнал. Такой сигнал может быть следствием OOM, действий среды или оператора, поэтому доказательством служат reason контейнера, события Kubernetes, сообщения ядра и учёт cgroup в одном временном интервале.
Лимит контейнера образует отдельную стену
Request и limit отвечают на разные вопросы. Memory request — заявленная потребность, по которой планировщик решает, на какой узел можно поставить Pod; она также влияет на приоритет при нехватке ресурса. Memory limit — верхняя граница, которую среда исполнения передаёт ядру для ограничения контейнера. Контейнеру разрешено использовать больше request, если ресурс доступен, но memory limit остаётся пределом. В cgroup v2 его задаёт жёсткая граница memory.max. При приближении к ней ядро пытается освободить память внутри группы, а если это не удаётся, возникает локальная ситуация OOM и процесс может быть завершён.
Именно поэтому свободная RAM хоста не спасает контейнер с тесным лимитом. Для ядра это разные бюджеты: общий сервер ещё способен обслуживать работу, но конкретной группе запрещено расти дальше. Kubernetes не обязан автоматически расширять её до всего свободного объёма узла. События cgroup помогают отделить этот случай. Memory.events учитывает достижение максимальной границы, ситуации OOM и фактические убийства процессов в группе. Счётчик oom_kill подтверждает такое завершение, но его всё равно связывают с состоянием контейнера и временем инцидента, а не читают как вечный диагноз.
При node OOM заканчивается память всего сервера
Node OOM возникает, когда ядро не может удовлетворить потребность в памяти на уровне узла после доступных попыток освобождения. Тогда оно выбирает жертву, чтобы остальные процессы получили шанс продолжить работу. Завершённый контейнер в этом случае может не быть источником всего дефицита. На выбор влияет oom_score_adj — поправка к оценке кандидата на принудительное завершение. Kubelet задаёт её контейнерам с учётом класса качества обслуживания Pod, или QoS. Класс отражает соотношение requests и limits, но не даёт абсолютной неуязвимости при физическом истощении.
Для СУБД node OOM опаснее одного перезапуска. Может завершиться база, служебный процесс узла или соседний компонент, от которого зависит хранение и сеть. Даже если Pod восстановится на том же сервере, повторный прогрев и возврат нагрузки способны снова привести систему к границе. Узлу нужна память не только для контейнеров. Операционная система, kubelet, среда исполнения и системные службы тоже потребляют ресурс. Kubernetes использует понятие allocatable — часть мощности узла, доступную для Pod после необходимых резервов; смешивать её с полной физической ёмкостью нельзя.
Kubelet может выселить Pod до OOM
Node-pressure eviction — управляемое выселение при давлении на ресурс узла. Kubelet наблюдает сигнал memory.available, то есть доступную для работы память по правилам Kubernetes. Когда значение пересекает заданный порог и освобождение ресурсов не помогает, kubelet выбирает Pod для завершения. При таком событии Pod получает фазу Failed и может быть создан заново контроллером на этом или другом узле. Это не означает, что контейнер обязательно получил reason OOMKilled. Причиной завершения является решение kubelet вернуть ресурс и предотвратить голодание сервера.
Разница важна для бизнеса. При cgroup OOM исправляют границу или профиль памяти конкретной нагрузки. При node OOM восстанавливают запас всего сервера и проверяют соседей. При eviction нужно понять, почему узел дошёл до порога и соответствует ли размещение заявленным requests и системным резервам. Механическое увеличение limit способно лишь перенести аварию. База перестанет упираться в свою cgroup, но получит право забирать больше общей RAM. Если соседи рассчитаны на прежний предел, следующий инцидент может стать node pressure или node OOM.
Память СУБД шире одного очевидного буфера
PostgreSQL использует shared_buffers для блоков таблиц и индексов, но этим расход не заканчивается. Отдельные процессы выполняют запросы, сортировки и обслуживание, а Linux удерживает файловые страницы. Одновременный отчёт, построение индекса или восстановление способны поднять общий расход контейнера. Redis хранит данные в памяти, но размер полезных значений не равен всей памяти процесса. Служебные структуры, клиентские и репликационные буферы, фоновые операции и особенности распределителя создают дополнительный объём. Поэтому лимит, рассчитанный только по размеру набора данных, может закончиться раньше ожиданий.
Cgroup учитывает не только очевидную анонимную память процессов. В её расход могут входить файловые страницы, которыми пользуется группа. А memory-backed emptyDir — временный том в RAM — учитывается как память контейнера или Pod и способен приблизить нагрузку к limit, хотя приложение считает его «файлом». Для руководителя важен не каталог счётчиков, а причинная связь. Если пик связан с отчётом или фоновой работой, исправлением может быть запрос, расписание или обоснованный лимит. Если растёт утечка, дополнительная память лишь отложит повторение. Если давит весь узел, локальная настройка базы не вернёт системный резерв.
Исправление выбирают по уровню ограничения
Покупка RAM оправдана, когда воспроизводимый спрос действительно исчерпывает физическую ёмкость узла при корректных requests, limits и системных резервах. Она не лечит контейнер, которому оставили прежний memory limit. И наоборот, увеличение limit требует убедиться, что новая граница помещается рядом с соседними нагрузками. Прозрачный страничный уровень класса gigaRAM не изменяет cgroup limit, request или решение kubelet об eviction. Его применимость обсуждают после того, как исключены утечка, тесная cgroup и общее давление узла, а у нагрузки подтверждены менее активные страницы. Это отдельная задача, а не средство от OOMKilled.
У критичной PostgreSQL или Redis должны быть понятны два договора: сколько памяти планировщик резервирует при размещении и сколько среда разрешает использовать в пике. Между ними может быть пространство для роста, но оно не должно превращать соседей и системные службы в случайный резерв. Правильный вывод начинается не со слова OOM, а с уровня события. Локальный OOM требует решения внутри границы контейнера, node OOM — восстановления физического запаса, eviction — управления размещением и давлением узла. Только после этого увеличение RAM, изменение лимита или работа с приложением становятся осмысленным расходом.
Источники. Kubernetes v1.35 — Resource Management разделяет requests, limits и учёт emptyDir.
Node-pressure Eviction описывает eviction и node OOM.
Reserve Compute Resources объясняет allocatable.
Linux 6.12 — cgroup v2 определяет memory.max, memory.events и oom_kill.
Linux signal(7) используется для SIGKILL.
PostgreSQL 17 подтверждает роль shared_buffers и кэша ОС.
Redis Memory Optimization разделяет данные и расход процесса.
Страница продукта — только для позиционирования решения.
