Настройка swap: как предотвратить аварии из-за нехватки памяти

Опубликовано 14 мин. чтения

Служба исчезла, отчёта об аварии нет, лог обрывается на середине строки: как доказать OOM-kill, аккуратно создать файл подкачки и понять, когда swap лишь откладывает проблему.

Служба исчезла. Ни отчёта об аварии, ни стека вызовов, лог обрывается на середине строки. MariaDB не отвечает, nginx отдаёт 502, Minecraft-сервер офлайн, а в самом приложении ничего подозрительного нет. Почти всегда это один и тот же сценарий: у ядра закончилась свободная память, и оно завершило процесс, чтобы система осталась живой. В этой статье показано, как доказать это без всяких сомнений, как аккуратно создать файл подкачки и прописать его на постоянной основе, а также в каких случаях swap лишь отодвигает проблему на несколько минут.

Собираем доказательства: dmesg и journalctl

Прежде чем что-либо настраивать, нужны доказательства. При каждом OOM-kill (out of memory) ядро записывает в кольцевой буфер подробный блок:

dmesg -T | grep -iE 'out of memory|oom-kill'

Если ни одной строки не возвращается, а команда завершается с кодом 1, значит с момента последней загрузки OOM на уровне ядра не было. Код возврата здесь не ошибка, а обычный ответ grep, когда совпадений нет. Если же вывод есть, на всех четырёх рассматриваемых системах он выглядит так:

mariadbd invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/mariadb.service,task=mariadbd,pid=1043,uid=107
Out of memory: Killed process 1043 (mariadbd) total-vm:2894760kB, anon-rss:1583204kB, file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:3820kB oom_score_adj:0

Здесь важны три вещи. Во-первых, invoked oom-killer называет процесс, который запросил память, а не обязательно виновника. Во-вторых, завершённый процесс указан в строке Killed process. В-третьих, ориентироваться надо на anon-rss, то есть на реально занятую анонимную память. total-vm — это зарезервированное адресное пространство, у Java или Go оно регулярно превышает реальное потребление в разы и ни о чём не говорит.

Если dmesg запускается без root, то на Debian 12, Debian 13, Ubuntu 22.04 и Ubuntu 24.04 он отвечает dmesg: read kernel buffer failed: Operation not permitted, потому что kernel.dmesg_restrict везде выставлен в 1. Значит, работать нужно через sudo или под root. Если сообщение остаётся и под root, вы находитесь в LXC- или OpenVZ-контейнере, у которого нет доступа к кольцевому буферу хост-системы. Тогда остаётся только второй путь, через journalctl -k.

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

journalctl -k --grep "Out of memory"

Ключ -k неявно включает -b, то есть показывает исключительно текущую загрузку. Если машина после инцидента была перезагружена, эта команда не выдаст ничего, хотя событие в журнале записано. Для предыдущей загрузки или для интервала времени:

journalctl -k -b -1 --grep "Out of memory"
journalctl --since "7 days ago" --grep "Out of memory|oom-kill"

Если первая из двух команд отвечает No journal boot entry found for the specified boot (-1), то в журнале просто нет более ранней загрузки. Это тоже не ошибка, а обычная ситуация для системы, журнал которой ведётся только с текущего запуска.

Работает всё это лишь при условии, что журнал вообще постоянный. Проверьте:

ls -d /var/log/journal

На Debian и Ubuntu этот каталог присутствует с завода, поэтому проверка почти всегда что-то находит, а следующая команда mkdir просто ничего не делает и ничего не ломает. Если каталога в виде исключения всё же нет, журнал лежит только в /run и теряется при каждой перезагрузке. Наверстать это можно двумя командами:

mkdir -p /var/log/journal
systemctl restart systemd-journald

OOM ядра или лимит systemd? Две разные причины

Именно это различие решает, даст ли swap хоть какой-то эффект. Обратите внимание на поле constraint в сообщении ядра.

