Когда серверу не хватает памяти, Linux не останавливается целиком: ядро выбирает процесс и принудительно завершает его. Этот механизм называется OOM killer (от out of memory — «память кончилась»). Снаружи это выглядит как внезапное падение базы данных, ошибка 502 на сайте или контейнер, который перезапускается без видимой причины. Ниже — как убедиться, что дело в памяти, найти, какой процесс завершён и почему, и что сделать, чтобы это не повторялось.

Команды проверены на Ubuntu 24.04 (systemd 255) и Ubuntu 26.04 (systemd 259), примеры с контейнерами — на Docker 29. Все сообщения ядра и systemd в статье — настоящий вывод из наших тестов.

Признаки нехватки памяти на сервере

  • Служба внезапно перезапустилась, а в её журнале нет ошибки — только code=killed, status=9/KILL.
  • Контейнер Docker остановился с кодом 137.
  • В журнале PostgreSQL — client backend (PID 83) was terminated by signal 9: Killed, после чего база обрывает все подключения и перезапускается.
  • Команда в терминале оборвалась со словом Killed.
  • Сервер долго отвечает, вход по SSH занимает десятки секунд, диск постоянно занят.

Сигнал 9 (SIGKILL) и код 137 (128 + 9) сами по себе не доказывают нехватку памяти: так же завершается процесс по kill -9. Доказательство — запись в журнале ядра, о ней ниже.

Как читать вывод free -h

free -h

Вывод на нашей тестовой машине (4 ГБ памяти, 1 ГБ подкачки) в момент, когда памяти не хватало:

               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       2.7Gi        63Mi        87Mi       1.3Gi       1.1Gi
Swap:          1.0Gi       1.0Gi        32Ki
  • total — вся память, used — занято программами.
  • free — совсем не используется. Маленькое значение здесь — норма: Linux занимает свободную память под кеш файлов.
  • buff/cache — кеш файлов и буферы ядра. Большую часть кеша ядро отдаёт программам по первому требованию.
  • available — главный столбец: сколько памяти могут получить программы, учитывая кеш, который можно освободить.
  • Swap — подкачка: участок диска, куда ядро выносит редко используемую память.

По free 63 МБ выглядит тревожно, но available — 1,1 ГБ, то есть прямо сейчас память есть. Тревожный признак в этом выводе другой — подкачка занята полностью: недавно памяти не хватило, и ядро вынесло на диск всё, что могло.

Если available в часы нагрузки регулярно опускается ниже 10 % от total, памяти мало.

vmstat: поля si и so показывают работу подкачки

free показывает момент, а vmstat — что происходит каждую секунду:

vmstat 1 6
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 0  0 1048400 161836   4260 1270584    6    8 13674   919 4579   10  5  4 91  0  0  0
 1  0 1048372  68376   4680 1318176   68   60 99328    60 6749 7141 40  9 50  1  0  0
 1  0 1048352 140808   4040 1287604   20    0 45132   240 3529 2531 16  6 78  1  0  0
 1  0 1048372  65872   3572 1327492   48  220 157552  7804 9445 9595 19 15 56 10  0  0
 2  0 1048364  95968   4608 1319124    4    0 14680    60 3748 2234 16 11 69  4  0  0
 7  0 1048352  69220   3312 1232392   12    0 129016     0 8178 9361 20 14 64  2  0  0

Первая строка — средние значения с момента загрузки, смотрите на следующие.

  • si / so — сколько килобайт в секунду ядро читает из подкачки и пишет в неё. Если оба столбца ненулевые постоянно, память работающих программ уже не помещается в оперативную, и сервер тратит время на перекладывание страниц между памятью и диском.
  • bi — чтение с диска. Большие значения при маленьком cache значат, что ядру не хватает памяти даже на кеш файлов и оно перечитывает их с диска.
  • wa — доля времени, когда процессор ждёт диск.
  • b — процессы, ожидающие ввода-вывода.

В выводе выше so доходит до 220 КБ/с, а bi — до 157 МБ/с: машина работает на пределе.

Ещё один показатель — давление на память (PSI), доля времени, в течение которого процессы ждали память:

cat /proc/pressure/memory
some avg10=1.27 avg60=5.41 avg300=21.02 total=5374333492
full avg10=1.22 avg60=4.57 avg300=15.49 total=3809828370

some avg300=21.02 — за последние 5 минут в 21 % времени хотя бы один процесс ждал память. На здоровом сервере здесь нули или доли процента.

Где искать OOM killer: dmesg и journalctl -k

Каждое срабатывание OOM killer ядро записывает в свой журнал. На Ubuntu журнал ядра читается с sudo:

sudo journalctl -k -g "out of memory" --since "2 days ago"
sudo dmesg -T | grep -i -E "out of memory|killed process"

Шаблон в journalctl -g пишите строчными буквами: тогда поиск не учитывает регистр и находит обе формы сообщения — Out of memory и Memory cgroup out of memory. С заглавной буквой вторая форма не найдётся.

Найдутся строки двух видов.

