14. Тюнинг ядра#

Предупреждение

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

14.1. Введение#

Как и любая другая программа, ядро имеет свои собственные настройки и параметры, которые контролируют поведение определенных его частей. И хотя настройки ядра Linux являются менее очевидными для понимания и часто "скрыты" с глаз обычных пользователей, в данном разделе мы рассмотрим различные настройки ядра с целью улучшения производительности и отзывчивости системы для домашнего ПК или ноутбука, подобрав наиболее оптимальные значения в зависимости от вашей конфигурации.

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

Стоит также отметить, что так как ядро является общей составляющей всех дистрибутивов Linux, то почти вся информация которая будет представлена в этом разделе применима не только к Arch Linux, но и к другим дистрибутивам. Учтите, что некоторые параметры зависят от вашей версии ядра, которая, по понятным причинам, от одного дистрибутива к другому может отличаться, поэтому обращайте внимание на примечания, где указано с какой версии появился тот или иной параметр. Узнать версию используемого вами ядра можно через команду uname -r.

14.1.1. Зачем?#

Часто люди задаются вопросом, зачем пытаться лезть под капот, когда "очевидно", что все уже настроено и отполировано за тебя. Отчасти это правда, и ядро Linux с каждой версией улучшается и "вылизывается" тысячами разработчиками по всему миру, и, наверное, параметры о которых пойдет речь далее уже имеют наиболее оптимальные значения. Но к сожалению, это не совсем так. Или скорее совсем не так. Во-первых, разработчики ядра часто не могут иметь представления о том, на каком конкретном железе будет работать ядро и для каких целей оно будет использоваться, в следствии чего главным приоритетом при разработке является совместимость и адаптивность ядра к как можно большему числу возможных задач и конфигураций. Такой компромиссный подход к разработке не всегда дает наилучшие результаты в чем-то конкретном, но зато позволяет ядру Linux одинаково подходить как для работы на серверах, роутерах, телефонах, микроконтроллерах, так и простых ПК. Грубо говоря, ядро Linux представляет собой швейцарский нож от мира IT, которым хоть и можно порезать хлеб, но удобнее это сделать обычным ножом. Наша задача в данном разделе это как раз заточить ядро под конкретную задачу, в нашем случае это интерактивное использование на домашнем компьютере.

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

14.1.2. Виды настроек#

В ядре Linux все параметры можно поделить на типы в зависимости от способа установки их значения [1]. Часть из них может быть установлена только на этапе загрузки ядра, то есть в качестве опций командой строки [2]. Это то, что мы обычно пониманием под просто "параметрами ядра". Они указываются в настройках вашего загрузчика, будь то GRUB, refind или systemd-boot. В разделе Настройка параметров ядра мы уже упоминали некоторые из них и прописывали их в конфигурационном файле загрузчика GRUB. К этой же категории можно отнести параметры модулей ядра, значения к которым передаются во время их загрузки. Правда они тоже могут быть переданы как параметры ядра в конфиге вашего загрузчика при их указании в следующем формате: module.parameter=value (например nvidia-drm.modeset=1). То есть сначала указывается имя модуля (драйвера), затем имя параметра и через знак равно передается значение.

14.1.2.1. sysctl#

Другой тип, это параметры, значение которых можно изменить прямо во время работы системы, что называется "на лету". Такие настройки представлены в виде файлов на псевдофайловой системе procfs в директории /proc/sys [3]. procfs называется псевдофайловой потому, что физически она не расположена на диске, а все файлы и директории создаются самим ядром при запуске системы в оперативной памяти. По этой причине у них отсутствует размер, и все они имеют чисто служебный характер. В директории /proc/sys/ каждая настройка это отдельный файл, куда мы должны просто передать (записать) значение в виде числа (часто 1 и 0 означают включить/выключить, но некоторые параметры также могут принимать значение, которое имеет строго определенный смысл, и только из определенного диапазона). Все настройки объедены в директории, которые характеризуют их отношение к чему-то общему. Например, все файлы в подкаталоге vm - это настройки для механизма виртуальной памяти ядра [4] , включая настройки для подкачки, кэширования, и т. д. В kernel - общие настройки ядра [5], а в net [6] - настройки сетевой подсистемы, протоколов TCP и IP. Именно эти три категории мы и будем рассматривать далее.

Конечно, процесс поиска всех файлов-настроек и записи значений средствами командой строки каждый раз весьма утомителен. Поэтому разработчики создали специальную утилиту под названием sysctl, которая значительно упрощает данный процесс. Теперь нам не нужно лазить каждый раз в /proc/sys/, чтобы изменить значение параметров. Вместо этого достаточно прописать в терминале:

sysctl -w kernel.sysrq=1

Это то же самое, что и данная команда, которая прописывает значение напрямую в файл из директории /proc/sys/:

echo "1" > /proc/sys/kernel/sysrq

Обратите внимание, что для изменения настроек всегда нужны права root, поэтому перед каждой такой командой мы должны добавить sudo:

sudo sysctl -w kernel.sysrq=1

Другим преимуществом sysctl является то, что мы можем делать такие изменения постоянными, просто прописав соответствующую строку в файл, который находится в директории /etc/sysctl.d/, например в /etc/sysctl.d/99-sysctl.conf:

kernel.sysrq = 1

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

Предупреждение

Настройки прописываемые в файле /etc/sysctl.conf не применяются начиная с версии 21x в systemd, поэтому прописывайте их только в файлах, которые расположены в подкаталоге /etc/sysctl.d. Имя файла не имеет значения.

14.1.2.2. tmpfiles.d#