CONSTRAINT_NONE означает: память закончилась у всей системы. Здесь swap помогает.

CONSTRAINT_MEMCG означает: лимит превысила лишь одна control-group, у остальной системы запас был приличный. Здесь swap не поможет, здесь надо поднимать лимит или учить приложение экономить. Такие завершения видны и по самой службе:

systemctl status mariadb

Если там стоит Main process exited, code=killed, status=9/KILL, а ниже Failed with result 'oom-kill', значит это был OOM-kill. Был ли при этом задействован лимит cgroup, покажет файл со счётчиками. Он требует cgroup v2, который на всех четырёх дистрибутивах используется по умолчанию; в более старом cgroup v1 такого пути нет:

cat /sys/fs/cgroup/system.slice/mariadb.service/memory.events
systemctl show mariadb -p MemoryMax -p MemoryHigh

Значение больше нуля у oom_kill вместе с заданным MemoryMax и есть доказательство локального лимита.

Есть и третий кандидат, о котором часто забывают, потому что он вообще ничего не пишет в dmesg: systemd-oomd. Эта служба работает в пространстве пользователя, анализирует показатель нагрузки PSI и завершает целые control-group ещё до того, как вмешается ядро. В журнале её сообщение выглядит примерно так: Killed /system.slice/... due to memory pressure for /system.slice being 60.00% > 50.00% for > 20s with reclaim activity. Проверьте, запущена ли она:

systemctl is-active systemd-oomd

В Ubuntu systemd-oomd начиная с 22.04 установлен и активен с завода, в Debian он в стандартную поставку не входит. Поэтому inactive на Debian-сервере ожидаем и признаком неисправности не является.

Важно для дальнейшего: systemd-oomd может сработать и потому, что заполняется swap. То есть при активном oomd больший объём подкачки способен приблизить аварийные завершения, а вовсе не отсрочить их.

Перед настройкой swap: сколько памяти не хватает на самом деле

Две минуты измерений избавляют от неверного решения.

free -h

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

ps -eo pid,comm,rss,%mem --sort=-rss | head -n 11
systemd-cgtop --order=memory -b -n 1

Первая команда показывает самые крупные отдельные процессы, вторая группирует их по службам. На практике там почти всегда одни и те же три подозреваемых: MariaDB со слишком большим innodb_buffer_pool_size, PHP-FPM со слишком высоким pm.max_children и JVM со слишком щедрым -Xmx.

В качестве отправной точки для размера файла подкачки достаточно этой таблицы. На сервере больше не значит лучше: swap, который действительно используется целиком, делает машину неуправляемой.

Оперативная памятьРазумный размер файла подкачки
1 ГБ1-2 ГБ
2 ГБ2 ГБ
4-8 ГБ2-4 ГБ
16 ГБ и больше4 ГБ, редко больше

Создание файла подкачки

Сначала посмотрите, нет ли swap уже сейчас. Серверы Ubuntu, установленные с ISO-образа, часто уже несут /swap.img, Debian из установщика обычно получает настоящий раздел подкачки. В облачных образах обоих дистрибутивов, как правило, нет ничего.

swapon --show

Если вывод остаётся пустым, swap отсутствует. Следующим шагом проверьте файловую систему, от неё зависит порядок действий:

findmnt -no FSTYPE -T /

На ext4 или xfs можно продолжать сразу. Создавайте файл командой dd, а не fallocate. fallocate работает быстрее, но в зависимости от файловой системы и ядра оставляет в файле незаписанные области, и тогда swapon его отвергает. dd пишет настоящие нули и работает везде:

dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
chmod 600 /swapfile

Перед следующим шагом стоит выполнить проверку, которую многие пропускают. Если по пути /swapfile уже включён swap, например от прежней попытки или из настроек образа, mkswap откажется работать с сообщением mkswap: error: /swapfile is mounted; will not make swapspace. Значение имеет только одно: числится ли путь активным в /proc/swaps. Поэтому проверьте и при необходимости отключите:

