Swap выключили, чтобы сервер не тормозил. Теперь он минутами не отвечает

Swap на серверах принято выключать. Диск медленный, и если память кончилась, пусть лучше OOM killer быстро прибьёт виновника, чем сервер будет полчаса ползать по свопу. Kubernetes с версии 1.8 вообще отказывался стартовать на ноде с включённым swap, и swapoff -a много лет был первой строкой в любой инструкции по развёртыванию. Привычка закрепилась и разошлась по всем серверам, даже по тем, где Kubernetes никогда не было.
Без swap, как считается, ядру при нехватке памяти выгружать нечего, и оно сразу зовёт OOM killer. Между тем у ядра остаются файловые страницы — код запущенных программ, библиотеки, кэш файлов. Всё это лежит на диске, поэтому из памяти его можно выбросить и потом прочитать снова.
Сервису, который этот код выполняет, он понадобится опять почти сразу.
Отсюда и минуты между «память кончилась» и «пришёл OOM killer», в которые машина почти не отвечает. Сколько их наберётся, зависит от скорости утечки, и медленная утечка держит машину в таком состоянии дольше быстрой.
Что ядро выбрасывает без swap
Если посадить на виртуалку с 1 ГБ памяти и диском, ограниченным примерно как базовый облачный SSD, сервис на Python, который каждые 100 мс делает немного работы и раз в секунду трогает горячий файл на 64 МБ (так моделируются код больших бинарей и горячий кэш), а рядом запустить процесс, который быстро набирает около 750 МБ анонимной памяти и потом течёт по 1 МБ в секунду, то поначалу сервис тикает как часы.
Потом свободная память заканчивается, и ядро начинает освобождать место. Анонимные страницы утечки отдать некуда, потому что swap выключен, и в ход идут файловые. Ядро выбрасывает код Python, libc, libssl и кэш файлов. Через 100 мс сервис снова их трогает, и всё это приходится читать с диска заново.
swap выключен, утечка 1 МБ/с, фаза давления 78 с
файловые страницы в памяти 86 → 59 → 39 → 25 → 5 → 0,1 МБ
major faults 200–400 в секунду, в среднем ~300
повторных чтений выброшенного 4–10 тыс. в секунду
чтение с диска 17–38 МБ/с всю фазу
перечитано за фазу 2,0 ГБ при 1 ГБ RAM
swap in/out 0
тиков сервиса выполнено 3%Виртуалка работала без аппаратного ускорения, поэтому всё, что упирается в процессор, растянуто по времени. Абсолютные секунды здесь завышены, а сравнивать сценарии между собой можно.
За 78 секунд с диска перечитано 2 ГБ, хотя горячий набор сервиса укладывается в несколько десятков мегабайт, так что одни и те же страницы прочитали десятки раз.
Счётчик swap стоит на нуле, потому что swap просто нет. Всё это чтение приходится на код и горячие файлы, которые ядро выкинуло секунду назад. Сервис выполнил 3% своих тиков. Большую часть времени он ждал страницу с диска или крутился внутри освобождения памяти.
Снаружи такая машина выглядит почти зависшей. Сервис стоит, и даже запуск /bin/true растягивается до 2–5 секунд. Входу по ssh надо поднять с диска sshd, PAM и шелл, так что ему, скорее всего, ещё хуже.
OOM killer всё это время не приходит, потому что ядро объявляет OOM, только когда освобождение памяти раз за разом ничего не возвращает. Выброшенный код освобождается успешно, хоть через 100 мс его и прочитают снова. С точки зрения ядра прогресс есть, просто полезной работы в нём почти нет.
На сервере это видно по трём счётчикам:
vmstat 1 # bi в КиБ/с: 10000–40000, si и so — нули
grep -E 'pgmajfault|workingset_refault_file' /proc/vmstat
cat /proc/pressure/memoryБольшое чтение с диска при нулевом свопинге, сотни major faults в секунду и быстро растущий workingset_refault_file вместе означают перечитывание выброшенного кода.
Обычная нагрузка на диск выглядит иначе. Там растёт bo или с диска читается что‑то новое.
Смотреть на эти счётчики лучше заранее, в мониторинге, потому что, когда /bin/true запускается по нескольку секунд, до консоли можно и не добраться.
Чем медленнее утечка, тем дольше зависание
Я рассчитывал, что машина в таком состоянии повиснет навсегда, но OOM killer в итоге приходил во всех прогонах без swap. Зато время до его прихода зависит от скорости утечки, и дольше всего ждать пришлось при самой медленной.
swap выключен фаза давления самая длинная пауза тиков сервиса
утечка 20 МБ/с 13 с 9,9 с 28%
утечка 1 МБ/с 78 с 6,7 с 3%
утечка 1 МБ/с, повтор 75 с 7,6 с 7%
утечка 0,25 МБ/с 273 с 6,4 с 4%При быстрой утечке память кончается резко и файловых страниц быстро не остаётся. Ядро сдаётся за 13 секунд.
При медленной ядро добывает каждую новую порцию памяти, выбрасывая ещё чуть‑чуть кода, и такое равновесие держится долго.
На 0,25 МБ/с сервис 4,5 минуты почти не работал, за это время набралось 99 пауз дольше секунды, а с диска перечитали 6,7 ГБ. Быстрый OOM, ради которого swap выключают, получается только при быстрой утечке.
А медленная утечка — дело обычное. Так текут кэш без лимита, растущая очередь, сессии, которые никто не чистит.
Даже быстрая утечка не гарантирует быстрого OOM, когда течёт сам сервис. На 20 МБ/с у него вышло 123 секунды давления, 3% тиков и 3 ГБ перечитанного с диска. OOM killer в конце убил сам сервис, как и должен был, только на две минуты позже, чем все ждали.
Выбор, который даёт swap
Включаем swap‑файл на 1 ГБ, vm.swappiness оставляем по умолчанию, 60, и повторяем те же сценарии.
swap-файл 1 ГБ фаза давления самая длинная пауза тиков сервиса перечитано
утечка 20 МБ/с 60 с 8,6 с 74% 245 МБ
утечка 1 МБ/с нет за 600 с 1,7 с 100% 26 МБПри медленной утечке сервис 10 минут работал как ни в чём не бывало, с p99 тика 102 мс и одной паузой 1,7 секунды в самом начале давления.
Утечка спокойно уезжала в swap мегабайт за мегабайтом. Ядро отдало туда холодные страницы утечки, которые никто не трогал, и оставило в памяти горячий код сервиса.
За 10 минут с диска перечитали 26 МБ, тогда как без swap за 78 секунд набралось 2 ГБ.
Swap даёт ядру место, куда можно выгрузить анонимные страницы, которые давно никто не трогал. Без него выбор у ядра один — выбрасывать файловые страницы, даже горячие. Со swap оно выбирает, что выгоднее выбросить, и чаще всего выбирает правильно. Дисковый ввод‑вывод без swap никуда не исчезает, он превращается в бесконечное перечитывание кода.
Со swap OOM приходит позже, но приходит. При быстрой утечке фаза давления длилась 60 секунд вместо 13, пока swap не заполнился, с паузой 8,6 секунды в момент, когда начался свопинг, и 6,7 секунды перед OOM.
Если сервис течёт, его всё равно убьют, просто позже и, если утечка медленная, с нормальной работой до самого конца. Поэтому для сервиса, который течёт медленно, даже swap‑файл в пару гигабайт на SSD означает спокойную работу до момента, когда процесс всё‑таки придётся перезапустить, и время увидеть рост памяти на графике раньше, чем его увидят пользователи.
С zram, сжатым swap в памяти на 512 МБ, при медленной утечке паузы вышли длиннее, чем в любом другом прогоне.
У сервиса набралось 37,5 секунды, а запуск /bin/true растягивался до 33,8 секунды. В остальное время сервис работал и выполнил 72% тиков против 3% без swap.
Сжатие ест процессор. А процессор в моей виртуалке был в 20 раз медленнее настоящего. На рабочем железе цифры будут скромнее, но zram на маленькой машине с медленным процессором — не бесплатная замена swap на диске.
swappiness=0 на новом ядре — тот же сервер без swap
Частый компромисс звучит так: «swap оставим, но поставим vm.swappiness=0, чтобы ядро его не трогало».
Значение задаёт относительную цену выгрузки анонимной памяти против файловой, от 0 до 200.
По документации ядра при 0 старый механизм выбора страниц не начнёт свопить, пока свободные и файловые страницы не опустятся ниже high watermark, значит, начнёт в последний момент.
В MGLRU, который сейчас включён по умолчанию в большинстве дистрибутивов, при 0 вытесняются только файловые страницы. По коду mm/vmscan.c анонимная память в обычном освобождении памяти не трогается вообще, и получается тот же сервер без swap, с которого началась статья.
Минимум, если хочется «почти не свопить», — 1.
sysctl vm.swappiness # 60 по умолчанию
cat /sys/kernel/mm/lru_gen/enabled
cat /sys/kernel/mm/lru_gen/min_ttl_msВторая и третья строки относятся к MGLRU, новому механизму выбора страниц для выгрузки.
Он появился в Linux 6.1 в декабре 2022 и включён по умолчанию в Ubuntu с 23.10, в Fedora, Arch и Debian с 13-й версии. В Debian 12 и Amazon Linux 2023 он собран, но выключен.
Сам по себе MGLRU от такого зависания не спасает. Со старым механизмом и с новым фаза давления вышла одинаковой, 12 против 13 секунд и 91 против 78, в пределах разброса.
Поможет другая его настройка, по умолчанию выключенная. min_ttl_ms описан в документации ядра в разделе «Thrashing prevention». С ним ядро не выбрасывает рабочий набор младше N миллисекунд, а если сохранить его не получается, вызывает OOM killer.
Документация рекомендует 1000:
echo 1000 | sudo tee /sys/kernel/mm/lru_gen/min_ttl_ms # сбрасывается при перезагрузкеС ним фаза давления сократилась до 5–7 секунд вместо 13 и 78, и утечку убили на 60 МБ раньше. Одна пауза сервиса на 6–11 секунд перед самым убийством всё равно осталась.
Работает настройка только при включённом MGLRU. На Debian 12 и AL2023 файл есть, но толку от него нет.
Документация адресует её ноутбукам и десктопам, где нет userspace‑убийцы, но на сервере без swap она работает так же, и на таких серверах с включённым MGLRU значение 1000 имеет смысл выставить сразу.
Убить раньше ядра
Спокойнее всего сервер пережил утечку с earlyoom на параметрах по умолчанию. Он до 10 раз в секунду смотрит на доступную память и убивает процесс с наибольшим oom_score, не дожидаясь, пока ядро начнёт выбрасывать код.
earlyoom, swap выключен, утечка 20 МБ/с
убил утечку при 88 из 960 МиБ доступной памяти (9,2%)
самая длинная пауза сервиса 0,1 с
major faults у сервиса 0
перечитано с диска 0Убит тот же процесс, которого секунд через пятнадцать‑двадцать убил бы OOM killer, только до того, как сервер встал.
На сервере, где swap выключен и включать его не хотят, earlyoom или systemd‑oomd на своих службах закрывают те минуты между «память кончилась» и «OOM пришёл», которые при медленной утечке сервер проводит без ответа.
С systemd‑oomd так просто не выйдет, хотя в Fedora он стоит по умолчанию с версии 34.
По давлению памяти он по умолчанию следит только за пользовательскими сессиями, а для всей системы реагирует на заполнение swap. Служба на сервере без swap этими настройками не покрыта, и ей нужно отдельно повесить ManagedOOMMemoryPressure=kill на юнит или slice.
В Ubuntu 22.04 systemd‑oomd включили только в Desktop‑версии.
Мерить давление лучше по cgroup сервиса, чем по машине целиком, потому что системный PSI в моих прогонах обманывал в обе стороны.
Без swap сервис выполнил 3% тиков, а системный PSI за 78 секунд набрал всего 7,7 секунды ожидания. Со swap на медленном диске PSI показывал до 88%, хотя сервис был в полном порядке.
Ждала, похоже, сама утечка. Возможно, часть этого объясняется медленной виртуалкой. На железе я это не проверял, как и сам PSI по отдельным cgroup.
Базы данных и Kubernetes про swap
Из трёх систем, чью документацию я посмотрел, совет «под базу swap выключать» верен только для поискового движка.
Elasticsearch действительно рекомендует полностью отключить swap или выставить vm.swappiness=1, или держать память процесса через bootstrap.memory_lock, потому что у него сборка мусора на свопе может растянуться с миллисекунд до минут.
Redis в документации по администрированию советует обратное, держать swap включённым и размером с оперативную память. Без него при перерасходе памяти Redis упадёт или его убьёт OOM killer, а со swap будут хотя бы видны всплески задержек.
PostgreSQL 18 в разделе про overcommit тоже предлагает увеличить swap, потому что OOM killer вызывается, только когда исчерпаны и память, и swap.
В самом Kubernetes функция NodeSwap стала стабильной в версии 1.34 от 27 августа 2025 года. Режим по умолчанию NoSwap. В режиме LimitedSwap swap получают только поды класса Burstable, пропорционально своему запросу памяти.
Kubelet по умолчанию всё ещё не стартует на ноде со swap, и разрешать это надо явно:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
failSwapOn: false
memorySwap:
swapBehavior: LimitedSwapGKE с версии 1.34.1 даёт включить swap на нодах отдельной настройкой.
Привычка swapoff -a выросла из требований старого Kubernetes и к 2026 году пережила сам запрет. Swap на нодах теперь поддерживается официально. Его достаточно явно разрешить в конфиге kubelet.
Контейнер с memory.high так и не дожидается OOM
На машине целиком глобальный OOM killer в итоге приходил. Внутри cgroup с memory.high зависание получилось уже без него. Сервис и утечка сидят в одной группе, memory.high 500 МБ, memory.max 600 МБ, swap выключен:
memory.high=500M, memory.max=600M, swap выключен
к 30-й секунде утечка дошла до 490 МБ, за следующие 570 с прибавила 3
тиков сервиса за 577 с 0
перечитано с диска 7,75 ГБ
memory.events high 557150 max 0 oom 0
снаружи MemAvailable ~317 МБ, /bin/true за 100 мсИспользование памяти упёрлось в memory.high и до memory.max не дошло, поэтому OOM не случился ни разу.
Документация cgroup v2 так это поведение и описывает. Превышение memory.high «never invokes the OOM killer», процессы группы тормозят и сидят под давлением освобождения памяти, а разгружать их должен внешний процесс.
В Kubernetes 1.37 MemoryQoS перешёл в бету и включён по умолчанию, но memory.high kubelet ставит, только если в конфиге задан memoryThrottlingFactor, например 0.9. Тогда контейнеры Burstable и BestEffort без swap попадают в тот же сценарий, упираются в memory.high и тормозят без OOM.
История здесь та же, что и с машиной целиком. Без swap ядро добывает память, выбрасывая код, который тут же понадобится снова, и тянется это до тех пор, пока кто‑нибудь не решит, кого убить.
Группе с memory.high решать было некому. Кроме кода, освободить там было нечего, и группа 10 минут, пока я её не убил, перечитывала его с диска.
OOM за все 600 секунд так и не пришёл. Утечка за последние 570 секунд прибавила 3 МБ, сервис за 577 секунд не выполнил ни одного тика, с диска перечитано 7,75 ГБ.
Снаружи узел всё это время выглядел здоровым: треть гигабайта памяти свободна, /bin/true отрабатывает за 100 мс.
А вам попадались контейнеры, которые упёрлись в
memory.highи тормозили без единого OOM?

Когда сервер начинает тормозить из‑за нехватки памяти, недостаточно просто увеличить ресурсы или дождаться OOM killer. Важно понимать, как ядро управляет памятью, какие параметры влияют на поведение системы и как вовремя обнаружить деградацию производительности.
Продолжить разбираться в настройке ядра и мониторинге Linux можно на бесплатных вебинарах:
24 ноября в 20:00. «sysfs на практике: как управлять параметрами модулей ядра». Записаться
30 ноября в 20:00. «Grafana на практике: от настройки дашбордов до автоматизации». Записаться
Еще больше материалов для углублённого изучения технологий и практик системного администрирования ищите в дайджесте.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.