К сожалению, далеко не все настройки ядра можно изменить при помощи sysctl или псевдофайловой ФС /proc/sys. Часть из них является отладочными, поэтому они расположены в виде файлов на другой псевдофайловой системе - sysfs, которая в основном отвечает за представление информации об устройствах, которыми управляет ядро. В директории в /sys/kernel представлены ряд других полезных параметров, которые мы рассмотрим в рамках общей темы. Чтобы выполнить установку значения в файлах, которые находятся в /sys/kernel/, мы будем использовать такой инструмент как systemd-tmpfiles.d [7]. Он есть только в дистрибутивах, использующих systemd в качестве системы инициализации, то есть в большей части дистрибутивов Linux включая Arch. Суть этой службы состоит в управлении, создании и удалении временных файлов или редактировании уже существующих. В нашем случае мы будем его использовать для записи значений в файлы настроек расположенных в /sys/kernel/. Для этого, по аналогии с sysctl, нужно создать файл в директории /etc/tmpfiles.d, например /etc/tmpfiles.d/99-settings.conf. Формат записи каждой строки в файле будет следующим:

w! /sys/kernel/mm/lru_gen/min_ttl_ms - - - - 2000

Первый символ - это тип действия, который systemd-tmpfiles будет выполнять с указанным по пути файлом. В нашем случае мы будем использовать только запись w некоторого значения в уже существующие файлы, а не создавать новые. Восклицательный знак ! указывает, что значение будет прописываться только один раз при загрузке системы. После пути до файла идут четыре прочерка, в них должны быть указаны права доступа, которые мы хотим изменить, но так как мы имеем дело со служебными файлами, то пишем везде прочерки, чтобы ничего не менять. В конце указываем значение, которое будет прописано в файл, то есть значение параметра.

Другими словами, везде, куда не дотянется sysctl, мы будем использовать tmpfiles.

14.1.2.3. udev#

По сути первых двух инструментов уже достаточно, чтобы выполнить полную настройку ядра, но мы используем ещё одну вещь - правила udev. Udev [8] - менеджер для управления вашими устройствами, который отслеживает их подключение/выключение, и предоставляет возможность создавать так называемые "правила", которые вызываются каждый раз, когда происходит определенной действие с тем или иным устройством. Внутри этого правила можно указать, при каких событиях и для какого конкретно устройства (условие для срабатывания) мы будем выполнять определенную команду или устанавливать некоторое значение. Это очень полезный инструмент, который позволит нам применять целый ряд настроек в зависимости от некоторых условий и подстраиваясь под железо, которое у вас есть в системе. Приведу пример, чтобы стало понятнее. Для разных типов носителей подходит разный планировщик ввода/вывода. Для обычных SSD - mq-deadline, для HDD - bfq. Правила udev позволят нам при подключении определенного типа устройства сразу выбирать нужный планировщик и дополнительные параметры для него, даже если у вас в системе есть и SSD, и HDD одновременно. Подробнее планировщики ввода/вывода будут рассмотрены далее вместе с синтаксисом самих правил.

14.2. Оптимизация ввода/вывода#

Фууух, что же, надеюсь вы не устали от всего этого скучного вступления выше и мы можем наконец-то переходить к сути. Начнем с оптимизации ввода/вывода, то бишь к настройке подкачки (она же своп, от англ. swap), различных кэшей и планировщиков.

14.2.1. Общие сведения#

Прежде чем перейти непосредственно к настройке необходимо понять принцип работы механизма виртуальной памяти и подкачки в Linux. Это важно, так как в этой теме ходит целая куча различных мифов, которые мы сейчас разберем.

Итак, для начала чрезвычайно важно понять, что ядро Linux разбивает всю вашу память на маленькие "гранулы" - страницы памяти, как правило по 4 КБ (для x86 архитектуры), не больше и не меньше. Это может показаться странным, но если не вдаваться в технические подробности, то такой подход позволяет ядру Linux проявлять достаточно большую гибкость, так как данные страницы могут быть одинаково обработаны ядром вне зависимости от того, что в них записано, предотвращая обильную фрагментацию. Тем не менее, все страницы памяти можно разбить на несколько типов. Сейчас мы не будем рассматривать их все, но остановимся на самых главных:

  • Файловая "подложка" или файловые страницы - это страницы в которых ядро "отображает", то есть представляет данные файла, считываемые с диска в виде страниц в памяти. С этими страницами тесно связано понятие страничного кэша (page cache) [9]. Если некоторый процесс открывает какой-то новый файл и читает из него информацию, то в первый раз ядро считывает эти данные с диска и сохраняет их в страничном кэше, а все последующие операции ввода и вывода к этим же данным будут осуществляться уже при использовании кэша, что значительно ускоряет все базовые операции чтения и записи, предотвращая повторные обращения к диску. При этом память для таких страниц выделяется по требованию, поэтому если процесс открыл файл, но ничего из него не читает, то никакой реальной памяти для таких страниц выделено не будет. Собственно, то, что вы видите в графе "Кэш" в любой программе аналоге системного монитора в Linux - и есть страничный кэш. Обратите внимание, что исполняемые файлы (программы) тоже загружаются в память как файловые страницы.

https://biriukov.dev/docs/page-cache/images/page-cache.png

(Licensed under the CC BY-NC 4.0. © Vladislav Biriukov, All rights reserved)

  • Очевидно, что далеко не все данные, которыми оперирует программа, могут быть представлены в виде реальных файлов на диске, поэтому были созданы анонимные страницы, которые, как следует из названия, не ассоциированы с файлами. Программы запрашивают их у ядра во время своей работы для динамических данных. Если вы разработчик, то вы наверняка сталкивались с такими понятиями как "Куча" (Heap) и "Стэк" (Stack). Так вот, ядро хранит данные из кучи и стэка именно в анонимных страницах памяти.

  • Грязные страницы (dirty pages) - по сути это подвид файловых страниц, ключевое отличие которых состоит в том, что программы в них пишут какие-то изменения, а так как ядро кэширует все данные считываемые из файлов во избежание излишней нагрузки на диск, то изменения, которые программа делает с файлом, на самом деле происходят сначала в кэше, и только потом синхронизируются с реальным файлом на диске. Более подробно об этом виде страниц и процессе их синхронизации с диском мы поговорим в следующем разделе.