swapon --show
swapoff /swapfile

Если в выводе swapon --show нет строки с /swapfile, команду swapoff можно пропустить. После этого записывается область подкачки:

mkswap /swapfile

mkswap отвечает строкой Setting up swapspace version 1, size = 2 GiB (2147479552 bytes) и новым UUID. Только после этого включаем:

swapon /swapfile

Порядок не обсуждается: chmod перед mkswap, mkswap перед swapon, а возможный swapoff прежде всего остального.

Как убедиться, что всё действительно заработало

То, что swapon отработал без сообщений об ошибке, доказательством ещё не является. Доказательство — это два следующих вывода:

swapon --show
free -h

swapon --show обязан выдать строку с /swapfile, типом file, размером и приоритетом. В free -h строка Swap: должна смениться с 0B на новый размер. Если один из двух выводов остался прежним, swap не активен, что бы ни сообщала команда до этого.

Постоянная запись без риска для загрузки

Команда swapon перезагрузку не переживает. Запись должна попасть в /etc/fstab, и именно там серверы обычно и ломают. Сначала резервная копия:

cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Обратите внимание на два знака «больше». Одиночный > перезапишет весь файл, и тогда машина уже не загрузится нормально. После этого проверьте синтаксис, прежде чем перезагружаться:

findmnt --verify

Одно предупреждение при этом совершенно нормально и не является поводом убирать строку: [W] non-bind mount source /swapfile is a directory or regular file. Для файла подкачки иначе и быть не может, команда всё равно завершается с кодом возврата 0. Если у файла вдобавок нет корректного заголовка подкачки, добавится [W] cannot detect on-disk filesystem type, и вот тогда вы действительно забыли mkswap.

Если выше вы уже включили swap вручную командой swapon /swapfile, он активен и сейчас. Если нет, swapon -a активирует все записи из fstab без перезагрузки. Но настоящая проверка другая. Из каждой строки fstab systemd создаёт отдельный unit, для /swapfile он называется swapfile.swap. Если этот unit появляется и активен, при следующем запуске swap будет подключён гарантированно:

systemctl daemon-reload
systemctl list-units --type swap

Ожидается строка swapfile.swap loaded active active Swap. Если её нет, строка в fstab неверна, и перезагрузка закончится без swap. Только когда всё сходится, имеет смысл перезагружаться с последующей проверкой через swapon --show.

Правильная настройка swappiness

Параметр ядра vm.swappiness задаёт, насколько охотно анонимные страницы вытесняются в подкачку вместо сброса файлового кэша. На Debian 12, Debian 13, Ubuntu 22.04 и Ubuntu 24.04 значение по умолчанию одинаково и равно 60:

cat /proc/sys/vm/swappiness

Начиная с ядра 5.8 диапазон значений идёт от 0 до 200, и все четыре дистрибутива поставляются с более новыми ядрами. Два распространённых заблуждения. vm.swappiness=0 не отключает подкачку, а лишь запрещает упреждающее вытеснение: ядро всё равно уйдёт в подкачку, прежде чем кого-то завершить. И низкое значение не делает систему быстрее, если памяти попросту не хватает.

Разумные значения: 10-20 на серверах баз данных, 60 на смешанных веб-серверах, 100 и больше при использовании zram. На постоянной основе это прописывается в отдельный файл в /etc/sysctl.d/, а не в /etc/sysctl.conf, который мешается при обновлении пакетов:

echo 'vm.swappiness = 10' > /etc/sysctl.d/99-swappiness.conf
sysctl --system
cat /proc/sys/vm/swappiness

Третья команда служит проверкой результата. sysctl --system читает все каталоги в фиксированном порядке, и уже имеющийся файл с большим номером может перекрыть ваше значение.

Если что-то пошло не так: сообщения об ошибках дословно

