Кого Linux убьёт при нехватке памяти: как работает OOM Killer
В сентябре 2004 года разработчик Томас Хабетс вернулся к рабочей станции и увидел разблокированный экран: OOM Killer убил xlock. Хабетс предложил список процессов, которые ядро не должно трогать даже в самом безнадёжном случае. Патч тогда не приняли, но именно с этого началась работа над тем, чтобы ядро убивало умнее.
OOM Killer — страховка ядра на случай, когда память кончилась, а процессы продолжают её просить. Linux разрешает программам запрашивать больше виртуальной памяти, чем есть на самом деле: malloc() возвращает адрес, а реальные страницы RAM выделяются, только когда процесс начинает их трогать.
Когда свободных страниц не хватает, ядро сначала пытается освободить память: вытесняет файловые страницы из кэша, сбрасывает грязные на диск, выгружает анонимную память в swap. Если не помогло — разрешает убийство. OOM Killer срабатывает не когда память кончилась до нуля, а когда аллокатор уже не может выполнить запрос.
Поведение при выделении памяти задаёт параметр vm.overcommit_memory. По умолчанию стоит 0 — эвристика, «очевидно безумные» запросы ядро отклоняет. Режим 1 разрешает оверкоммит без проверки. Режим 2 — строгий учёт: лимит считается как CommitLimit, и malloc() возвращает NULL, когда он исчерпан.
Кого убьёт ядро, решает функция oom_badness() в файле mm/oom_kill.c. До ядра 2.6.36 эвристика была многофакторной и непредсказуемой: OOM Killer мог прихлопнуть sshd или базу данных. В 2010 году Дэвид Риентджес из Google переписал её: теперь score почти полностью зависит от памяти — складываются RSS, страницы в swap и таблицы страниц ядра.
Внутри memory cgroup «доступной памятью» считается лимит группы, так что процесс, занявший 100 МБ из 128 МБ, получит высокий score, даже если на хосте свободны гигабайты. Кандидата на смерть можно посмотреть в /proc: файлы oom_score и oom_score_adj есть у каждого процесса.
Выбор часто кажется несправедливым, потому что ядро не знает ваш бизнес. База данных, занявшая 12 ГБ, для OOM Killer логичнее sshd с парой мегабайт. В контейнерах действуют два сценария: локальный memcg OOM при достижении лимита cgroup и глобальный — при нехватке на всей ноде.
Ещё пара механизмов появилась позже. В Linux 4.6 добавили oom_reaper — поток ядра, который освобождает анонимные страницы жертвы, не дожидаясь полного завершения процесса. В 4.20 появился интерфейс PSI: он показывает не объём занятой памяти, а время, потерянное из-за нехватки ресурса.
У нас на проде OOM Killer однажды положил PostgreSQL вместо воркеров, с тех пор держим vm.overcommit_memory в режиме 2 и вручную выставляем oom_score_adj для критичных процессов. Хорошо, что в статье объяснили, что ядро убивает не самого жадного, а того, кто с его точки зрения нанёс меньше всего вреда.
Понравилась история про Томаса Хабетса и xlock — забавно, что защитить процесс от ядра тогда не разрешили, а сейчас об этом пишут в каждой статье про управление памятью. У меня на домашнем сервере OOM Killer однажды снёс базу вместо тяжёлого фонового задания, и я долго не могла понять, куда делись данные. После этой статьи стало яснее, почему жертвой стала именно она: ядро оценивает не жадность, а ценность процесса для системы. Хорошо, что в современных дистрибутивах это можно настраивать, а не полагаться только на удачу.
А обычный пользователь может как-то узнать, что компьютер убил именно OOM Killer, а не просто завис? У меня он подвисает, когда открыто много вкладок, вот и думаю, это тот самый случай или дело в чём-то другом.
А на практике чаще спасает не сам OOM Killer, а демоны вроде earlyoom или systemd-oomd, которые убивают кандидата заранее. Интересно, насколько их логика отличается от классической badness-эвристики ядра — они правда выбирают жертву точнее или это тот же механизм с другим порогом?
Любопытно, а есть ли статистика, что oom_badness реально убивает более полезный процесс, а не просто первого попавшегося под руку? Судя по комментариям, почти у всех жертвой становилась база данных или что-то важное — и спасает только ручная настройка oom_score_adj. Как-то слабо верится, что эвристика сама по себе сильно умнее случайности.
Хорошо, что ядро открытое: логику выбора жертвы можно посмотреть прямо в исходниках, в mm/oom_kill.c, а не гадать по поведению. Кто-нибудь пробовал задавать oom_score_adj через параметры systemd-сервисов или это по-прежнему только руками?
@Игорь, OOMScoreAdjust= в юнит-файле systemd позволяет задать значение прямо в параметрах сервиса, ничего не трогая руками. Но работает он только для процессов, запущенных через systemd, а демонам из скриптов или контейнеров значение всё равно приходится прописывать вручную либо оборачивать запуск во враппер. Ещё нюанс: если параметр зашит в unit, при переезде на другую систему инициализации его легко потерять, так что я бы держал это в отдельном конфиге. А ты пробовал выставлять oom_score_adj через что-то ещё, кроме systemd?
Как пользователю мне не хватает в этом сценарии хоть какого-то видимого сигнала: приложение просто молча исчезает, и понять, что дело в памяти, можно только по логам, куда обычный человек не заглядывает. Для десктопа напрашивается простое уведомление «приложение остановлено из-за нехватки памяти», как это делают некоторые окружения со своими системными сообщениями. Это сразу снимало бы половину вопросов вроде «куда делась моя вкладка». Интересно, пробовал ли кто-нибудь прикрутить такое уведомление к earlyoom или systemd-oomd?
@Светлана, спасибо за пояснение. А такое уведомление уже встроено в какую-нибудь систему или его всегда нужно настраивать самому?
Режим 2 с запретом оверкоммита выглядит надёжнее, но у него есть неприятная побочка: приложения, которые резервируют память с запасом, та же JVM или некоторые СУБД, начинают падать ещё на этапе выделения, хотя физической памяти достаточно. Получается, что приходится выбирать между риском внезапного убийства процесса и стабильностью запуска тяжёлых сервисов. Хотелось бы увидеть больше конкретных цифр, кто какие значения oom_score_adj выставляет для критичных демонов, а не только общие рассуждения.
Ещё вопрос в тему контейнеров: при нехватке памяти в cgroup ядро убивает процесс внутри этой группы, и oom_score_adj, выставленный в юните systemd на хосте, до процессов в контейнере часто не доходит. Приходится прокидывать настройку внутрь или полагаться на дефолтные значения. Кто-нибудь уже настраивал лимиты через cgroup и смотрел, кого ядро выбирает жертвой в таком сценарии?
У меня так Firefox и падал
У меня на сервере файл подкачки вообще отключён, и я думала, что при нехватке памяти тогда всё ещё страшнее. Получается, чаще всего гибнет процесс, который запросил больше всех, или важные сервисы можно как-то защитить заранее?