Кончилась память всего сервера. Так ядро завершило MySQL на нашей тестовой машине, когда другие процессы заняли всю память:

containerd invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=-500
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/docker/83e3731abda6…,task=mysqld,pid=56630,uid=999
Out of memory: Killed process 56630 (mysqld) total-vm:2177364kB, anon-rss:332220kB, file-rss:1184kB, shmem-rss:0kB, UID:999 pgtables:1264kB oom_score_adj:0

Процесс упёрся в ограничение своей службы или контейнера (о том, как его задать, — ниже):

python invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
memory: usage 102400kB, limit 102400kB, failcnt 108
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=b3ef78fd1125…,mems_allowed=0,oom_memcg=/docker/b3ef78fd1125…,task_memcg=/docker/b3ef78fd1125…,task=python,pid=42515,uid=0
Memory cgroup out of memory: Killed process 42515 (python) total-vm:116168kB, anon-rss:101572kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:264kB oom_score_adj:0

Как читать:

  • X invoked oom-killer — процесс, которому не хватило памяти в этот момент. Это не обязательно виновник: в первом примере память просил containerd, а завершён mysqld.
  • constraint=CONSTRAINT_NONE и global_oom — кончилась память всего сервера. CONSTRAINT_MEMCG — кончился лимит группы процессов (контейнера или службы systemd), остальной сервер не пострадал.
  • Killed process 56630 (mysqld) — кого завершили.
  • anon-rss — сколько памяти процесс занимал на момент завершения: у MySQL — 332 МБ.
  • Перед этими строками ядро печатает таблицу всех процессов с их памятью (Tasks state) — по ней видно, кто ещё занимал память.

Служба systemd

Если ядро завершило процесс службы, systemd пишет об этом в журнал службы:

systemctl status myapp
journalctl -u myapp -o cat
Active: failed (Result: oom-kill) since Sun 2026-10-11 11:19:28 UTC; 6s ago
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

Так пишет systemd 255 в Ubuntu 24.04. В Ubuntu 26.04 (systemd 259) первая строка звучит иначе: The kernel OOM killer killed some processes in this unit. Ищите по oom-kill — эта строка одинакова в обеих версиях.

Контейнер Docker

Контейнер, процесс которого завершён по нехватке памяти, останавливается с кодом 137, а Docker отмечает это в его состоянии:

docker ps -a                                                  # Exited (137)
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' myapp   # true 137
docker events --filter event=oom                              # следить за новыми случаями

OOMKilled становится true и при ограничении контейнера, и при нехватке памяти на всём сервере: так было с MySQL в примере выше.

Как OOM killer выбирает процесс: oom_score и oom_score_adj

Ядро считает для каждого процесса «очки» — в основном по тому, сколько памяти он занимает, — и завершает процесс с наибольшими очками. Поэтому первыми под удар попадают базы данных и крупные приложения.

Очки и поправка к ним лежат в /proc:

cat /proc/1234/oom_score       # текущие очки, чем больше — тем вероятнее завершение
cat /proc/1234/oom_score_adj   # поправка от -1000 до 1000, по умолчанию 0

Список процессов с наибольшими очками:

for p in /proc/[0-9]*; do printf "%s %s %s\n" "$(cat $p/oom_score)" "${p#/proc/}" "$(cat $p/comm)"; done 2>/dev/null | sort -rn | head

Поправку меняют, чтобы защитить важный процесс или, наоборот, подставить второстепенный. -1000 запрещает завершать процесс совсем. Поменять поправку у работающего процесса — choom:

sudo choom -p 1494 -n 300
pid 1494's OOM score adjust value changed from -500 to 300

Чтобы поправка сохранялась после перезапуска, задайте её в настройках службы (OOMScoreAdjust=-500, см. следующий раздел) или контейнера (docker run --oom-score-adj=-500).

Не защищайте от OOM killer всё подряд: если процесс с -1000 сам займёт всю память, ядро будет завершать всё остальное, включая SSH.

systemd-oomd на Ubuntu Server

systemd-oomd — служба, которая завершает процессы раньше ядра, когда давление на память (PSI) держится высоким. В Ubuntu она входит в настольную редакцию: пакет systemd-oomd ставится как зависимость ubuntu-desktop и ubuntu-desktop-minimal.

В Ubuntu Server 24.04 и 26.04 её нет: пакета нет в списке пакетов облачного образа, и пакет ubuntu-server от него не зависит. Проверить на своём сервере:

systemctl status systemd-oomd
Unit systemd-oomd.service could not be found.

Значит, на сервере работает только OOM killer ядра, и искать его следы нужно в журнале ядра, как описано выше.

Как ограничить память службы: systemd MemoryMax

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

Ограничение задаётся в дополнительном файле настроек службы, чтобы не править основной файл. Для службы myapp:

