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

Т.е. когда процесс говорит, что нуждается в 2 Гб памяти ОС резервирует за ним эти 2 Гб, но использование памяти происходит по факту. Возможно, что эти 2 Гб памяти будут использоваться только при пиковой нагрузке на процесс, а сейчас по факту он использует 200-300 Мб. Получается, что в итоге за процессом резервируется 2Гб, но используется только 200-300 Мб. Т.е. все 2Гб будут выделены процессу только тогда, когда они ему действительно нужны.

memmory allocation

Минус такого подхода в том, что может так сложиться что в определённый момент ОС зарезервирует больше памяти чем у неё есть. И если вся память ОЗУ будет израсходована, то произойдёт сбой системы.

memmory allocation

Т.е. в системе с 2 Гб ОЗУ процессам может быть зарезервировано все 3 Гб.

Демонстрация

Для того чтобы продемонстрировать что произойдёт с ОС при полной загрузке ОЗУ просто забьём всё свободное место в директориях /dev/shm и /run. Перед выполнением запустите в другом терминале команду top чтобы мониторить загрузку по ОЗУ.

Внимание не повторяйте эти команды если вам важно время работы ОС.

Для начала отключим swap:

sudo swapoff -a

И теперь используя команду dd забьём всё свободное место:

sudo dd if=/dev/zero of=/run/fill bs=3k count=1024k
sudo dd if=/dev/zero of=/dev/shm/fill bs=3k count=1024k

И когда мы это выполнили загрузка по ОЗУ должна быть 100%.

Теперь попробуем выполнить любую команду.

sudo df -h
-bash: fork: Cannot allocate memory

И всё, у вас нет больше памяти для новых процессов. Более того вы даже не сможете выполнить команду reboot для перезагрузки ОС.

Как выбраться

Для того чтобы отменить предыдущий шаг демонстрации можно перезагрузить ОС на горячую, используя кнопку перезагрузки. И второй способ — это убить какой-нибудь не сильно важный процесс в системе, благо команда kill работает и уже после удалить файлы /dev/shm/fill и /run/fill.

kill -9 1122
rm /dev/shm/fill /run/fill

OOM Killer

Ну и согласитесь терять вот так боевой сервер, тем более если это физический сервер и стоит он где-то в другом городе не хотелось бы только из-за того, что какой-то процесс отъелся ОЗУ. И для того чтобы такого не происходило у нас есть Out Of Memory Killer. Он принудительно завершает один из процессов что в итоге освобождает память. По сути в блоке сверху я сделал то же самое что и OOM Killer, за исключением того, что я выбрал ненужный мне процесс. OOM Killer же может убить и весьма важный процесс для вас, например процесс PostgreSQL.

Понять, что тот или иной процесс был убит OOM Killer можно по записям в файле /var/log/messages. Он будет содержать запись Out of Memory: Killed process 125 (vault).

Для того, чтобы завершить процесс ОС вызывает функцию out_of_memory. После чего OOM Killer решает какой именно процесс ему завершить и потом проделывает это с ним.

Перед тем как вызвать out_of_memory выполняются следующие проверки:

  • Достаточно ли в ОС еще места подкачки (swap) (nr_swap_pages > 0)? Если да, то не вызываем ООМ
  • Был ли завершений процесс в течение последних 5 секунд? Если да, то не вызываем ООМ
swapoff -a
perl -wE 'my @xs; for (1..2**20) { push @xs, q{a} x 2**20 }; say scalar @xs; sleep;'
swapon -a
Killed
cat /var/log/messages | grep "Out of"
Jan 24 11:57:39 vm-2 kernel: Out of memory: Killed process 11144 (perl) total-vm:6500608kB, anon-rss:3421028kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:12752kB oom_score_adj:0

Какой процесс завершать?

В функции out_of_memory, которую мы упоминали выше вызывается функция под названием select_bad_process, которая отвечает за выбор процесса для завершения. Она решает насколько тот или иной процесс подходит для уничтожения с помощью функции badness. В итоге благодаря функции badness процесс проходит отбор по правилам и в конце назначается репутация процесса.