swapon: /swapfile: insecure permissions 0644, 0600 suggested. Это лишь предупреждение, swap всё равно работает. Устранить его тем не менее стоит, иначе любой пользователь сможет прочитать содержимое вытесненных процессов: chmod 600 /swapfile.

swapon: /swapfile: swapon failed: Invalid argument Самая частая ошибка. Либо забыли mkswap, либо в файле есть дыры. В журнале ядра тогда дополнительно появляется swapon: swapfile has holes. Решение: удалить файл и создать заново через dd, а не через fallocate.

На btrfs действует та же ошибка плюс BTRFS warning: swapfile must not be copy-on-write. Здесь порядок другой: файл нужно создать пустым и до заполнения пометить как не copy-on-write. Сразу важное замечание, от которого может зависеть судьба работающего сервера: truncate -s 0 и rm отрабатывают и в том случае, если по этому пути ещё подключён активный swap. После этого ядро ссылается на блоки, которых больше нет. Поэтому, если swapon --show показывает путь как активный, перед этим обязателен swapoff /swapfile.

truncate -s 0 /swapfile
chattr +C /swapfile

Дальше как обычно: заполнить через dd, затем chmod 600, mkswap, swapon. На сжатых подтомах и в снимках swap по-прежнему не работает.

swapon: /swapfile: swapon failed: Operation not permitted Вы находитесь в контейнере. LXC, OpenVZ и Docker используют общее ядро хоста и не имеют права включать собственный swap. Проверка:

systemd-detect-virt

Если команда сообщает kvm, qemu или none, работает собственное ядро и swap возможен. Если она сообщает lxc, openvz или docker, поможет только увеличение оперативной памяти или продукт с полноценной виртуализацией. KVM root-серверы и выделенные серверы KernelHost работают с собственным ядром, swap там настраивается без ограничений.

dd: error writing '/swapfile': No space left on device Накопитель переполнен. Сначала df -h, затем удалите наполовину записанный файл командой rm /swapfile, иначе он продолжит занимать место. Здесь тоже действует правило: если swapon --show показывает путь как активный, перед удалением нужен swapoff /swapfile.

Система больше не загружается, появляется аварийная консоль. Почти всегда это опечатка в /etc/fstab. Войдите через консоль в личном кабинете, затем mount -o remount,rw /, удалите ошибочную строку или скопируйте обратно /etc/fstab.bak, после чего перезагрузите. Именно для этого резервная копия и создавалась заранее.

Различия между Debian 13, Debian 12, Ubuntu 24.04 и 22.04

Команды на всех четырёх системах одинаковы, а вот исходное состояние нет.

  • Уже имеющийся swap: Ubuntu Server из ISO-установщика часто создаёт /swap.img, Debian из установщика — раздел подкачки. Облачные образы обоих дистрибутивов идут без swap. Всегда начинайте с swapon --show.
  • systemd-oomd: в Ubuntu начиная с 22.04 установлен и активен с завода, в обеих версиях Debian в стандартную поставку не входит. Это объясняет, почему одинаковое ПО на двух внешне одинаковых системах может умирать по-разному.
  • База данных: Debian 12 и Debian 13 не поставляют mysql-server, там всегда работает MariaDB (10.11 в Debian 12, 11.8 в Debian 13). В Ubuntu 22.04 и 24.04 есть и то, и другое. Значения innodb_buffer_pool_size по умолчанию соответственно различаются, и именно этот параметр на небольших серверах чаще всего приводит к OOM.
  • Java: в Debian 12 есть только OpenJDK 17, в Debian 13 только OpenJDK 21, Ubuntu 22.04 и 24.04 покрывают версии с 8 по 21. JVM без заданного -Xmx по умолчанию забирает четверть оперативной памяти, и при нескольких экземплярах OOM-kill запрограммирован заранее.
  • Сообщения ядра: формат и формулировки сообщения об OOM на всех четырёх системах совпадают, приведённые выше примеры подходят везде.
  • swappiness: везде 60 по умолчанию.

