Swap - это не дополнительная память. Это место, куда ядро откладывает страницы, которые, по его мнению, вам не нужны, чтобы занимаемую ими память можно было использовать под что-то другое. Когда он работает, вы его не замечаете. Когда машине действительно не хватает памяти, swap вас не спасает: он лишь оттягивает момент, когда вы об этом узнаете, а процесс, который погибнет, выбираете не вы, а ядро.
На небольших серверах ломается одно из двух. Первое - полное отсутствие swap, из-за чего killer нехватки памяти приходит без предупреждения. Второе - много swap и нагрузка, которая трогает всю свою память: быстрая машина превращается в непригодную к работе, и при этом ничего не падает. В этой статье: сколько swap дать VDS, какие настройки реально работают, как принимает решение OOM killer и как прочитать строку, которую он оставляет в логе, чтобы чинить именно то, что нужно.
Для чего на самом деле нужен swap#
Linux делит используемую память на два вида. Page cache привязан к файлам: это копии того, что лежит на диске, которые держат в памяти, потому что повторное чтение бесплатно. Его можно сбросить в любой момент, поэтому free -h на здоровом сервере показывает почти нулевую свободную память, и это не проблема. Анонимная память - это всё остальное: heap, стеки, реальные рабочие данные ваших процессов. Файла за ней нет, поэтому просто сбросить её нельзя. Если ядро хочет её вернуть, ему нужно куда-то её положить, и это место - swap.
Так что swap даёт одно конкретное: возможность вытеснять холодную анонимную память. Это важнее, чем кажется. Долго работающий сервер накапливает память, которая была выделена и больше никогда не трогалась, - буферы инициализации, библиотеку, подгруженную ради функции, которой никто не пользуется, путь логирования, пройденный один раз при запуске. На машине без swap каждый байт этого месяцами сидит в RAM. С небольшим swap ядро тихо выгружает это и отдаёт место под page cache, а чтение с диска ускоряется.
Чего swap не даёт - это ёмкости. Если вашим процессам действительно нужно 10 ГБ живого рабочего набора на машине с 8 ГБ, swap превращает быстрый отказ из-за нехватки памяти в медленный и мучительный. Обычно это хуже, потому что падение заметно, а машина, работающая на десятой части скорости, - нет.
| Ситуация | Без swap | С 2 ГБ swap |
|---|---|---|
| Холодные страницы после запуска | Вечно в RAM | Вытеснены, RAM переиспользована под кэш |
| Кратковременный всплеск выделения | OOM kill | Поглощён, небольшое замедление |
| Рабочий набор больше RAM | Быстрый OOM kill | Thrashing, а затем всё равно OOM kill |
| Утечка памяти в течение дней | Kill в предсказуемый момент | Kill позже, после часов тормозов |
Сколько swap и как его добавить#
Старое правило «вдвое больше RAM» родом из эпохи гибернации и машин на 256 МБ. Для сервера, который не уходит в гибернацию, полезный объём swap невелик и примерно постоянен.
| RAM | Swap | Обоснование |
|---|---|---|
| 1-2 ГБ | 1-2 ГБ | Реальный запас на машине такого размера |
| 4-8 ГБ | 2 ГБ | Достаточно, чтобы вытеснять холодные страницы, но мало, чтобы долго thrashing |
| 16-24 ГБ | 2-4 ГБ | Та же задача; больше только отсрочит неизбежное |
| База данных или нагрузка на JVM | 1-2 ГБ | Намеренно слишком мало, чтобы скрыть ошибку в расчёте размера |
Проверьте, что у вас уже есть. Во многих образах VDS его нет вообще.
$ swapon --show$ free -h$ cat /proc/swapsФайл подкачки на любом современном ядре так же быстр, как раздел, и бесконечно проще в изменении размера, так что используйте файл:
$ fallocate -l 2G /swapfile$ chmod 600 /swapfile$ mkswap /swapfile$ swapon /swapfile$ swapon --showЕсли fallocate создаёт файл, который ядро отказывается использовать, - на некоторых файловых системах так бывает, - запишите его медленным способом командой dd if=/dev/zero of=/swapfile bs=1M count=2048 и повторите начиная с mkswap. Затем сделайте так, чтобы он пережил перезагрузку:
/swapfile none swap sw 0 0Убрать его позже - это swapoff /swapfile, затем удалить файл и строку в fstab. swapoff сначала должен вернуть всё в RAM, поэтому при нехватке свободной памяти он завершается ошибкой, а на загруженной машине может идти долго.
Swappiness и настройка, которую понимают наоборот#
vm.swappiness - число от 0 до 100, которое задаёт, насколько сильно ядро предпочитает вытеснять анонимные страницы, а не сбрасывать page cache, когда ему нужно освободить память. По умолчанию почти во всех дистрибутивах стоит 60. Начиная с ядра 5.8 максимум равен 200, и это позволяет выразить предпочтение swapping перед возвратом кэша, а не просто баланс.
Распространённое заблуждение - считать это порогом: «уходить в swap, когда память заполнена на 60%». Это не так. Это относительное предпочтение, которое применяется только тогда, когда ядро уже освобождает память. Машина с большим количеством свободной памяти не будет использовать swap ни при каком значении.
$ cat /proc/sys/vm/swappiness60$ sysctl -w vm.swappiness=10 # until rebootvm.swappiness = 10vm.vfs_cache_pressure = 50Примените командой sysctl --system. Значение 10 разумно для сервера, на котором работают одна-две чувствительные к задержкам вещи: swap есть, холодные страницы всё равно вытесняются, но ядро сначала берётся за page cache. Значение 0 swap не отключает. Начиная с ядра 3.5 оно означает «не уходить в swap, если иначе пришлось бы делать OOM», что оправданно для хоста базы данных и плохо для машины общего назначения, потому что вы теряете вытеснение холодных страниц, ради которого всё и затевалось.
vm.vfs_cache_pressure определяет, насколько охотно ядро освобождает кэши записей каталогов и inode. Значение по умолчанию 100 нейтрально; снижение до 50 дольше держит метаданные файловой системы в памяти, что помогает серверу с множеством мелких файлов. Это настройка второго порядка, и трогать её без причины не стоит.
О vm.overcommit_memory стоит знать, но менять почти никогда не стоит. Значение по умолчанию 0 позволяет ядру принимать выделения, превышающие имеющуюся память, исходя из разумного допущения, что большинство программ просят больше, чем используют. Значение 2 вводит строгий учёт выделений относительно RAM плюс swap, умноженного на vm.overcommit_ratio, и тогда malloc честно отказывает, а не OOM killer срабатывает позже. Но это ломает много ПО, которое рассчитывает на возможность выделять с запасом, включая JVM и Redis, поэтому оставьте всё как есть, если у вас нет конкретной причины.
OOM killer и как он выбирает#
Когда ядро не может удовлетворить выделение и не может ничего освободить, оно вызывает killer нехватки памяти. Это не сбой. Это осознанное решение завершить один процесс сигналом SIGKILL, чтобы машина выжила.
Жертва выбирается по оценке. Она есть у каждого процесса, видна в /proc/<pid>/oom_score и получается в основном из того, какую долю от общего объёма памяти процесс использует. Обычно выбирается самый крупный потребитель, поэтому OOM killer так надёжно убивает именно то, что вам дорого: на сервере самый большой процесс - это ваша база данных или игровой сервер, а не утекающее задание cron, которое подтолкнуло машину за край.
Это решение можно смещать через /proc/<pid>/oom_score_adj, значения которого лежат от -1000 до 1000 и прибавляются к оценке. -1000 делает процесс практически неприкосновенным. 1000 - добровольно выдаёт его.
Есть два разных события, которые оба называют OOM, и умение их различать - самый полезный диагностический навык в этой теме:
- Глобальный OOM. Кончилась память у всей машины. Под угрозой всё, ядро выбирает жертву где угодно в системе, а в строке лога стоят
constraint=CONSTRAINT_NONEиglobal_oom. - OOM в cgroup. Одна контрольная группа упёрлась в собственный лимит, хотя у машины память ещё есть. Кандидаты - только процессы внутри этой группы. В строке лога указан путь
task_memcg. Именно это происходит с контейнером и с любым юнитом systemd, у которого заданMemoryMax=.
От этого различия зависит исправление. Глобальный OOM означает: купите больше памяти или используйте меньше. OOM в cgroup означает, что один сервис превысил лимит, который задали вы или ваш хостер, а с остальной машиной всё было в порядке.
Как прочитать убийство в логе#
Ядро пишет полный отчёт, и его стоит научиться читать, а не пролистывать.
$ dmesg -T | grep -iE "out of memory|killed process|oom-kill"$ journalctl -k -b | grep -i oom$ journalctl -k --since "1 hour ago" | grep -A20 "Out of memory"Две важные строки выглядят примерно так:
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0, global_oom,task_memcg=/system.slice/minecraft.service,task=java,pid=2841,uid=1000Out of memory: Killed process 2841 (java) total-vm:9412304kB, anon-rss:6182940kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:13248kB oom_score_adj:0Читайте anon-rss, а не total-vm. total-vm - это виртуальное адресное пространство, которое у JVM или программы на Go обычно в несколько раз больше реального потребления и почти ничего не значит. anon-rss - это анонимная память, которую процесс реально держал в RAM, и именно её нужно сравнивать с вашим лимитом. В примере процесс Java, державший около 5,9 ГБ, был убит на машине, которая не смогла найти больше.
Над этими строками ядро печатает таблицу всех процессов с их RSS, и она показывает, что ещё было в памяти в тот момент. Именно по этой таблице выясняется, что погибло не то, что выросло: скрипт бэкапа, прочитавший большой файл, или второй сервис, вдвое увеличившийся в размере, могут столкнуть самый большой процесс за край, сами никогда не становясь жертвой.
Если сообщения об OOM нет вообще, а сервис всё равно пропал, это было не ядро. Посмотрите systemctl status на код выхода юнита, а остальные причины, которые встречаются чаще, чем принято думать, разобраны в статье почему ваш игровой сервер постоянно перезапускается.
Защита нужного вам процесса#
Выбор ядра можно переопределить, и systemd - чистый способ это сделать.
[Service]OOMScoreAdjust=-500MemoryHigh=5GMemoryMax=6GMemorySwapMax=512MOOMPolicy=stopOOMScoreAdjust смещает глобальное решение об убийстве так, чтобы ядро предпочло что-то другое. MemoryHigh - мягкий лимит: при его превышении cgroup попадает под сильное давление освобождения памяти и притормаживается, но не убивается. MemoryMax - жёсткий лимит: при его превышении cgroup получает собственный OOM kill, а остальная машина остаётся нетронутой. MemorySwapMax ограничивает, какая часть этого лимита может приходиться на swap.
Шаблон, который стоит перенять, - MemoryHigh чуть ниже MemoryMax на сервисах, которым вы не доверяете. Мягкий лимит даёт процессу шанс освободить память до того, как что-то умрёт, а жёсткий означает, что вышедший из-под контроля сервис снесёт сам себя, а не всю машину. После правки выполните daemon-reload, а про остальную часть файла юнита см. сервисы systemd для ваших приложений.
В свежих Ubuntu также есть systemd-oomd - демон в пользовательском пространстве, который следит за pressure stall information и убивает cgroup раньше, чем это придётся делать ядру. Он действует раньше и предсказуемее, чем killer ядра, а причины пишет в journalctl -u systemd-oomd. Если на Ubuntu 22.04 или новее сервис умирает без сообщения ядра об OOM, загляните туда, прежде чем решать, что он упал сам.
Давление, на которое он реагирует, можно смотреть напрямую:
$ cat /proc/pressure/memorysome avg10=0.00 avg60=0.13 avg300=0.09 total=4192031full avg10=0.00 avg60=0.04 avg300=0.02 total=1104822some - доля времени, в которую хотя бы одна задача простаивала в ожидании памяти; full - доля, когда простаивало всё. Если full avg60 стабильно выше нескольких процентов, машина тратит реальное время на освобождение памяти, и это лучшее раннее предупреждение, чем когда-либо был столбец free.
Swap, задержки и JVM#
Swap и сборка мусора - плохое сочетание, и именно здесь с проблемой сталкивается большинство операторов игровых серверов.
Heap JVM - это анонимная память, которую сборщик периодически обходит. Если ядро выгрузило часть этого heap в swap, потому что она казалась холодной, следующая полная сборка должна прочитать всё обратно, страница за страницей, через устройство хранения. Сборка, которая обычно занимает 40 миллисекунд, занимает несколько секунд. На сервере Minecraft это заметное для всех онлайн замирание, и оно повторяется. Та же логика применима к любой среде выполнения с трассирующим сборщиком и к любой базе данных, держащей большой кэш внутри процесса.
Практические правила:
- Подбирайте heap так, чтобы он помещался в RAM с запасом.
-Xmxплюс внепроцессные накладные расходы JVM плюс операционная система должны с запасом укладываться в общий объём. Что представляют собой эти накладные расходы, рассказано в статье флаги JVM и версии Java для Minecraft, а расчёт размера - в статье сколько RAM нужно серверу Minecraft. - Держите swappiness низким на таких машинах.
vm.swappiness = 10или1. - Ограничивайте swap юнита через
MemorySwapMax=, а не полагайтесь на один swappiness. - Не добавляйте swap, чтобы починить слишком слабый сервер. Если heap не помещается, ответ - план побольше, а не файл подкачки побольше. Как отличить одно от другого, описано в статье когда пора сменить тариф.
Чтобы увидеть, выгружен ли конкретный процесс в swap прямо сейчас:
$ grep VmSwap /proc/2841/statusVmSwap: 412360 kB$ for p in /proc/[0-9]*; do printf "%s %s\n" "$(grep VmSwap $p/status 2>/dev/null | awk '{print $2}')" \ "$(cat $p/comm 2>/dev/null)" done | sort -rn | headЕсли у нужного вам процесса в VmSwap сотни мегабайт, это и есть ваша проблема с задержками, а swapoff -a && swapon -a немедленно вернёт всё в RAM, если у вас хватает свободной памяти.
Thrashing: когда swap делает всё только хуже#
Thrashing - это когда рабочий набор не помещается, и ядро тратит время на то, чтобы гонять одни и те же страницы туда и обратно. Симптом отличительный, стоит его один раз увидеть: load average высокий, процессор в основном простаивает, всё медленно, и ничего не упало.
$ vmstat 1 10$ iostat -x 2 # from the sysstat packageВ vmstat столбцы si и so показывают число страниц, загруженных из swap и выгруженных в него за секунду. Редкие небольшие значения - это нормально. Устойчивые сотни или тысячи при высоком wa (iowait) в столбцах процессора - это thrashing, и никакая настройка его не исправит. Машине нужно меньше работы или больше памяти.
Поэтому небольшой файл подкачки - это достоинство, а не компромисс. С 2 ГБ swap вышедший из-под контроля процесс упирается в OOM killer через несколько минут, и машина восстанавливается. С 16 ГБ swap она сначала час мучается, мониторинг срабатывает на задержки, а не на падение, и вы тратите этот час на попытки зайти по SSH на машину, которой слишком тяжело выдать вам оболочку. Как выглядят характерные формы, показано в статье как читать график нагрузки сервера.
На управляемом плане RE:NODE этот режим отказа убран намеренно. Каждый сервер работает в своём контейнере с жёстким лимитом памяти, и на этом лимите ядро останавливает контейнер, а он чисто перезапускается, а не остаётся в swap. Это противоположность настройки по умолчанию в upstream Pterodactyl, и это компромисс: вы теряете шанс, что короткий всплеск будет поглощён, и получаете сервер, который поднимается за секунды, а не такой, который формально работает и час непригоден к использованию. С процессором то же самое - жёсткое ограничение по купленной доле, так что сервер на 100% работает медленно, но не ломается. На VDS всё это остаётся вам для настройки, а именно об этом мы и говорили в этой статье. Как взвесить оба варианта всерьёз, описано в статье выбор между VDS и игровой панелью.
Стоит упомянуть и альтернативу для небольших машин: zram создаёт в RAM сжатое блочное устройство и использует его как swap. Страницы сжимаются, а не пишутся на диск, обычно до трети или половины исходного размера, так что вы получаете часть пользы swap без задержки хранилища. В Debian и Ubuntu пакет zram-tools настраивает его в несколько строк. Он действительно полезен на машине с 1-2 ГБ и почти бессмысленен выше 8 ГБ, где настоящий ответ на нехватку памяти - использовать её меньше.
FAQ#
Нужен ли серверу swap вообще?
Да, небольшой. Нулевой swap означает, что холодные анонимные страницы вечно сидят в RAM, а OOM killer приходит вообще без предупреждения. Один-два гигабайта дают ядру место, куда положить страницы, которые никто не трогает, но не дают достаточно места, чтобы час мучиться в thrashing.
Предотвращает ли больший swap OOM killer?
Только откладывая его. Killer срабатывает, когда ядро не может ничего освободить, а огромный файл подкачки означает, что до этой точки идти гораздо дольше, причём на доле скорости машины. Если вы регулярно упираетесь в OOM, решение - потреблять меньше памяти или добавить RAM.
Что делает swappiness 0?
Он говорит ядру избегать вытеснения анонимной памяти в swap, пока альтернативой не станет OOM kill. Swap он не отключает и освобождение page cache не отключает. Для обычного сервера берите 10; 0 или 1 - только на хосте, выделенном под базу данных или JVM.
Почему OOM killer убил мою базу данных, а не то, что дало утечку?
Потому что он оценивает по использованию памяти, а ваша база данных была самым большим процессом на машине. Утечка подтолкнула систему за край, а выбор жертвы сделала оценка. Используйте OOMScoreAdjust на сервисе, который хотите защитить, и MemoryMax на том, которому не доверяете.
Как понять, кончилась память у контейнера или у всей машины?
Прочитайте строку oom-kill: в dmesg. constraint=CONSTRAINT_NONE вместе с global_oom означает машину. Путь task_memcg, указывающий на конкретный slice или контейнер, без global_oom, означает, что эта cgroup упёрлась в собственный лимит, пока с хостом всё было в порядке.
Достаточно ли быстр swap на NVMe, чтобы это не имело значения?
Он намного лучше, чем на вращающихся дисках, и всё равно примерно в тысячу раз медленнее RAM. NVMe превращает swap из катастрофического в просто плохой. Он не делает swap заменой памяти и не спасает сборщик мусора, которому приходится подгружать весь свой heap обратно.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.