oom killer functions

Получается, что OOM Killer не завершает любой процесс, он завершает именно тот который по мнению функции select_bad_process является самым плохим основываясь на оценке процесса (oom_score).

Посмотреть какую именно оценку получил тот или иной процесс вы всегда можете, выполнив команду cat /proc/$PID/oom_score, где $PID - id процесса.

cat /proc/868/oom_score
670

Чем выше репутация у процесса, тем больше вероятности что именно его и завершит OOM Killer.

Оценка вычисляется благодаря функции int_sqrt. Во время оценки внимание обращается на:

  • Объем памяти, используемый процессом.
  • Размер памяти любого дочернего процесса (не включая поток ядра).
  • Оценка процесса увеличивается для хороших процессов и уменьшается для длительных процессов.
  • У процессов с возможностями CAP_SYS_ADMIN и CAP_SYS_RAWIO снижены оценки.

Если процесс запущен от root его значение будет разделено еще на 4, потому что от root запускаются как правило только хорошие процессы. То же самое происходит и с процессами, которые имеют доступ к физическим устройствам (/dev).

Для примера запустим 2 perl скрипта и проверим их репутацию.

perl -wE 'my @xs; for (1..2**15.7) { push @xs, q{a} x 2**15.7 }; say scalar @xs; sleep;'
perl -wE 'my @xs; for (1..2**14.6) { push @xs, q{a} x 2**14.6 }; say scalar @xs; sleep;'
   3914 root       20   0 2731M 2706M   424 S  0.0 73.4  0:00.54 perl -wE my @xs; for (1..2**15.7) { push @xs, q{a} x 2**15.7 }; say scalar @xs; sleep;
   4018 root       20   0  615M  590M   424 S  0.0 16.0  0:00.12 perl -wE my @xs; for (1..2**14.6) { push @xs, q{a} x 2**14.6 }; say scalar @xs; sleep;
cat /proc/3914/oom_score
cat /proc/4018/oom_score
1156
773

В итоге получается, что процесс, который использует больше памяти имеет репутацию больше чем второй процесс.

Отключить OOM Killer

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

Но если вы всё же хотите отключить OOM Killer, то выполняем следующее:

sudo cat /proc/sys/vm/panic_on_oom
0
sudo echo 1 > /proc/sys/vm/panic_on_oom

Уменьшаем oom_score

Если вам важно чтобы какой-то процесс не завершался, то можно уменьшиться его репутацию. Для этого в файл oom_adj помещаем значение от -16 до +15.

cat /proc/8367/oom_score
1157
echo -5 > /proc/8367/oom_adj
cat /proc/8367/oom_score
961
echo 5 > /proc/8367/oom_adj
cat /proc/8367/oom_score
1353

Отключить OOM Killer для процесса

Если вы хотите отключить OOM Killer только для определённого процесса в файл oom_adj записываем число -17. Но нужно понимать, что как только вы перезапустите службу отвечающую за этот процесс его id изменится и соответственно и репутация.

echo -17 > /proc/8367/oom_adj
cat /proc/8367/oom_score
0

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

И в таком случае лучше задавать репутацию в файле сервиса через переменную OOMScoreAdjust.

[Service] OOMScoreAdjust=-17

PostgreSQL

В приложении сервера БД PostgreSQL каждый запрос порождает отдельный процесс. Соответственно если ОЗУ закончилось сам сервис PostgreSQL отключен не будет, завершиться только тот процесс, который занял всё ОЗУ.

При этом сама база перейдёт в режим recovery из-за отмены транзакции.

В итоге

Не нужно боятся OOM Killer он наоборот сохранит вам нервы. Другое дело что нужно писать свои сервисы так чтобы они не съедали всё ОЗУ на сервере или ограничивать объём используемого ОЗУ для сервиса.