Как Linux обещает память, которой у него нет, и что бывает, когда её попросят
Вы смотрите free -h, видите гигабайты свободной памяти, и через минуту получаете «Killed process» в dmesg. Или наоборот — приложение вызывает malloc() на объём, который заведомо больше физической RAM сервера, и вызов молча успевает. Это не баг и не мистика: так работает overcommit памяти в Linux — ядро сознательно выдаёт обещания, которые не всегда способно сдержать. Разберём, зачем это сделано именно так, что происходит, когда обещание приходится исполнять, и как отличить безопасную ситуацию от той, что закончится убитым процессом.
Содержание
- Что значит «выделить память» на самом деле
- vm.overcommit_memory: три режима и кто кому верит
- Почему это фича, а не баг: fork() и разреженные аллокации
- Когда виртуальные обещания встречаются с реальностью: OOM killer
- free и доступная память: что цифры реально означают
- Как оценить реальный риск нехватки памяти на своём сервере
Что значит «выделить память» на самом деле
Когда программа вызывает malloc(), mmap() или получает память под стек нового потока, происходит не то, что интуитивно кажется. Ядро не идёт немедленно искать физические страницы RAM и не закрепляет их за процессом. Вместо этого оно резервирует диапазон адресов в виртуальном адресном пространстве процесса и создаёт запись в таблице отображений — то есть выдаёт обещание: «эти адреса твои, обращайся к ним когда угодно».
Физическая страница памяти встаёт за этим адресом только в момент первого реального обращения — записи или чтения. Это называется отложенным выделением (demand paging): при обращении к ещё не подкреплённому адресу процессор генерирует page fault, ядро ловит его, находит свободную физическую страницу, отображает её на нужный виртуальный адрес и возвращает управление процессу — тот даже не замечает, что произошло прерывание.
Из этого следует ключевой факт: между «память выделена» (виртуальный адрес зарезервирован) и «память используется» (физическая страница реально занята) может быть огромная разница. Программы почти всегда резервируют больше, чем реально трогают:
- каждый поток в Linux по умолчанию получает под стек 8 МБ виртуального адресного пространства — но подавляющее большинство потоков использует от него несколько десятков или сотен килобайт;
- языки с управляемой памятью (JVM, Go, различные рантаймы) часто резервируют большой непрерывный кусок адресного пространства под кучу заранее, чтобы не фрагментировать его позже, и заполняют физическими страницами по мере роста;
- разреженные структуры данных, буферы «на вырост», mmap-отображения файлов — везде резервируется с запасом.
Если бы ядро требовало сразу подкреплять физической RAM каждый зарезервированный байт, огромная доля этих резервов простаивала бы без дела, а RAM, которой реально не хватает многим сервисам, тратилась бы на неиспользуемый запас. Overcommit — это способ ядра не резервировать физическую память заранее под то, что, скорее всего, никогда не будет использовано целиком.
vm.overcommit_memory: три режима и кто кому верит
Поведение ядра при выделении памяти управляется параметром vm.overcommit_memory, у которого три значения:
cat /proc/sys/vm/overcommit_memory
Режим 0 — эвристический (значение по умолчанию в большинстве дистрибутивов). Ядро разрешает «разумный» overcommit: явно абсурдные запросы (например, malloc() на объём, кратно превышающий любые разумные пределы системы) отклоняются, но большинство обычных запросов проходят без проверки реального наличия памяти. Эвристика непрозрачная и завязана на внутреннюю логику ядра — полагаться на неё как на гарантию не стоит.
Режим 1 — всегда разрешать overcommit. Ядро не проверяет вообще ничего и говорит «да» на любой запрос на выделение виртуальной памяти, сколько бы ни попросили. Это самый предсказуемый в плане успеха malloc() режим, но и самый рискованный: реальная нехватка физической памяти всплывёт позже, в момент обращения к странице, а не в момент выделения.
Режим 2 — строгий учёт (commit accounting). Ядро ведёт бухгалтерию: суммарный объём памяти, который можно раздать всем процессам, ограничен формулой swap + RAM * overcommit_ratio / 100 (по умолчанию overcommit_ratio около 50, но это настраиваемое значение, не измеренная величина — проверяйте своё). Как только сумма всех выделений (Committed_AS) подходит к этому пределу (CommitLimit), новые вызовы malloc()/mmap() начинают завершаться ошибкой ENOMEM — то есть отказ происходит сразу, на этапе выделения, а не позже на этапе обращения к странице.
Посмотреть текущие цифры:
grep -E 'MemAvailable|Committed_AS|CommitLimit' /proc/meminfo
sysctl vm.overcommit_ratio
Изменить режим временно (до перезагрузки):
sudo sysctl -w vm.overcommit_memory=2
sudo sysctl -w vm.overcommit_ratio=80
Постоянно — через /etc/sysctl.d/99-overcommit.conf с теми же ключами и sudo sysctl --system.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему это фича, а не баг: fork() и разреженные аллокации
Самый наглядный довод в пользу overcommit — системный вызов fork(). Когда процесс форкается, ядро должно создать копию его адресного пространства для потомка. Делать это физическим копированием всех страниц было бы дорого и почти всегда бессмысленно, поэтому применяется copy-on-write (COW): обе копии — родитель и потомок — первое время делят одни и те же физические страницы, помеченные «только для чтения», и физическая копия страницы создаётся только в момент, когда одна из сторон в неё пишет.
Проблема в том, что с точки зрения учёта виртуальной памяти после fork() оба процесса теперь «владеют» полным объёмом адресного пространства родителя. Процесс, занимающий половину RAM сервера, после форка формально запрашивает ещё одну такую же половину — хотя физически, скорее всего, скопируется лишь малая часть страниц, прежде чем потомок завершится (например, чтобы сделать exec() в дочернем shell) или закончит свою узкую задачу (как это делает fork() перед BGSAVE в Redis).
Без overcommit такой fork() на процессе, который уже занимает существенную долю памяти, попросту не смог бы выполниться — ядру пришлось бы отказать из-за формальной нехватки резерва, даже если реальный физический спрос вырастет на доли процента. Именно поэтому базы данных и демоны, которые полагаются на форк для снапшотов (Redis с BGSAVE, PostgreSQL с некоторыми механизмами), обычно требуют vm.overcommit_memory=1 — иначе форк может отказать в самый неподходящий момент.
Когда виртуальные обещания встречаются с реальностью: OOM killer
Overcommit откладывает момент истины, но не отменяет его. Если суммарный реальный спрос на физические страницы (не виртуальные резервы, а именно страницы, к которым процессы фактически обращаются) превышает то, что способны предоставить RAM и swap вместе, ядро оказывается в ситуации, когда очередной page fault обслужить нечем.
Порядок действий ядра в этот момент:
- Попытаться освободить страницы более дешёвым способом — сбросить часть страничного кеша, выгрузить неиспользуемые анонимные страницы в swap (если он есть и не исчерпан).
- Если этого недостаточно и физической страницы всё ещё не найти — вызвать OOM killer: он проходит по процессам, считает для каждого «показатель вины» (сочетание занятой памяти и настраиваемого смещения
oom_score_adj) и посылаетSIGKILLпроцессу с наибольшим значением.
Ключевой момент, который часто путают: OOM killer срабатывает не в момент вызова malloc(), а в момент реального обращения к странице — то есть спустя произвольное время после того, как приложение «попросило» память и получило успешный ответ. Это прямое следствие overcommit: раз выделение виртуальной памяти почти всегда проходит успешно, значит и убийство процесса — событие, полностью оторванное по времени от того места в коде, где память запрашивалась.
Как это выглядит в системных логах:
dmesg -T | grep -i "killed process"
journalctl -k | grep -i "out of memory"
Подробно про сам алгоритм выбора жертвы, oom_score и как защитить конкретный процесс через oom_score_adj, разобрано отдельно в статье про то, как ядро выбирает, кого убить при нехватке памяти — здесь важно другое: OOM killer это последняя линия защиты именно от того риска, который создаёт overcommit, позволяя виртуальным обещаниям накопиться быстрее, чем растёт реальное потребление.
free и доступная память: что цифры реально означают
Раз overcommit разводит понятия «выделено» и «реально занято», логично, что и стандартные утилиты мониторинга нужно читать с поправкой на это. Возьмём вывод free -h:
free -h
total used free shared buff/cache available
Mem: XXG XXG XXG XXG XXG XXG
Swap: XXG XXG XXG
- used — память, реально занятая процессами (резидентные страницы), без учёта кеша. Именно это ближе всего к «физическому спросу», о котором шла речь выше.
- buff/cache — страничный кеш и буферы файловой системы. Ядро охотно занимает под кеш всю свободную память, потому что кеш почти бесплатно ускоряет чтение файлов, но эти страницы забираются обратно моментально, как только процессу реально понадобилась память.
- available — оценка ядра, сколько памяти реально можно выдать новым процессам без ухода в swap, с учётом того, что часть кеша можно вытеснить. Это более честная цифра, чем free, для вопроса «хватит ли памяти новому процессу».
Важно: ни одна из этих колонок напрямую не показывает, насколько близко система подошла к пределу overcommit. Значение used может быть небольшим, а Committed_AS из /proc/meminfo — то есть сумма всех виртуальных обещаний, розданных всем процессам, — уже приближаться к CommitLimit. Это не одно и то же: used — это то, что реально занято сейчас, Committed_AS — это то, что ядро теоретически может быть обязано предоставить, если все процессы разом решат тронуть всю зарезервированную ими память. Подробнее о том, куда уходит «свободная» память и почему кеш путает картину, — в статье про страничный кеш и то, куда девается свободная память.
Как оценить реальный риск нехватки памяти на своём сервере
Overcommit сам по себе не опасен — опасно не понимать, где проходит граница между «ядро пока обещает» и «ядро скоро не сможет сдержать обещание». Несколько практических шагов, которые дают эту картину.
1. Смотрите committed-память, а не только used.
awk '/MemTotal|MemAvailable|Committed_AS|CommitLimit/' /proc/meminfo
Если Committed_AS заметно больше MemTotal + SwapTotal, система живёт в режиме сильного overcommit — это нормально для многих нагрузок (много контейнеров с общим базовым образом, много потоков с недоиспользуемыми стеками), но означает, что при одновременном «пробуждении» всех обещанных страниц системе физически не хватит места.
2. Держите под наблюдением активность swap, а не только его наличие.
vmstat 1 5
Колонки si/so (swap in / swap out) показывают, реально ли ядро уже перекладывает страницы в подкачку и обратно. Разовые всплески не страшны, но устойчивый ненулевой so — сигнал, что физической памяти уже не хватает и до OOM killer может остаться немного, особенно если swap небольшой. О том, как правильно рассчитать объём подкачки под конкретную нагрузку, — в статье про размер swap для VPS.
3. Для критичных процессов решите заранее, что предпочтительнее — быстрый отказ или риск позднего убийства.
Если для сервиса важнее «упасть сразу и явно, чем быть неожиданно убитым позже», для него имеет смысл vm.overcommit_memory=2 — тогда malloc() откажет с ENOMEM в момент запроса, приложение (если умеет) обработает ошибку контролируемо, вместо того чтобы получить SIGKILL в произвольной точке выполнения. Обратная сторона — часть легитимных сценариев (тот же форк для снапшота) может начать отказывать раньше, чем раньше.
4. В контейнерах смотрите не только на общесистемный overcommit, но и на cgroup-лимиты.
Контейнер работает поверх того же ядра и того же механизма overcommit, но вдобавок ограничен собственным лимитом памяти через cgroups. Внутри контейнера free может показывать память всего хоста, вводя в заблуждение, а реальным потолком окажется лимит cgroup — при его превышении сработает уже cgroup-специфичный OOM killer, а не общесистемный. Как это устроено и почему приложение внутри контейнера может «не знать» о своём настоящем лимите, разобрано в статье про то, как cgroups ограничивают контейнер.
5. Настройте oom_score_adj для процессов, которые нельзя терять первыми.
echo -500 > /proc/$(pgrep -f my-critical-service)/oom_score_adj
Это не защита от нехватки памяти как таковой, а способ управлять порядком, в котором OOM killer выбирает жертву, если до убийства всё же дошло.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если overcommit опасен, почему бы просто не отключить его совсем?
Отключить полностью нельзя — можно только выставить vm.overcommit_memory=2, что резко снижает overcommit, но не убирает его целиком: строгий режим всё равно допускает выделение сверх физически занятого, просто в рамках более консервативного лимита (swap + RAM * overcommit_ratio / 100). Полный отказ от overcommit сломал бы стандартные механизмы вроде fork() на больших процессах.
Успешный malloc() гарантирует, что память реально доступна?
Нет, и в этом суть overcommit. Успех malloc() в режимах 0 и 1 означает только, что ядро зарезервировало виртуальный адрес, а не то, что под него гарантированно найдётся физическая страница в момент, когда программа реально начнёт туда писать.
Как понять, что сервер уже близок к OOM, а не просто активно использует память под кеш?
Смотрите на available из free, на Committed_AS/CommitLimit из /proc/meminfo и на si/so в vmstat — устойчивая активность подкачки в сочетании с падающим available говорит о реальном риске гораздо точнее, чем просто высокий процент used.
Стоит ли менять vm.overcommit_memory на продакшене без тестирования?
Нет — режим 2 может привести к тому, что приложения, которые раньше работали благодаря overcommit (форки, резервирование памяти с большим запасом), начнут получать ENOMEM там, где раньше всё проходило. Меняйте на копии окружения, проверяйте поведение сервисов под нагрузкой и только затем переносите на боевой сервер.
Разный overcommit_ratio на сервере с большим объёмом swap и без него — это нормально?
Да, поскольку формула строгого режима учитывает и RAM, и swap. Сервер с большим swap может позволить себе выше overcommit_ratio, потому что у ядра есть куда выгружать страницы при реальном давлении на память, прежде чем дело дойдёт до OOM killer.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →