Если вам неудержимо хочется использовать оборудование из музея для современной разработки — статья специально для вас.

Машины должны служить а не требовать ресурсы. И автор патча l9 об этом знает.
Машины должны служить а не требовать ресурсы. И автор патча l9 об этом знает.

Эпический баг

Сейчас наверное некоторые читатели сильно удивятся:

с 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 это виртуальная машина, с ограниченным обьемом памяти. Скорее всего даже ваш корпоративный сайт крутится на виртуальной машине с 4Гб памяти.

Так что на самом деле проблема касается практически всех пользователей Linux, а не только идейных нищебродов энтузиастов, собирающих себе оборудование по музеям.

Как так получилось

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

либо команда разработки ядра Linux — поголовно некомпетентны, либо у автора контракт с рептилоидами в описании выше был упущен ряд важных нюансов.

Правда как обычно где‑то между — «особенных» среди современных разработчиков Linux действительно хватает, но ряд нюансов я все же намеренно упустил.

Опишу в какой момент проявляется этот баг:

надо долго и упорно увеличивать нагрузку на использование памяти, причем маленькими порциями и обязательно из нескольких разных процессов — чтобы OOM Killer не успел отработать.

На практике надо либо заниматься тренировкой нейросетей, либо непрерывно гонять тяжелые приложения на Java/Node (в первую очередь 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), которую запрещается перегружать даже самым хитрым приложениям, откусывающим память по килобайтам.

Небольшая демонстрация пропатченного ядра в работе:

Разумеется патч заметили и тут находится архив эпической переписки в рассылке 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.

Так вот на момент появления l9ec патча, он был включен в сборку Xanmod:

С официальной страницы с отзывами, между прочим.
С официальной страницы с отзывами, между прочим.

Но в последних 6.х версиях Xanmod его уже нет, на что есть формальная причина — появление вот этого патча, вроде как окончательно решающего проблему c зависанием:

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 и скорее всего работает прямо сейчас и у вас в системе, если конечно у вас современный линукс и MGLRU не отключен вручную.

К сожалению принцип работы MGLRU другой (см. комментарий выше про 32Гб памяти на десктопе) и тестировался его функционал тоже в другом месте:

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» в очередной раз придется заботиться о себе и своих проблемах самостоятельно.

Эта история — еще одна причина, по которой стоит использовать *BSD. Реклама.
Эта история — еще одна причина, по которой стоит использовать *BSD. Реклама.

Портирование на 6.х ядро

К сожалению автор патча l9 видимо устав бодаться с идиотами, не стал переносить свой замечательный патч в 6.х ветку ядра, решив что раз более умные ребята из Google выкатили 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.

Ниже по шагам объясняю как выполнить перенос логики патча, чтобы процедуру можно было повторить и на более новых ядрах и на «vanilla» версиях.

Скачиваем архив с 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 если MGLRU включен, отключить можно командой:

echo 0 | sudo tee /sys/kernel/mm/lru_gen/enabled

Вот тут у автора патча лежат готовые скрипты для автоматизации всего этого цирка.

Я же просто добавил строчку с отключением в /etc/rc.local.

Пруфы

Для тестов портированного патча, был взят один из моих боевых ноутбуков Lenovo Z580 2012го года выпуска, с 8Гб памяти:

На нем постоянно творится всевозможная дичь — тут пять разных операционных систем и куча проектов и инструментов для разработки на каждой.

Поэтому без особого труда были одновременно запущены:

  • PostgreSQL с реальной базой

  • MySQL тоже с реальной базой

  • Intellij Idea

  • VSCode

  • Сборка проекта на Node.js с Webpack и hot reload

  • Сборка достаточно крупного Java-проекта (~3000 исходных файлов)

  • Chromium с 20 вкладками

Напоминаю что все это на 8Гб реальной памяти и на ноутбукe.

Причем в качестве ОС в этот раз была обычная Ubuntu:

Как-то так это выглядит в действии:

Через неделю после публикации я решил пойти еще дальше и поставил пропатченное с l9 ядро на ноутбук 2007 года с 3Гб памяти. И повторил тесты с нагрузкой.

Видео тут.

Все более чем работает и пропатченное ядро замечательно отрабатывает свою пайку.

Эпилог

Можно сколько угодно стебаться с пожеланиями «купи себе наконец нормальный компьютер», скажу что намеренно и давно использую старое железо — в первую очередь для оценки производительности создаваемого ПО.

И это одна из причин, по которой у нас получаются технические чудеса вроде Телепорты.

Если вы пока не дошли до столь глубокой стадии просвещения в разработке — все равно стоит знать, что мы ловили подобные зависания и в виртуальных машинах с Linux, например на CI‑сервере при сборке нескольких проектов одновременно.

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

Ну и разумеется автору патча лучи респекта, благо это лучший представитель отечественной инженерной школы.

Оригинал, в котором автор статьи себя не сдерживал и в красках рассказал все что думает о разработчиках ядра Linux как обычно можно найти в нашем блоге.

Комментарии (24)