Вернемся к подкачке. Один из самых больших мифов, связанных с подкачкой, состоит в том, что пользователи рассматривают её как некую "дополнительную память", которую свободно можно использовать в случае нехватки реальной, то есть физической памяти. Это конечно же не так, хотя бы потому, что процессор имеет доступ к оперированию только данными, которые находятся внутри ОЗУ. В случае нехватки памяти у ядра есть по сути всего один вариант - это освобождать уже имеющуюся память от тех страниц, которые не используются в данный момент, выгружая их в область на диске которую мы и называем подкачкой. Да, память не берется из воздуха, и подкачка - это просто "чердак", куда ядро скидывает все неиспользуемые вещи, чтобы освободить место для новых или более часто используемых страниц. При этом для процессов не меняется ровным счетом ничего, ибо они как и раньше могут обратиться к данным в памяти, которые были расположены на странице, которая была вытеснена ядром в подкачку, но когда процесс это сделает, ядро найдет эту страницу, считает её из подкачки и обратно загрузит в оперативную память. Это ещё одно преимущество механизма виртуальной памяти, повсеместно используемого ядром Linux.

Вопрос лишь в том, какие именно страницы нужно "вытеснить" из памяти. На самом деле, это достаточно сложный вопрос. Прежде всего, конечно же это будут именно анонимные страницы, так как файловые страницы и так по сути ассоциированы с данными на диске, следовательно в случае чего их точно так же можно повторно считать, и выгружать их в подкачку просто не имеет никакого смысла, что и происходит на практике. Но что если анонимных страниц много, а часть из них реально используется программами в данный момент? Какие из них тогда должны первым делом попасть в подкачку? На данный и многие другие вопросы отвечает специальный алгоритм в ядре Linux, называемый LRU (а поныне и MGLRU). Если очень упрощенно, то данный алгоритм ведет учет использования каждой страницы, то есть количество обращений к ней, и на основе данной статистики предполагает, какие из них реже всего используются процессами, и следовательно какие из них можно без проблем выгрузить в подкачку.

Рядовые пользователи часто не до конца понимают, какие именно данные расположены у них в подкачке. Теперь мы можем дать чёткий ответ: в подкачке хранятся только неиспользуемые анонимные страницы памяти.

14.2.2. Настройка подкачки#

Мы разобрались с основополагающими понятиями, и наконец-то можем переходить к настройке. Для настройки поведения подкачки используется параметр sysctl vm.swappiness (значение по умолчанию 60). Вокруг него так же ходит целый ряд заблуждений, что приводит к неправильным умозаключениям. Итак, во-первых, vm.swappiness напрямую никак не влияет на то, когда у вас начнет использоваться подкачка, то есть его значение - это вовсе не процент занятой памяти, при достижении которого начинает использоваться подкачка. Ядро всегда начинает использовать подкачку только в ситуациях нехватки памяти (это, как правило, когда занято 85-90% ОЗУ).

Во-вторых, параметр vm.swappiness влияет только на предпочтение ядра к вытеснению определенного типа страниц в случае этой самой нехватки. Он принимает значения от 0 до 200 (начиная с версии ядра 5.8 и выше, до этого максимальным значением было 100). Для более наглядного понимания, параметр vm.swappiness можно представить в виде весов, где более низкие (ниже 100) значения приводят к склонности ядра сначала вытеснять все страницы из файлового кэша, а более высокие (больше 100) - освобождение анонимных страниц из памяти в подкачку [10]. Значение 100 - это своего рода баланс, при котором ядро будет в одинаковой степени стараться вытеснять как файловые, так и анонимные страницы.

Другим крайне распространенным заблуждением является то, что более низкие значения vm.swappiness уменьшают использование подкачки - следовательно уменьшается нагрузка на диск, и что это якобы увеличивает отзывчивость системы. На деле это лишь на половину правда, так как, да, ядро при низких значениях старается откладывать использование подкачки, хотя это и не значит, что она вообще не будет использоваться, но важно понять, что это происходит за счёт более агрессивного вытеснения файловых страниц из страничного кэша - что точно так же приводит к нагрузке на ввод/вывод. Почему? Потому что каждый раз, когда ядро вытесняет страницу из страничного кэша, это приводит к тому, что все ранее хранящиеся в ней данные снова придется считывать с диска по новой.

Во-вторых, нагрузка на ввод/вывод, которую создаёт подкачка оказывается слишком переоценена. Для современных SSD накопителей переварить такую нагрузку без замедления работы системы не составит труда. Тем не менее, если страница была вытеснена в подкачку, то любая операция обращения к ней будет в разы медленнее, чем если бы она находилась в ОЗУ, даже если ваш носитель это NVMe накопитель, то операция записи страницы в файл/раздел подкачки и последующая операция чтения из него будет в любом случае затратна. Но даже если у вас HDD, то вам на помощь спешит Zswap - ещё один встроенный механизм ядра Linux, позволяющий значительно снизить нагрузку на диск и ускорить процесс вытеснения. Он представляет собой буфер в памяти, в который попадают анонимные страницы, которые на самом деле должны были попасть в подкачку на диске, и сжимаются внутри него, экономя тем самым драгоценную память насколько это возможно. Если пул страниц Zswap заполнится (по умолчанию он равен 20%), то ядро выполнит выгрузку страниц из Zswap в подкачку.

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

