l9ec: волшебный патч нищебродов
В этой истории прекрасно абсолютно все: масштаб проблемы, решения — одно п#зданутее другого и эпилог в виде текущего состояние дел.
Гордость и предубеждение, п#дорство и долбо#бизм и все это в разработке ядра Linux. Все как вы любите.
Эпический баг
Сейчас наверное некоторые читатели сильно ох#еют удивятся:
с 2007 года в ядре Linux живет серьезный баг, приводящий к полному зависанию системы при работе под большой нагрузкой на память.
На дворе на момент написания статьи — май 2025 года, так что баг уже успел отпраздновать совершеннолетие.
Разумеется разработчики ядра в курсе проблемы, но по ряду причин.. не считают этот баг важным.
«полное зависание системы под нагрузкой» и «разработчики не считают важным исправлять» — как вам такие реалии Linux?
Более того, недавно тикет с описанием бага вообще закрыли с эпической формулировкой «just become obsolete»:
С легким намеком, что некоторым пользователям стоит перестать собирать компьютеры по помойкам:
but now I don't bother with less than 32Gb of RAM for a desktop.
Теперь прокрутите обсуждение вниз и посмотрите на последнее сообщение о проблеме:
Оно конечно все замечательно, у самого автора давно 64Гб на одной из рабочих машин, а некоторые из коллег успели впихнуть и 128Гб, причем в ноутбук — мы наконец увидели SUSE Linux, которая не тормозит.
Но дело в том что на нынешние облачные времена типичное рабочее окружение Linux это внезапно виртуальная машина, с ограниченными ресурсами разумеется.
VDS — виртуальные серверы, где обычно никаких 32Гб памяти не бывает.
Зуб даю что даже ваш корпоративный сайт крутится на виртуальной машине с 4Гб памяти максимум.
Так что все описанное касается практически всех пользователей Linux, а не только идейных нищебродов, собирающих себе оборудование по помойкам.
Как так получилось
Если вы хоть немного понимаете в компьютерах, прочитав абзац выше и сопоставив масштаб проблемы и отношение к ней разработчиков Linux, уже сделали определенные выводы:
либо команда разработки ядра Linux — поголовно криворукие п#дорасы, либоу автора контракт с рептилоидамив описании выше был упущен ряд важных нюансов.
Правда как обычно где-то между — п#дорасов среди разработчиков Linux действительно хватает, но ряд нюансов я все же упустил.
Опишу в какой момент проявляется этот баг:
надо долго и упорно увеличивать нагрузку на использование памяти (в течение минимум часа), причем маленькими порциями и обязательно из нескольких разных процессов — чтобы OOM Killer не успел отработать.
На практике надо либо заниматься тренировкой нейросетей либо непрерывно гонять тяжелые приложения на Java (в первую очередь IDE) и постоянно запускать сборку больших проектов.
И все это на неподготовленном офисном оборудовании с 4-6 Гб памяти, либо в виртуальной машине.
Патч l9ec
Уже очень давно существует неофициальный патч, решающий описанную проблему с зависанием радикальным способом:
The kernel does not provide a way to protect the working set under memory pressure. A certain amount of anonymous and clean file pages is required by the userspace for normal operation. First of all, the userspace needs a cache of shared libraries and executable binaries. If the amount of the clean file pages falls below a certain level, then thrashing and even livelock can take place.
По сути, этим патчем формируется небольшой объем памяти (тот самый working set
), которую запрещается перегружать даже самым хитровы#банным приложениям, откусывающим память по 1Кб.
Небольшая демонстрация пропатченного ядра в работе:
Разумеется патч заметили, тут находится архив эпической переписки в рассылке Linux Kernel длиною в год, где автор пытается объяснить окружающим что он не верблюд и проблема действительно есть.
Хвала его терпению, год объяснять долбое#бам от разработки что проблема действительно есть — надо уметь.
Но разумеется патч в мейнстрим так и не попал.
История с Xanmod
Помимо основной версии ядра т. н. «vanilla», исходники которого забираются с известного kernel.org, существуют «васянские сборки» — специальные наборы патчей ядра, собранные энтузиастами под ту или иную задачу.
Одна из таких сборок называется Xanmod и посвящена работе современного ядра на desktop-системе с минимальными визуальными задержками:
XanMod is a general-purpose Linux kernel distribution with custom settings and new features. Built to provide a stable, smooth and solid system experience.
К сожалению с недавних времен автор и главный глиномес ментейнер Xanmod немного поехал кукухой, поэтому доступ к сайту закрыт с территории РФ.
Хотя если для вас это является проблемой — думаю заниматься вопросами кастомных сборок ядра Linux вам пока рановато.
Вообщем на момент появления l9ec патча, он был включен в сборку Xanmod:
Но в последних 6.х ядрах патча уже нет, на что также есть формальная причина — появление вот этого патча, вроде как окончательно решающего проблему:
MGLRU is a kernel innovation we've been eager to see merged in 2022 and it looks like that could happen for the next cycle, v5.19, for improving Linux system performance especially in cases of approaching memory pressure.
На данный момент MGLRU уже давно в mainline ядра и работает прямо сейчас и у вас (если у вас современный линукс).
К сожалению это Phoronix эта хня не работает, потому как принцип работы MGLRU сильно другой и тестировался этот функционал также в другом месте:
On Android, our most advanced simulation that generates memory pressure from realistic user behavior shows 18% fewer low-memory kills, which in turn reduces cold starts by 16%.
Как нетрудно догадаться, «realistic user behavior» на мобильном Android несколько отличается от тотальной перегрузки тяжелыми средствами разработки на полудохлом десктопе или еще более слабой виртуальной машине.
Поэтому «продвинутым пользователям Linux» в очередной раз придется заботиться о своих проблемах самостоятельно.
Портирование на 6.х ядро
К сожалению автор патча l9 видимо устав бодаться с идиотами в одиночку, не стал переносить свой замечательный патч в 6.х ядро, решив что раз более умные ребята из Гугла выкатили MGLRU — от его решения толку больше не будет.
Как ни странно, но это не так и l9 патч куда более предсказуем и надежен как удар ломом, в отличие от всего цирка с 14 патчами MGLRU:
These initial multi-generational LRU patches amount to 14 patches at the moment and in a patched kernel can be enabled via the LRU_GEN Kconfig switch
Собственно эта статья появилась на свет, когда автор опять получил зависание под нагрузкой во время работы над реальным проектом, из-за чего решил откопать дедовский пулемет портировать известный патч в 6.х ядро.
За основу был взят последний патч для 5.х ветки без учета MGLRU: le9ec-5.15.patch а добавлялся он в Xanmod версию ядра 6.14.5.
Скачиваем архив с Xanmod ядром и l9 патч по ссылкам выше и распаковываем.
Стоит сразу предупредить, размер текущей версии ядра Linux в распакованном виде ~1.8 Гигабайт, а для сборки понадобится еще ~28 Гигабайт.
Разумеется применить готовый diff
автоматически для ветки 6.х не получится, так что будем переносить логику патча по шагам.
Всего в рамках патча изменения происходят в пяти файлах:
Поскольку исправлять документацию нам не очень актуально, первый файл можно пропустить.
Таким образом первое актуальное исправление находится в файле include/linux/mm.h
, куда добавляются глобальные переменные, отвечающие за настраиваемые лимиты:
Все что нужно сделать — вставить строки в файл include/linux/mm.h
:
extern unsigned long sysctl_anon_min_kbytes; extern unsigned long sysctl_clean_low_kbytes; extern unsigned long sysctl_clean_min_kbytes;
Необходимо найти массив static struct ctl_table vm_table[]
в файле kernel/sysctl.c
и добавить внутрь три блока, отвечающих за настройку.
{ .procname = "anon_min_kbytes", .data = &sysctl_anon_min_kbytes, .maxlen = sizeof(unsigned long), .mode = 0644, .proc_handler = proc_doulongvec_minmax, }, { .procname = "clean_low_kbytes", .data = &sysctl_clean_low_kbytes, .maxlen = sizeof(unsigned long), .mode = 0644, .proc_handler = proc_doulongvec_minmax, }, { .procname = "clean_min_kbytes", .data = &sysctl_clean_min_kbytes, .maxlen = sizeof(unsigned long), .mode = 0644, .proc_handler = proc_doulongvec_minmax, },
Следующая правка в файле mm/Kconfig
, которой добавляется управление новыми настраиваемыми параметрами ядра:
По-сути правки, вам надо добавить в файл mm/Kconfig
три блока: ANON_MIN_KBYTES
, CLEAN_LOW_KBYTES
и CLEAN_MIN_KBYTES
вместе со всем содержимым.
Все что выше отвечало лишь за настройку, основная логика патча l9 приходится на файл mm/vmscan.c
, в котором будут происходить оставшиеся правки.
Первым делом добавляем локальные переменные:
Затем добавляем логику присваивания значений из параметров ядра:
Ориентируетесь на макрос #define prefetchw_prev_lru_folio
, строки добавляются после него:
unsigned long sysctl_anon_min_kbytes __read_mostly = CONFIG_ANON_MIN_KBYTES; unsigned long sysctl_clean_low_kbytes __read_mostly = CONFIG_CLEAN_LOW_KBYTES; unsigned long sysctl_clean_min_kbytes __read_mostly = CONFIG_CLEAN_MIN_KBYTES;
Следующая правка добавляется в метод static void get_scan_count
который успел поменять сигнатуру:
Я добавил сразу после блока с переменными:
struct pglist_data *pgdat = lruvec_pgdat(lruvec); struct mem_cgroup *memcg = lruvec_memcg(lruvec); unsigned long anon_cost, file_cost, total_cost; int swappiness = sc_swappiness(sc, memcg); u64 fraction[ANON_AND_FILE]; u64 denominator = 0; /* gcc */ enum scan_balance scan_balance; unsigned long ap, fp; enum lru_list lru; /* * Force-scan anon if clean file pages is under vm.clean_low_kbytes * or vm.clean_min_kbytes. */ if (sc->clean_below_low || sc->clean_below_min) { scan_balance = SCAN_ANON; goto out; }
Следующая правка в этом же файле должна быть вставлена в этот же метод get_scan_count
, но ниже по коду — ориентируйтесь на строку nr[lru] = scan;
благо она такая одна:
Я вставил логику проверки сразу над ней:
/* * Hard protection of the working set. */ if (file) { /* * Don't reclaim file pages when the amount of * clean file pages is below vm.clean_min_kbytes. */ if (sc->clean_below_min) scan = 0; } else { /* * Don't reclaim anonymous pages when their * amount is below vm.anon_min_kbytes. */ if (sc->anon_below_min) scan = 0; } nr[lru] = scan;
Следующей правкой добавляется новая функция prepare_workingset_protection
, которая должна вызываться из существующего метода shrink_node_memcgs
:
Так что вам надо найти функцию shrink_node_memcgs
(она такая одна) и вставить новую функцию prepare_workingset_protection
над ней:
static void prepare_workingset_protection(pg_data_t *pgdat, struct scan_control *sc) { /* * Check the number of anonymous pages to protect them from * reclaiming if their amount is below the specified. */ if (sysctl_anon_min_kbytes) { unsigned long reclaimable_anon; reclaimable_anon = node_page_state(pgdat, NR_ACTIVE_ANON) + node_page_state(pgdat, NR_INACTIVE_ANON) + node_page_state(pgdat, NR_ISOLATED_ANON); reclaimable_anon <<= (PAGE_SHIFT - 10); sc->anon_below_min = reclaimable_anon < sysctl_anon_min_kbytes; } else sc->anon_below_min = 0; /* * Check the number of clean file pages to protect them from * reclaiming if their amount is below the specified. */ if (sysctl_clean_low_kbytes || sysctl_clean_min_kbytes) { unsigned long reclaimable_file, dirty, clean; reclaimable_file = node_page_state(pgdat, NR_ACTIVE_FILE) + node_page_state(pgdat, NR_INACTIVE_FILE) + node_page_state(pgdat, NR_ISOLATED_FILE); dirty = node_page_state(pgdat, NR_FILE_DIRTY); /* * node_page_state() sum can go out of sync since * all the values are not read at once. */ if (likely(reclaimable_file > dirty)) clean = (reclaimable_file - dirty) << (PAGE_SHIFT - 10); else clean = 0; sc->clean_below_low = clean < sysctl_clean_low_kbytes; sc->clean_below_min = clean < sysctl_clean_min_kbytes; } else { sc->clean_below_low = 0; sc->clean_below_min = 0; } }
Собственно последняя правка это вызов новой функции из существующей shrink_node_memcgs
:
После внесения всех этих исправлений, запускаем один из вариантов настройки ядра:
make xconfig
и наблюдаем новые поля настройки:
Цепочка сборки и установки ядра совершенно стандартная:
make && make modules && make modules_install && make install
К сожалению это еще не все, прежде чем патч заработает — вам надо будет отключить зло#бучий MGLRU, который успели внести в основную ветку ядра:
cat /sys/kernel/mm/lru_gen/enabled
Должен показать 0x0007
если он включен, отключить можно командой:
echo 0 | sudo tee /sys/kernel/mm/lru_gen/enabled
Вот тут у автора патча лежат готовые скрипты для всего этого цирка.
Пруфы
Для тестов портированного патча, был взят один из моих боевых ноутбуков Lenovo Z580 2012го года выпуска, с 8Гб памяти.
На нем постоянно творится всевозможная дичь — тут пять разных операционных систем и куча проектов и инструментов разработки в каждой.
Поэтому без особого труда были одновременно запущены:
- PostgreSQL с реальной базой
- MySQL тоже с реальной базой
- Intellij Idea
- VSCode
- Сборка проекта на Node.js с Webpack
- Сборка достаточно крупного Java-проекта (~3000 исходных файлов)
- Chromium с 20 вкладками
Напоминаю что все это на 8Гб реальной памяти, на ноутбуке да. Причем в качестве ОС в этот раз была самая обычная Ubuntu:
Как-то так это выглядело в действии:
Эпилог
Можете сколько угодно стебаться в стиле Phoronix с пожеланиями «купи себе наконец нормальный компьютер за 500 баксов», скажу что намеренно и давно использую старое железо, в первую очередь для оценки производительности создаваемого ПО.
И это одна из причин, по которой у нас получаются технические чудеса вроде Телепорты.
Если вы пока не дошли до столь глубокой стадии просвещения, то вам все равно стоит знать, что мы ловили подобные зависания на VDS, например на CI-сервере при сборке нескольких проектов одновременно.
Так что актуальность описанного все также высокая и как получилось, что столь простой и очевидный патч до сих пор не используют активно — ума не приложу.