sudo mkdir -p /etc/systemd/system/myapp.service.d
sudo tee /etc/systemd/system/myapp.service.d/memory.conf > /dev/null <<'EOF'
[Service]
MemoryMax=300M
OOMScoreAdjust=300
Restart=on-failure
EOF
sudo systemctl daemon-reload
sudo systemctl restart myapp
  • MemoryMax — жёсткий предел: при превышении ядро завершает процесс службы.
  • OOMScoreAdjust — поправка к очкам для этой службы.
  • Restart=on-failure — systemd запустит службу снова после завершения.

Если на сервере есть подкачка, служба сверх MemoryMax может занять ещё и её. Запретить это — строкой MemorySwapMax=0.

Проверка — в systemctl status myapp появится строка с пределом, а в /proc/<PID>/oom_score_adj — поправка:

Memory: 90.6M (max: 300.0M available: 209.3M peak: 90.6M)

Что происходит, когда служба выходит за предел (journalctl -u myapp -o cat, Ubuntu 24.04):

Started myapp.service - Test app.
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.
myapp.service: Scheduled restart job, restart counter is at 1.
Started myapp.service - Test app.

Есть и мягкий порог — MemoryHigh. При его превышении ядро не завершает процессы, а замедляет их и отбирает у службы кеш. В нашем тесте служба с MemoryHigh=40M и без подкачки упёрлась в этот порог и почти перестала работать, но завершена не была. Используйте MemoryHigh вместе с MemoryMax и чуть ниже него — как сигнал, а не как единственное ограничение.

Как ограничить память контейнера Docker

docker run -d --name myapp --memory=300m --memory-swap=300m myimage

Если указать только --memory, контейнер сможет занять столько же ещё в подкачке: по умолчанию --memory-swap равен удвоенному --memory. Одинаковые значения запрещают контейнеру подкачку.

Как это выглядит, проверим на контейнере с пределом 100 МБ, в котором Python выделяет память по 10 МБ:

docker run --name oomtest --memory=100m --memory-swap=100m python:3-slim python -c '
import time
chunks = []
for i in range(1, 1000):
    chunks.append(bytearray(10 * 1024 * 1024))
    print(f"выделено {i * 10} МБ", flush=True)
    time.sleep(0.1)
'
echo "exit code: $?"
docker inspect oomtest --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
docker rm oomtest
выделено 80 МБ
выделено 90 МБ
exit code: 137
OOMKilled=true ExitCode=137

В журнале ядра при этом — строки Memory cgroup out of memory, которые мы разобрали выше.

В Docker Compose то же самое:

services:
  app:
    image: myimage
    deploy:
      resources:
        limits:
          memory: 300M
    memswap_limit: 300M
    restart: unless-stopped

Проверка: docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' <контейнер> покажет 314572800 314572800, а в docker stats в столбце LIMIT — 300MiB.

Подкачка как буфер

Файл подкачки не заменяет память, но даёт серверу запас на пики: при кратковременной нехватке ядро вынесет на диск редко используемые страницы вместо того, чтобы сразу завершать процесс. На серверах с 1–2 ГБ памяти это особенно полезно — например, на время сборки проекта или обновления системы. Как создать файл подкачки, описано в разделе «Файл подкачки» статьи о первой настройке сервера.

Если же si и so в vmstat ненулевые постоянно, а подкачка заполнена, подкачка уже не буфер: сервер работает медленно, потому что постоянно обращается к диску. Это признак того, что памяти нужно больше.

Когда добавлять память

Добавляйте память, если выполняется хотя бы одно из условий:

  • available в free -h в часы нагрузки регулярно ниже 10 % от total;
  • OOM killer срабатывает при обычной нагрузке, а не из-за утечки памяти в одном процессе;
  • si и so в vmstat ненулевые постоянно, подкачка заполнена;
  • some avg300 в /proc/pressure/memory держится выше нескольких процентов.

Если же память раз за разом съедает один процесс и растёт без остановки — это утечка памяти. Добавленная память лишь отложит проблему: ограничьте процесс и ищите причину в приложении.

Сколько памяти нужно под типовые задачи, мы посчитали в статье «Сколько оперативной памяти нужно VPS». В Frogus Cloud память можно добавить к работающему серверу; уменьшить её потом нельзя, поэтому добавляйте по замерам, а не впрок.

Если не работает

Что видно Что делать
Служба перезапускается, но в журнале ядра нет out of memory Причина не в памяти: смотрите journalctl -u <служба> перед завершением
sudo dmesg пуст или не показывает старые записи Буфер ядра ограничен по размеру и очищается при перезагрузке; ищите в journalctl -k -b -1 — журнал прошлой загрузки
journalctl -k -g "Out of memory" не находит Memory cgroup out of memory Пишите шаблон строчными буквами: -g "out of memory"
Завершается база данных, хотя память занимает приложение Ограничьте приложение через MemoryMax или --memory, базе поставьте отрицательный OOMScoreAdjust
Служба с MemoryHigh «зависла» Она упёрлась в мягкий порог: поднимите MemoryHigh или задайте MemoryMax
Контейнер завершается с кодом 137, но OOMKilled=false Процесс завершили сигналом 9 вручную или по таймауту остановки (docker stop), а не из-за памяти

Что дальше