Учитывая всё вышеперечисленное, автор рекомендует устанавливать значение vm.swappiness в 100. Это позволит ядру равномерно вытеснять в подкачку оба типа страниц. В современных реалиях выкручивание параметра в низкие значения не приводит к желаемому эффекту. Конечно, всё индивидуально, и имеет смысл поиграться на своем железе, чтобы понять что лучше подходит лично вам имея прописанный багаж знаний по теме. Зафиксировать это значение можно через конфиг sysctl:

sudo nano /etc/sysctl.d/90-sysctl.conf#
vm.swappiness = 100

Предупреждение

Автор настоятельно не рекомендует устанавливать значение параметра в 0 или отключать подкачку вовсе. Подробнее о том, почему это вредно читайте в данной статье - https://habr.com/ru/company/flant/blog/348324/. Если вы хотите минимизировать использование подкачки чтобы минимизировать нагрузку на ввод/вывод, то используйте ZRAM, о котором пойдет речь далее.

14.2.2.1. ZRAM#

Но что делать, если у вас и правда очень медленный носитель или вы хотите минимизировать нагрузку на ввод/вывод и износ диска? В этом случае лучшим решением является использование ZRAM - вида подкачки, при котором все неиспользуемые анонимные страницы не выгружаются на диск, а сжимаются прямо внутри памяти при помощи алгоритмов сжатия без потерь. Точно так же как вы сжимаете простые файлы через архиватор, то же самое делает ядро со страницами памяти. Понятно, что уже сжатые страницы использовать нельзя, поэтому если они снова понадобятся процессу, то ядру придется их расжать перед использованием. Конечно, стоит учитывать, что сжатие и расжатие страниц происходит ресурсами процессора, и это имеет определенные накладные расходы, но они довольно несущественны для современных многоядерных процессоров, чтобы ими можно было пренебречь. Тем не менее, всегда можно выбрать более "легковесный" алгоритм сжатия.

Примечание

Некоторые пользователи задаются вопросом: В чем разница между zswap и ZRAM? На самом деле хотя они и занимаются по сути одной и той же работой, разница здесь в том, что Zswap является сжатым буфером в памяти, то есть промежуточным звеном между памятью и подкачкой, которое призвано помочь минимизировать нагрузку на ввод/вывод, а не заменить обычную подкачку на диске целиком как это делает ZRAM. Вытеснная страница при включенном Zswap имеет следующий цикл жизни: RAM -> Zswap -> Подкачка. Если процесс обратиться к странице, которая была вытеснена в Zswap, но которая так и не попала в подкачку на диске, то тогда ядро просто распакует её внутри памяти готовой для использования. В случае если она всё таки была вытеснена на диск, ядро считает её с диска и загрузит в память, как это обычно и происходит без zswap.

Об установке ZRAM было уже коротко рассказано в разделе Базовое ускорение системы. Однако не во всех дистрибутивах Linux есть служба zram-generator, поэтому покажем универсальный способ его настройки, основанный на обычных правилах udev.

Прежде чем мы перейдем к настройке ZRAM надо уточнить, что одновременное использование ZRAM и zswap имеет неопределенный эффект. С одной стороны, это вполне возможно, и в этом случае Zswap становится промежуточным буфером уже для ZRAM, но это не имеет особого смысла, так как они оба занимаются одним и тем же - сжатием данных внутри ОЗУ. ZRAM также ведет свою статистику о том, какие страницы и в каком количестве были сжаты, и которая может быть искажена, в силу того что помимо него в системе может работать Zswap, поэтому настоятельно рекомендуется его отключить перед использованием ZRAM. Для этого достаточно указать параметр ядра zswap.enabled=0 в конфиге вашего загрузчика, либо деактивировать прямо во время работы системы:

echo 0 > /sys/module/zswap/parameters/enabled

Если у вас затруднения с настройкой вашего загрузчика (а такое вполне может быть на атомарных системах), то вы можете настроить его перманентное отключение через создание файла в директории /etc/tmpfiles.d со следующим содержимым:

sudo nano /etc/tmpfiles.d/90-disable-zswap.conf#
w! /sys/module/zswap/parameters/enabled - - - - 0

Примечание

Важно отметить, что для использования ZRAM вам вовсе не обязательно отключать обычную подкачку, если она у вас до этого была настроена. В этом случае ядро по умолчанию будет использовать в качестве основной подкачки тот раздел или файл, примонтированный в служебную точку монтирования [swap], который имеет приоритет выше, чем другой. Поэтому если вы установите для ZRAM приоритет 100, как мы это сделаем ниже в файле /etc/fstab, то обычная подкачка на диске станет использоваться ядром только как запасная в случае если ZRAM переполнится, либо при использовании функции гибернации, которая может работать только с подкачкой на диске.

Перейдем к настройке ZRAM. Обратите внимание, что среди "мейнстримных" дистрибутивов Linux (как например Fedora) ZRAM начинают поставлять по умолчанию вместо обычной подкачки на диске. Поэтому сначала проверьте, не задействован ли уже ZRAM в вашей системе. Сделать это можно очень просто через команду zramctl, либо проверив по наличию файла /dev/zram0, который представляет собой блочное устройство куда будут попадать все вытесняемые ядром страницы (этакий виртуальный раздел подкачки).

Если же нет, то продолжаем. Для начала нам нужно форсировать загрузку модуля ZRAM, для этого нужно создать файл в директории /etc/modules-load.d/30-zram.conf и прописать в него всего одну строчку:

sudo nano /etc/modules-load.d/zram.conf#
zram

Теперь используя правила udev, мы будем создавать наше блочное устройство /dev/zram0 и делать из него раздел подкачки. Для этого создадим файл в директории /etc/udev/rules.d/30-zram.rules:

sudo nano /etc/udev/rules.d/30-zram.rules#
ACTION=="add", KERNEL=="zram0", ATTR{comp_algorithm}="zstd", ATTR{disksize}="8G", RUN="/usr/bin/mkswap -U clear /dev/%k", TAG+="systemd"