Когда swap помогает, а когда лишь откладывает проблему

Swap надёжно выручает при коротких пиках, например во время резервного копирования, обновления пакетов или ночного импорта. Он помогает со службами, которые занимают много памяти, а потом сутками ничего не делают: такие страницы вполне могут полежать на накопителе. И в критической ситуации он дарит вам те самые минуты, когда SSH-сессия ещё отвечает и вы можете вмешаться, вместо того чтобы смотреть на мёртвый сервер.

Swap не помогает, когда постоянная потребность в памяти просто превышает объём RAM. Тогда система начинает буксовать (thrashing): она непрерывно вытесняет страницы и загружает их обратно, load average уходит в двузначные значения, загрузка CPU остаётся низкой, всё ждёт I/O. На практике это хуже честного OOM-kill, потому что даже вход в систему перестаёт проходить. Две команды показывают, находитесь ли вы в этом состоянии:

vmstat 1 5
test -e /proc/pressure/memory && cat /proc/pressure/memory

У vmstat важны столбцы si и so. Постоянные трёхзначные значения означают активное вытеснение и обратную загрузку страниц. В /proc/pressure/memory решающим является full avg10: значения выше 10 говорят о том, что вся система десять процентов времени ждёт память. Выше 40 машина практически мертва. Правда, файл существует только тогда, когда в ядре активен механизм Pressure Stall Information. Штатные ядра Debian и Ubuntu его содержат, другие ядра не обязательно. Отсюда и подстраховка через test -e: если файла нет, вывод останется пустым, и вы не примете No such file or directory за неисправность. Дополнительно PSI включается параметром загрузки ядра psi=1.

Какие процессы действительно лежат в подкачке, покажет эта строка:

