Разработчик запускает приложение, которое запрашивает 10 гигабайт памяти. На машине физически 8 гигабайт. malloc() возвращает указатель, приложение продолжает работу. Никакой ошибки. Никакого предупреждения. Всё выглядит нормально ровно до того момента, когда приложение начинает реально писать в эту память - и тогда ядро оказывается перед неудобным выбором.
Это не баг и не случайность. Это намеренное архитектурное решение, которое называется overcommit - выдача виртуальной памяти без гарантии наличия физического хранилища под неё. Linux делает это по умолчанию, и большинство систем работают именно в таком режиме. Понять, почему это разумно, что происходит при исчерпании памяти и как управлять этим поведением - значит перестать удивляться тому, почему сервер с 64 гигабайтами ОЗУ иногда убивает процессы.
Виртуальная память и почему malloc не делает того что кажется
Каждый процесс в Linux работает в изолированном виртуальном адресном пространстве. Вызов malloc() просит ядро зарезервировать участок в этом пространстве, и ядро отвечает согласием - но не выделяет физическую память в этот момент. Создаётся запись в таблице страниц, описывающая диапазон виртуальных адресов, которые формально принадлежат процессу. Физические страницы RAM остаются свободными.
Реальное выделение физической памяти происходит позже, при первом обращении к странице. Процессор генерирует исключение страничного отказа (page fault), ядро перехватывает его, находит свободную физическую страницу, отображает её в виртуальное адресное пространство процесса и возвращает управление. Для процесса это незаметно - операция просто занимает чуть больше времени, чем обычный доступ к памяти.
Эта схема - Copy-on-Write и ленивое выделение страниц - существует не из-за overcommit. Она фундаментальна для эффективной работы fork(): когда процесс создаёт дочерний через fork(), обе копии разделяют одни и те же физические страницы только для чтения. Страница копируется в физической памяти только когда кто-то из них пишет в неё. Без этого механизма fork() тысяч воркеров в Apache или Nginx съедал бы ОЗУ мгновенно.
Overcommit - это следствие и расширение этой же логики. Если страницы выделяются лениво, почему бы не разрешить суммарный объём зарезервированных виртуальных страниц превышать физическую память? Статистически большинство процессов никогда не используют всю запрошенную память. Хорошо известно, что многие приложения резервируют память заранее из соображений производительности, используя лишь малую её часть.
Три режима overcommit и их реальное поведение
Ядро предоставляет три режима управления overcommit через параметр vm.overcommit_memory. Выбор режима определяет, как ядро отвечает на запросы выделения памяти:
# Посмотреть текущий режим
cat /proc/sys/vm/overcommit_memory
# Изменить режим немедленно
sysctl -w vm.overcommit_memory=2
# Зафиксировать постоянно
echo "vm.overcommit_memory=2" >> /etc/sysctl.conf
Режим 0 - эвристический, включён по умолчанию. Ядро разрешает большинство запросов памяти, отказывая только в явно неразумных - например, когда процесс запрашивает объём больше суммы физической памяти и swap. Для 99% рабочих нагрузок этот режим оптимален.
Режим 1 - безусловное разрешение. Ядро удовлетворяет любой запрос malloc() без проверок. Используется в научных вычислениях с разреженными матрицами: программа резервирует гигантский массив, заведомо зная, что большинство элементов никогда не будет записано. Для production-серверов этот режим опасен: при исчерпании памяти система оказывается в неопределённом состоянии.
Режим 2 - строгий учёт. Ядро ведёт счётчик commit charge - суммарный объём виртуальной памяти, потенциально требующей физического хранилища. Новые выделения разрешаются только если commit charge не превысит лимит:
# Лимит в режиме 2 = swap + (RAM * overcommit_ratio / 100)
cat /proc/sys/vm/overcommit_ratio # обычно 50 по умолчанию
cat /proc/sys/vm/overcommit_kbytes # альтернатива ratio, в КБ
# Просмотреть текущий commit charge
cat /proc/meminfo | grep -E "Committed_AS|CommitLimit"
Режим 2 гарантирует, что при исчерпании лимита malloc() вернёт NULL вместо неожиданного убийства процесса позднее. Это предпочтительный режим для баз данных, PostgreSQL официально рекомендует именно его. Минус - часть памяти остаётся неиспользованной, потому что приложения резервируют больше, чем реально тратят.
Как ядро выбирает жертву при нехватке памяти
Когда физическая память и swap исчерпаны, а новая страница нужна прямо сейчас, ядро запускает OOM killer. Это механизм последнего выбора: ядро должно пожертвовать одним процессом ради выживания системы в целом. Альтернатива - паника ядра, что значительно хуже.
OOM killer работает по балльной системе. Каждый процесс имеет оценку oom_score от 0 до 1000. Чем выше балл - тем вероятнее процесс будет убит. Базовый алгоритм начисления баллов ориентируется на объём памяти, занятой процессом и его дочерними процессами: процесс, занимающий 90% ОЗУ, получает около 900 баллов.
# Посмотреть текущий oom_score процесса
cat /proc/$$/oom_score
# Топ процессов по oom_score
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
score=$(cat /proc/$pid/oom_score 2>/dev/null)
comm=$(cat /proc/$pid/comm 2>/dev/null)
echo "$score $pid $comm"
done | sort -rn | head -10
# Проверить события OOM в кольцевом буфере ядра
dmesg | grep -i "oom\|out of memory\|killed process"
Ядро выведет в dmesg подробный отчёт об OOM-событии: имя убитого процесса, его PID, объём занятой памяти, состояние swap и список других процессов с их oom_score в момент принятия решения. Это ценный источник при расследовании неожиданных завершений процессов на сервере.
Управление oom_score_adj и защита критических процессов
Базовый oom_score можно скорректировать через oom_score_adj. Диапазон значений от -1000 до +1000. Значение -1000 полностью защищает процесс от OOM killer - ядро никогда не выберет его жертвой. Значение +1000 гарантирует, что при любой нехватке памяти этот процесс будет убит первым.
# Защитить критический процесс от OOM killer
echo -1000 > /proc/$(pgrep postgres)/oom_score_adj
# Сделать процесс первым кандидатом на убийство
echo 1000 > /proc/$$/oom_score_adj
# Посмотреть текущее значение adj
cat /proc/$$/oom_score_adj
Для системных служб правильнее устанавливать OOMScoreAdjust в юните systemd, чтобы значение применялось автоматически при каждом запуске:
# /etc/systemd/system/myservice.service
[Service]
OOMScoreAdjust=-500
Здесь важно понимать границы защиты. Если защитить от OOM killer все значимые процессы, ядро всё равно найдёт жертву среди оставшихся. Если нет незащищённых процессов с достаточным объёмом памяти, система может убить что-то совсем неожиданное или, в крайнем случае, перейти к панике ядра. Правильная стратегия - защищать действительно критичное, а остальное оставлять в распоряжении OOM killer.
Swap как буфер между виртуальной и физической памятью
Swap участвует в системе overcommit не только как дополнительное хранилище, но и как часть расчёта commit limit в режиме 2. При подкачке ядро вытесняет редко используемые страницы физической памяти на диск, освобождая ОЗУ для активных процессов. Это расширяет возможности overcommit, но создаёт latency при обращении к вытесненным данным.
# Текущее использование swap
free -h
swapon --show
# Настройка агрессивности использования swap
cat /proc/sys/vm/swappiness
# Снизить использование swap (предпочитать вытеснение кэша файлов)
sysctl -w vm.swappiness=10
# Полное состояние памяти включая commit charge
cat /proc/meminfo
Параметр swappiness от 0 до 200 управляет предпочтением ядра: при значении 0 ядро максимально старается не использовать swap, вытесняя файловый кэш вместо анонимных страниц процессов. При значении 100 анонимные страницы и файловый кэш вытесняются с равным приоритетом. Для серверов с базами данных, которые сами управляют своим кэшем (PostgreSQL, Redis), рекомендуется swappiness=10 или ниже - база лучше знает, что ей нужно держать в памяти, чем общий механизм вытеснения ядра.
Когда отключать overcommit и когда оставлять его включённым
Выбор политики overcommit - это инженерное решение с реальными последствиями, а не настройка ради настройки. Несколько конкретных сценариев помогают принять осознанное решение.
Режим 0 по умолчанию подходит для большинства серверов общего назначения, десктопов и контейнерных окружений. Ядро само справляется с балансировкой, OOM killer вмешивается только в действительно критических ситуациях. Менять что-то без конкретной причины не стоит.
Режим 2 оправдан для серверов баз данных, где неожиданное убийство процесса катастрофично. PostgreSQL, Oracle, некоторые конфигурации MySQL работают стабильнее в режиме строгого учёта: они получают ошибку выделения памяти предсказуемо, обрабатывают её штатно и не рискуют быть убитыми посредине транзакции. Типичные значения для production PostgreSQL:
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80
Значение overcommit_ratio=80 оставляет 20% физической памяти за пределами commit limit - запас для ядра и нераспределённого использования.
Режим 1 почти никогда не нужен в production. Его единственная законная область - специализированные вычислительные задачи с контролируемым использованием разреженной памяти, где разработчики точно знают, что делают.
Overcommit - это компромисс, встроенный в архитектуру Linux. Он позволяет системе работать эффективно в типичных условиях ценой непредсказуемого поведения при нетипичных. Понимание механизма, мониторинг commit charge, осознанная настройка oom_score_adj для критических процессов и правильный выбор режима для конкретной нагрузки превращают это непредсказуемое поведение в управляемое.