Теперь подробно о том, что из себя представляет само udev правило. В начале мы указываем при каком действии мы хотим, чтобы оно срабатывало. В нашем случае это ACTION=="add", то есть появление нового блочного устройства под названием KERNEL=="zram0". Это блочное устройство создается ядром автоматически при загрузке модуля ZRAM, форсированную загрузку которого мы уже прописали выше. Здесь можно заметить, что все проверки в правилах udev осуществляются через ==.

А дальше мы говорим, что в этом случае нужно делать. Во-первых, мы меняем значение атрибута (в udev правилах все они пишутся как ATTR{name}, где name - имя атрибута) comp_algorithm нашего блочного устройства, который указывает на используемый алгоритм сжатия. Для ZRAM в ядре предложены три алгоритма сжатия: lzo, lz4, zstd. В подавляющем большинстве случаев вы должны использовать только zstd, так как это наиболее оптимальный алгоритм по соотношению скорости/эффективности сжатия. LZ4 может быть быстрее при расжатии, но в остальном он не имеет больших преимуществ. LZO следует использовать только на очень слабых процессорах, которые просто не тянут сжатие большого объема страниц через Zstd.

Следующим атрибутом мы меняем disksize - это размер блочного устройства. Теперь очень важно: размер блочного устройства - это тот объем несжатых страниц, который может попасть внутрь ZRAM, и он может быть равен объему ОЗУ или даже быть в два раза больше него. Как это возможно? Представим, что у вас 4 Гб ОЗУ. Вы устанавливаете объем ZRAM тоже в 4 Гб. Вы полностью забиваете всю свою память, открывая 300 вкладок в Chromium, и любой системный монитор или аналог htop покажет вам, что подкачка тоже полностью забита, но проблема в том, что это тот размер страниц, которые попали в ZRAM до сжатия. То есть на деле у вас в ОЗУ вытесненные страницы занимают в разы меньший объем из-за сжатия. Увидеть это можно через команду zramctl, вывод которой может быть следующим:

NAME       ALGORITHM DISKSIZE DATA COMPR  TOTAL STREAMS MOUNTPOINT
/dev/zram0 zstd           15G   1G  232M 243.3M      16 [SWAP]

Здесь колонка DATA показывает какой объем страниц попал в /dev/zram0. Если вы опять откроете htop или другой аналог системного монитора, то вы увидите точно такой же объем того сколько у вас "занято" подкачки, но вот колонка COMPR показывает уже реальный размер вытесненных внутрь ZRAM страниц после сжатия, который очевидно будет меньше в 2-3 раза. Именно поэтому я рекомендую вам установить объем блочного устройства ZRAM, который в два раза больше, чем объем всей вашей памяти (Значение 8Gb - это лишь пример, замените его на то, сколько у объем вашей памяти и умножьте это на два**). Конечно, здесь нужно оговориться, что не все страницы бывают так уж хорошо сжимаемыми, но в большинстве случаев они будут помещаться без каких-либо проблем.

Надеюсь это добавило понимание того, почему не всегда нужно верить цифрам, которые вам говорит, например, команда free. Завершает наше udev правило действие, которое мы хотим сделать с нашим блочным устройством - запустить команду mkswap, чтобы сделать из нашего /dev/zram0 раздел подкачки.

Всё, что нам осталось теперь - это добавить запись в /etc/fstab, что /dev/zram0 это вообще-то наша подкачка и установить ей приоритет 100.

sudo nano /etc/fstab#
 /dev/zram0 none swap defaults,pri=100 0 0

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

Значение же vm.swappiness при использовании ZRAM рекомендуется установить в 150, так как более низкие значения приведут к излишнему вытеснению из файлового кэша, а анонимные страницы, которые потенциально могут быть легко сжаты, будут вытесняться в последний момент, что нежелательно. А вот при значении 150, файловый кэш будет дольше оставаться нетронутым, благодаря чему обращения к ранее открытым файлам останутся быстрыми, но при этом анонимные страницы просто сожмутся внутри памяти. Такой подход минимизирует нагрузку на ввод/вывод.

14.2.2.2. Отключение упреждающего чтения#

Из-за того, что процесс чтения вытесненной в подкачку страницы с диска и её записи обратно в оперативную память является довольно дорогостоящей операцией, ядро использует некоторые трюки, для того чтобы делать их как можно реже. Один из таких трюков это "упреждающее чтение" (readahead), когда при обращении процесса к вытесненной странице, ядро считывает не только запрошенную страницу, но и ещё некоторое количество страниц последовательно следующих за ней внутри подкачки.

Смысл здесь в том, что страница на практике это очень маленький фрагмент данных, которыми оперирует процесс, поэтому с большой долей вероятности обратившись к одной 4 Кб странице, процесс сделает ещё два и более запросов к тем страницам, которые тоже могли быть вытеснены в подкачку и быть записанными в него после той, которую процесс запрашивает в данный момент, и чтобы их потом тоже не искать и не читать ядро делает это сразу вместе с той вытесненной страницей, которую запросил процесс сейчас, так скажем, двух зайцев одним выстрелом.

Количество таких последовательно считываемых страниц за раз контролируется значением параметра vm.page-cluster. Это значение является степенью двойки, возведя в которую и можно получить количество страниц. Например, если установлено значение 1, то количество страниц, которые ядро считает заранее, будет равно 2^1, то есть просто два. Если значение параметра равно 2, то количество страниц уже будет равно в 2^2, то есть 4 и так далее. При значении 0 количество страниц будет 2^0, то есть 1 - это значение отключает упреждающее чтение страниц из подкачки.