grep VmSwap /proc/*/status | sort -k2 -rn | head

Точно так же swap бесполезен против лимита cgroup (CONSTRAINT_MEMCG), против памяти, закреплённой через mlock, и против JVM, у которой heap настроен больше имеющегося RAM. Сборщик мусора регулярно проходит по всему heap и немедленно возвращает каждую вытесненную страницу обратно.

Три инструмента на случай, когда swap не спасает

zram создаёт сжатую область подкачки в самой оперативной памяти. Она на порядки быстрее файла на накопителе и в зависимости от характера данных даёт эффективный прирост памяти на 20-40 процентов:

apt update
apt install -y zram-tools

Настройка выполняется в /etc/default/zramswap через ALGO=zstd и PERCENT=50, после чего systemctl restart zramswap. Контроль через zramctl и снова через swapon --show, где теперь появится /dev/zram0. С zram параметр vm.swappiness следует поднять до 100-180, потому что вытеснение здесь обходится дёшево.

earlyoom вмешивается раньше, чем ядро подвесит систему, и завершает процессы прицельно, а не по эвристике:

apt install -y earlyoom

В /etc/default/earlyoom через EARLYOOM_ARGS задаются пороги и защищаются важные процессы, например так: -m 5 -s 5 --avoid '(^|/)(sshd|systemd)$'. Благодаря этому вход по SSH остаётся доступным, пока пожиратель памяти падает.

Жёсткие лимиты для отдельной службы являются самым аккуратным решением, если из рамок регулярно выходит одна конкретная служба. Через systemctl edit mariadb впишите MemoryHigh=1200M и MemoryMax=1500M. Тогда служба сначала притормаживается, а в крайнем случае завершается в одиночку, вместо того чтобы утащить за собой весь сервер. Контроль через systemctl show mariadb -p MemoryMax.

Но настоящим исправлением в большинстве случаев остаётся конфигурация приложения: innodb_buffer_pool_size в реалистичных пределах, pm.max_children исходя из фактического потребления памяти одним воркером, заданный -Xmx для каждой JVM. Swap — это страховочная сетка, а не решение.

Откат, если решение не подошло

Путь назад короткий, и проходить его надо именно в таком порядке:

swapoff /swapfile

При сильно заполненной подкачке команда может выполняться несколько минут, потому что все страницы обязаны вернуться в RAM. Если она упирается в swapoff: /swapfile: swapoff failed: Cannot allocate memory, значит в RAM нет места для обратного переноса и сначала придётся остановить службы. Только после успешного swapoff удаляйте строку из fstab и сам файл:

rm /swapfile
swapon --show

Удаление файла, пока он ещё подключён, приводит к системе, чьё управление памятью ссылается на несуществующий inode. Заканчивается это плохо. В завершение ещё раз free -h и перезагрузка в качестве контрольной проверки.

Частые вопросы

Сколько swap нужно на сервере?
На серверах старое правило «двойной объём оперативной памяти» больше не действует. Разумно брать 1-2 ГБ при 1 ГБ RAM, 2 ГБ при 2 ГБ RAM и 2-4 ГБ при 4-8 ГБ RAM. Начиная с 16 ГБ достаточно 4 ГБ. Swap, который действительно используется целиком, делает систему неуправляемой из-за непрерывного вытеснения и обратной загрузки страниц, так что лишний объём — это не резерв, а лишь более долгая агония.
Почему journalctl -k не показывает OOM-kill, хотя он был?
Потому что ключ -k неявно включает -b и показывает только текущую загрузку. Если сервер после инцидента был перезагружен, вы ничего не увидите. Используйте journalctl -k -b -1 для предыдущей загрузки или journalctl --since "7 days ago" --grep "Out of memory|oom-kill" для интервала времени. Условие: постоянный журнал, наличие которого проверяется командой ls -d /var/log/journal, на Debian и Ubuntu он есть с завода. Если journalctl -k -b -1 вместо этого отвечает "No journal boot entry found for the specified boot (-1)", значит более ранней загрузки в журнале просто нет.
Как понять, кто завершил процесс: ядро или лимит systemd?
По полю constraint в сообщении ядра. CONSTRAINT_NONE означает, что память закончилась у всей системы, и здесь swap помогает. CONSTRAINT_MEMCG означает, что лимит превысила отдельная control-group, и здесь swap не поможет. Дополнительно проверьте systemctl show СЛУЖБА -p MemoryMax, а также счётчик oom_kill в файле memory.events соответствующей control-group.
Почему swapon завершается с ошибкой «Invalid argument»?
Либо забыли mkswap, либо файл был создан через fallocate и содержит незаписанные области. В журнале ядра тогда появляется swapon: swapfile has holes. Создавайте файл вместо этого командой dd if=/dev/zero. На btrfs файл дополнительно нужно заранее пометить как не copy-on-write через truncate -s 0 и chattr +C, причём только после swapoff /swapfile, потому что очистка подключённого файла подкачки заставит ядро ссылаться на несуществующие блоки. Если уже mkswap отказывается работать с сообщением "/swapfile is mounted; will not make swapspace", значит по этому пути ещё подключён активный swap, что видно в swapon --show.
Можно ли настроить swap в контейнере?
Нет. LXC, OpenVZ и Docker используют ядро хост-системы и не имеют права активировать собственный swap, swapon отвечает на это "Operation not permitted". Проверьте через systemd-detect-virt: при kvm, qemu или none работает собственное ядро и swap возможен. В контейнере поможет только увеличение оперативной памяти или переход на полноценную виртуализацию.
Означает ли vm.swappiness=0, что вытеснения в подкачку больше не будет?
Нет. Начиная с ядра 3.5 это значение запрещает только упреждающее вытеснение. Когда памяти становится мало, ядро всё равно уходит в подкачку, прежде чем завершить процесс. Тот, кто хочет действительно отключить swap, должен использовать swapoff. Диапазон значений на всех актуальных ядрах Debian и Ubuntu идёт от 0 до 200, значение по умолчанию везде 60.

Linux Swap устранение неполадок Debian Ubuntu администрирование серверов