На первый взгляд всё звучит здорово, и надо бы выкрутить значение побольше, чтобы ядро читало больше страниц за раз, но есть одна маленькая проблема, из-за которой я настоятельно рекомендую отключать этот параметр. Дело в том, что ядро считывает из подкачки страницы, которые были записаны по порядку за той страницей, которая в данный момент запрошена для загрузки обратно в память. Мы подразумевали, что это будут страницы того же процесса, который запросил данную страницу, но на деле это может вообще не так. Ядро записывает страницы из памяти в подкачку в том порядке, в котором они были вытеснены, и они вообще не обязательно могут относится к одному и тому же процессу, а даже если к одному, то могут быть совсем не теми, которые процесс запросит в будущем. Короче говоря, с упреждающим чтением мы играем в своего рода рулетку, повезет или нет. Но в подавляющем большинстве случаев ядро просто вернет в память обратно ещё 8 страниц (согласно значению по умолчанию), которые могут никогда не пригодиться в будущем, а если они не пригодятся, то их придется опять вытеснять в подкачку.

Таким образом, упреждающее чтение не только не решает заявленную проблему, но и наоборот её усугубляет. Для ZRAM это, конечно, может и не так критично, так как это вызовет лишь дополнительные циклы сжатия/расжатия страниц, но это в любом случае холостая работа. По этой причине разработчики ChromeOS и Android отключают данный параметр в своих системах по умолчанию [11] [12], что советую сделать и вам. Для этого как обычно достаточно просто прописать значение в конфиге sysctl:

sudo nano /etc/sysctl.d/99-sysctl.conf#
 vm.page-cluster = 0

14.2.3. Алгоритм MGLRU#

Мы уже говорили, что LRU - это алгоритм используемый ядром Linux для ведения учёта количества обращений ко всем страницам внутри памяти, позволяющий составлять выборку тех страниц, которые реже всего используются процессами и соответственно могут быть спокойно вытеснены в подкачку. Но начиная с версии 6.1 в ядре появилась альтернативная реализация этого алгоритма, называемая MGLRU (Multi-Generational LRU) [13]. Принципиальное отличие MGLRU от простого LRU алгоритма состоит в том, что выборка страниц, которые должны быть вытеснены, формируется не на основе только лишь одного признака (количества обращений к странице), а на основе целых двух признаков - количества обращений и времени последнего обращения. По этой причине новый алгоритм объединяет все страницы в так называемые "поколения" на основе времени обращения к ним, собственно именно поэтому его название и можно дословно перевести как "Многопоколенный LRU". Такой подход позволяет добиться большей точности в выборе из имеющихся страниц тех, которые по настоящему используются реже других, что в свою очередь позволяет уменьшать количество операций возврата страниц из подкачки, ибо чем точнее работает алгоритм выборки, тем больше вероятность, что вытесненная страница действительно никогда больше не понадобится и её не надо будет считывать с диска и загружать обратно в память.

Для того чтобы проверить собрана ли ваша версия ядра с поддержкой MGLRU достаточно прописать одну команду:

zgrep "CONFIG_LRU_GEN_ENABLED" /proc/config.gz

Если вывод команды не пустой, значит ваша текущая версия ядра собрана с поддержкой данного алгоритма, но это вовсе не значит, что он используется по умолчанию. Алгоритм MGLRU можно бесприпятственно включить или выключить прямо во время работы системы. Проверить статус работы алгоритма можно через файл /sys/kernel/mm/lru_gen/enabled:

cat /sys/kernel/mm/lru_gen/enabled

Если вывод команды равен 0x0000, значит MGLRU выключен, и его нужно самостоятельно включить следующей командой:

echo "y" | sudo tee /sys/kernel/mm/lru_gen/enabled

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

14.2.3.1. Защита от Page Trashing#

Одним из преимуществ алгоритма MGLRU над своим предшественником является предоставление дополнительной защиты от ситуаций Page Trashing.

Page Trashing - это ситуация острой нехватки памяти, при которой памяти становится настолько мало, что ядро начинает вытеснять в подкачку даже те страницы, которые активно используются процессами во время своей работы, так как все остальные малоиспользуемые страницы уже были вытеснены. Это приводит к тому, что количество операций возврата страниц из подкачки многократно увеличивается, так как к данным часто используемым страницам все время обращаются процессы, из-за чего ядру приходится читать их из подкачки с диска или распаковывать их из памяти, если речь идёт про ZRAM, и заново загружать память, после чего снова их вытеснять, так как других кандидатов для этого больше не осталось. Такой цикл становится очень заметным для пользователя, так как он порождает кратковременные зависания системы, ибо процессу каждый раз приходится ожидать, пока ядро достанет страницы из подкачки и загрузит их обратно в память.

Конечно, если потребление памяти в этом случае продолжит расти, то мы столкнемся с ситуацией Out Of Memory (OOM), после чего либо специальный демон по наводке ядра убьёт самый прожорливый процесс, чтобы освободить память, либо система полностью зависнет. Если потребление останется тем же, то мы продолжим испытывать постоянные микрозависания, что не очень приятно.

Здесь на сцену выходит алгоритм MGLRU, который хоть и не позволяет на 100% защититься от таких ситуаций, но позволяет убрать те самые кратковременные зависания, сделав систему более стрессоустойчивой и отзывчивой в условиях нехватки ОЗУ. Суть защиты состоит в том, что MGLRU предотвращает вытеснение "рабочего набора" страниц процесса (то есть таких страниц, которые действительно активно используются) в течении N миллисекунд, оставляя их не тронутыми в памяти на протяжении по крайне мере этого гарантированного времени. В этом случае процессам не придется каждый раз ожидать долгого восстановления страниц из подкачки и они сохранят свою скорость работы, но с другой стороны это увеличивает шанс возникновения ситуаций OOM, так как чем больше разрастается такой "рабочий набор" страниц, тем больше потребление памяти. По этой причине данный механизм защиты выключен по умолчанию, так как возникновение OOM ситуаций часто нежелательно на серверах и системах с большой нагрузкой, не предназначенных для интерактивного использования, где такие небольшие зависания были бы заметны глазу.

Для того чтобы включить данный механизм при использовании MGLRU нам нужно изменить значение параметра min_ttl_ms (по умолчанию 0), который как раз таки и устанавливает то время в миллисекундах, в течении которого рабочий набор страниц не будет вытесняться. Автор рекомендует брать значение от 1000 (это одна секунда), но не большее 5000, ибо это приведет к более частому возникновению OOM. Оптимальное значение для большинства - 2000 (2 секунды). В этом случае система достаточно сохранит свою интерактивность под нагрузкой. Указать значение можно как всегда через псевдофайловую систему sysfs, для автоматизации процесса воспользуемся файлом конфигурации systemd-tmpfiles:

sudo nano /etc/tmpfiles.d/90-page-trashing.conf#
w! /sys/kernel/mm/lru_gen/min_ttl_ms - - - - 2000

14.2.4. Настройка кэша VFS#

В страничный кэш попадают не только файловые страницы, в которых хранятся непосредственно данные считываемые с диска, но и метаданные к файлам и директориям. Доступ к ним осуществляется через так называемые индексные дескрипторы (inode) - специальные структуры, которые используются вашей файловой системой для хранения атрибутов, прав доступа и прочей служебной информации, а также они содержат номера секторов диска, которые указывают, где хранятся данные самого файла на носителе.

Перед открытием любого файла или дириктории сначала нужно выполнить его поиск на файловой системе, и это не самая быстрая операция как может показаться, даже несмотря на различные оптимизации, предоставляемые современными файловыми системами такими как использование B-деревьев для быстрого прохода по ним. В результате этой операции ядро как раз таки находит нужный индексный дескриптор, имея который можно обратиться к данным файла. Поэтому ядро кэширует все используемые во время работы системы дескрипторы и информацию о директориях внутри VFS [14] кэша, для того чтобы сделать все последующие обращения к файлами быстрыми, потому что ядро уже будет знать про них всё, что нужно.

Но все эти метаданные так или иначе занимают место внутри памяти, поэтому когда ядро начинает "промывку" (flush) страничного кэша, то оно вытесняет из него как данные самих файлов, так и метаданные для них. В ядре также есть специальный параметр sysctl vm.vfs_cache_pressure, который как раз таки регулирует, что будет вытесняться в первую очередь - сами данные или метаданные из кэша VFS. Здесь всё по аналогии с параметром vm.swappiness. При значении равном 100 (значение по умолчанию) ядро будет пытаться равномерно выгружать из памяти как кусочки содержимого самих файлов, так и индексные дескрипторы из кэша VFS. При значениях меньше 100 ядро будет больше отдавать предпочтение хранению метаданных в памяти, при значениях больше 100 - наоборот, больше избавляться от них в пользу обычных данных считываемых с диска.

Для наилучшего быстродействия системы рекомендуется устанавливать значение равным 50 [15], при котором вытеснение страниц, относящихся к VFS кэшу, происходит реже, чем для обычных файловых страниц, так как метаданные имеют большую ценность по сравнению с данными самих файлов, которые можно достаточно быстро повторно считать в страничный кэш на большинстве SSD накопителей при наличии индексного дескриптора файла, который как раз таки хранится внутри VFS кэша. Для сохранения значения как и всегда пропишем его в конфигурационный файл sysctl:

sudo nano /etc/sysctl.d/90-vfs-cache.conf#
vm.vfs_cache_pressure = 50

Конечно, лучший способ увеличения быстродействия ввод/вывода это кэшировать как можно больше данных в памяти, так как это самое быстрое устройство хранения в вашем компьютере (без учета кэша процессора), поэтому лучше всего как можно больше минимизировать вытеснение страниц из страничного кэша, но мы это уже сделали в разделе про настройку подкачки, установив большое значение параметра vm.swappiness и используя ZRAM для сжатия анонимных страниц прямо внутри памяти.

14.2.5. Настройка планировщиков ввода/вывода#

Планировщики ввода/вывода - это специальные модули ядра, которые регулируют порядок выполнения операций ввода/вывода во времени на уровне обращения к блочным устройстам (HDD дискам или SSD/NVMe/microSD/SD накопителям). Если вам казалось, что все запросы на чтение или запись происходят сразу же, то это не так.

Все запросы к носителю сначала попадают в очередь, которой и управляет планировщик ввода/вывода. В зависимости от используемого алгоритма он "ранжирует" все поступающие запросы таким образом, чтобы запросы которые осуществляются к соседним блокам на диске шли как бы друг за другом, а не в том порядке в котором они поступили в очередь. К примеру, если к планировщику поступили запросы на чтение 9, 3 и 5 блоков (условная запись), то он попытается разместить их в очереди как 3, 5 и 9. Зачем это делается? В силу исторических причин, все планировщики изначально разрабатывались с целью нивелировать недостатки механических дисков (и HDD в том числе), которые в силу своей специфики работы были чувствительны к порядку осуществления любых операций чтения или записи, так как чтобы выполнить любую операцию головке жесткого диска нужно было сначала найти нужный блок, а когда головка сначала выполняет чтение блока 9, а потом чтение "назад" блока 3, чтобы потом опять переместить головку вперед на блок 5, то очевидно что это несколько уменьшает пропускную способность диска.

Поэтому все планировщики и работают по принципу "лифта" (elevator): когда планировщик добавляет все запросы в очередь, но при этом планирует их выполнение уже в порядке возрастания по номерам блоков, к которым они обращаются. Кроме того, планировщик всегда будет отдавать предпочтение запросам на чтение запросам на запись, в силу того, что выполнение запросов на запись может быть неявно отложено ядром, либо происходит куда быстрее в силу того, что запись сначала осуществляется в страничный кэш (то есть в ОЗУ), а только потом на диск. В случае с операциями чтения их выполнение не может быть отложено, банально в силу того, что все программы, которые читают файлы, явно ожидают получения какого-то результата.

Конечно, на деле алгоритм планирования запросов ввода/вывода куда сложнее, но общий принцип остается тем же. На текущий момент в ядре существует три "реальных" планировщика ввода/вывода: BFQ, mq-deadline, kyber. Существует также четвертый вариант none, который устанавливает простую FIFO очередь для всех запросов. Это значит, что они будут обрабатываться ровно в том порядке, в котором поступили без какого-либо планирования.

Хотя выбор не велик, выбор планировщика может сильно зависеть от типа используемого носителя. Общие рекомендации к выбору планировщика под определенный тип носителя состоят в следующем:

  • Для NVMe и SATA SSD накопителей используйте none. Дело в том, что вся вышеописанная логика нахождения нужных блоков с использованием головки совершенно не актуальна для твердотельных накопителей с быстрым произвольным доступом [16], где любое обращение к блокам осуществляется за фиксированное время, поэтому порядок выполнения запросов для них не имеет такого же значения как для жёстких дисков. В то же время, накладные расходы при планировании прямо пропорциональны количеству запросов в очереди, которые планировщику нужно обработать ресурсами CPU, но в NVMe и простых SSD носителях планированием поступающих запросов на аппаратном уровне уже занимается встроенный контроллер, поэтому планировщик в ядре Linux по сути работает в холостую [17], нагружая при этом процессор, что в свою очередь может вызывать кратковременные зависания системы при большой нагрузке на ввод/вывод.

  • Однако для SATA SSD с плохим контроллером или устаревшим интерфейсом подключения (SATA 2) имеет смысл использовать планировщик mq-deadline. Для SD/microSD карт так же имеет смысл использовать только mq-deadline.

  • Для HDD следует использовать BFQ, но в целом любой планировщик должен быть лучше, чем его отсутствие как уже объяснено выше.

Как вы видите, здесь мы проигнорировали планировщик Kyber по той причине, что он практически не развивается за последние 3 года (то есть не получает новых значимых улучшений/оптимизаций) и рассчитан на работу со сверх быстрыми накопителями, которые чувствительны к задержкам, что не совсем актуально для домашней системы.

Итак, теория это хорошо, но как их все таки включить? Самый универсальный способ это написать собственные правила Udev, которые могли бы автоматически выбирать нужный планировщик в зависимости от типа носителя. Чтобы создать новые правила просто создадим новый файл в /etc/udev/rules.d/90-io-schedulers.rules:

sudo nano /etc/udev/rules.d/90-io-schedulers.rules#
# HDD
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"

# eMMC/SD/microSD cards
ACTION=="add|change", KERNEL=="mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"

# SSD
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"

# NVMe SSD
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"

(Чтобы использовать планировщик mq-deadline для SATA SSD просто поменяйте значение внутри кавычек в третьей строке с none на mq-deadline).

Помните, что универсального рецепта не существует, и всегда следует выполнить собственные тесты и бенчмарки (например при помощи программы KDiskMark), чтобы понять какой из планировщиков вам подходит лучше.

14.2.6. Увеличение размера карты памяти процесса#

Так как виртуальные страницы процесса представляют собой кучу маленьких фрагментов его данных, то для удобства вся его виртуальная память разграничивается ядром на зоны. Например, в одной зоне памяти процесса может быть загружена библиотека libc.so.6, а в других зонах - бинарный код другой библиотеки или данные самой программы, память под которые она запросила у ядра через функцию mmap. Зон может быть несколько, так как они различаются по своим правам доступа (Да, права есть не только у файлов, но и у виртуальных страниц). Информация об этих зонах памяти процесса ядро хранит в так называемой виртуальной карте памяти (memory map). Здесь речь идёт вовсе не о носителе данных, а о той карте, которая используются ядром для того чтобы понимать, начиная с какого адреса в памяти процесса расположена та или иная зона. Вы можете просмотреть эту карту прочитав файл maps на псевдофайловой системе procfs в директории, которая предоставляет информацию о процессе с соответствующим ID.

Размер таких карт у каждого процесса ограничен значением параметра vm.max_map_count , которое указывает на максимальное количество записей, которое может хранится в карте у процесса. По умолчанию это значение равно 65530 [4]. К сожалению, современные программы, в особенности игры запускаемые через Wine/Proton, с их потреблением более 8 Гб на процесс, могут запрашивать чрезмерно много виртуальных страниц у ядра, из-за чего количество зон для их процессов начинает превышать установленный лимит по умолчанию. Это приводит к тому, что ядро просто не даёт выделить ещё одну зону в памяти у процесса, что в свою очередь приводит к проблемам со стабильностью (приложение может просто аварийно завершится) и производительностью в таких прожорливых программах. Поэтому если вы столкнулись с неполадками во время игры, такими как частые вылеты или микрофризы, не спишите сразу писать гневные письма разработчикам Wine, а попробуйте увеличить значение данного параметра.

Чтобы избежать всех этих потенциальных проблем, рекомендуется увеличить допустимый размер карты памяти процесса, то есть значение параметра sysctl vm_max_map_count до 1048576. Тогда памяти хватит точно всем :)

sudo nano /etc/sysctl.d/99-sysctl.conf#
 vm.max_map_count = 1048576

Примечание

Во многих дистрибутивах., например таких как Fedora и Arch Linux, данное значение уже установлено по умолчанию [18] [19], поэтому пользователям данных дистрибутивов не нужно делать никаких дополнительных действий.

14.3. Источники#

Список источников используемых при составлении раздела: