Как тюнинг по гайдам ломает Linux


Недавно я ставила Ubuntu 26.04 на новый ThinkPad и по привычке полезла в заметки за своим post-install.sh, который доводит свежую установку до «правильного» состояния.
Живёт он у меня с 2012 года, когда я ставила Ubuntu 12.04 на ноутбук с жёстким диском, и копился по строчке из Ubuntu-вики, форумов и статей про пятнадцать способов ускорить Linux. Все эти годы я ни разу всерьёз не проверяла, что каждая строчка делает на ядре, вышедшем на десяток лет позже гайда.
В этот раз стало любопытно, и я прошлась по скрипту сверху вниз, держа в соседних вкладках документацию ядра, исходники и историю коммитов. Твиков там девять, от знаменитого swappiness до параметров планировщика, и удалить, как выяснилось, можно все девять, так что дальше я разбираю их по одному в том порядке, в каком они шли в скрипте.
Swappiness, который все ставят в десять
Строчка vm.swappiness = 10 встречается, кажется, вообще везде, и мне стало интересно, откуда она взялась. След ведёт в вики Ubuntu, на страницу SwapFaq.
Дальше становится смешно, потому что этот вопрос задавали ещё в мае 2011 года в рассылке ubuntu-users: вики говорит, что по умолчанию в Ubuntu стоит 60, а для типичного десктопа рекомендуется 10, так почему же установщик не считает десктоп типичным и не ставит десятку?
Ответили, что это документация сообщества и спрашивать надо того, кто её писал. Автор вопроса полез в историю правок и выяснил, что совет про значения от 5 до 10 впервые появился в двенадцатой ревизии страницы от одного из участников сообщества.Какие замеры стояли за этой цифрой, я так и не нашла, и, судя по рассылке, пятнадцать лет назад их тоже никто не нашёл.
Теперь о том, что параметр делает. В документации ядра swappiness описан как грубая относительная стоимость ввода-вывода при свопинге и при чтении файлов, и с ядра 5.8 он принимает значения от 0 до 200. При 100 ядро считает оба пути одинаково дорогими и давит на анонимную память и файловый кэш поровну. Для подбора там же дана формула: если своп вдвое быстрее файловой системы, то x + 2x = 200, и ставить надо 133.
Смотрите, подставляем наши 10 и получаем x + 19x = 200, то есть этой строчкой вы сообщаете ядру, что чтение из свопа у вас в девятнадцать раз дороже чтения файлов. Своп при этом лежит на том же NVMe, что и файлы, а в Fedora и вовсе в zram, для которого документация прямо советует значения больше 100.
Под нагрузкой десятка заставляет ядро выбрасывать файловый кэш вместе со страницами кода программ, которыми вы пользуетесь прямо сейчас, лишь бы не трогать анонимную память, где может годами лежать что-то, выделенное при старте браузера и с тех пор ни разу не прочитанное.
Ядро по умолчанию ставит 60, и на SSD это вполне разумная середина.
Нельзя просто так взять и выключить своп
Следующая строчка логично продолжает предыдущую: раз своп такой медленный, проще выключить его совсем, тем более что памяти сейчас дофига.
Эту логику ещё в 2018 году подробно разобрал Крис Даун, один из разработчиков подсистемы памяти ядра, в статье «In defence of swap: common misconceptions», и суть её укладывается в несколько абзацев. Анонимная память, то есть всё, что программы навыделяли себе через malloc и у чего нет файла на диске, может покинуть RAM только через своп.
Если свопа нет, под нагрузкой ядру остаётся освобождать одни файловые страницы, включая код запущенных программ, который тут же приходится читать с диска обратно. Система начинает бесконечно перечитывать собственные бинарники и встаёт колом на минуты задолго до того, как проснётся OOM-killer, потому что формально память у неё ещё есть.
Со свопом у ядра появляется выбор, и холодные анонимные страницы спокойно уезжают на диск, освобождая место под то, чем вы реально пользуетесь. Дистрибутивы это давно учли: установщик Ubuntu сам создаёт своп-файл, а Fedora с 33-й версии держит своп в zram, сжатым прямо в оперативке, чтобы ядру было куда девать холодные страницы, вообще не трогая диск.
Так что swapoff в моём скрипте аккуратно ломал ровно то, что дистрибутив сделал правильно.
Сброс кэша по крону
Задача в cron, которая раз в час сбрасывает кэши, растёт из одной и той же картинки: человек запускает free, видит, что свободной памяти почти не осталось, и решает, что Linux её съел.
Паника была настолько массовой, что под неё даже завели отдельный сайт с объяснением, что ядро заполняет свободную память кэшем и отдаёт её по первому требованию. С 2014 года это видно и без всяких объяснений: в ядре 3.14 в /proc/meminfo появилось поле MemAvailable, а free в том же году обзавёлся колонкой available, которая показывает, сколько памяти реально можно занять. Документация ядра про drop_caches написана максимально прямо: кэши ядро освобождает само, когда память нужна кому-то ещё, а ручной сброс может стоить заметного количества ввода-вывода и процессорного времени на повторное чтение всего выброшенного.
Поэтому пользоваться этим механизмом советуют только для тестов и отладки. По сути, ваш крон раз в час выгружает из холодильника всю еду, чтобы холодильник стоял пустым, после чего вы бегаете в магазин за каждым яйцом.
Самое прекрасное при этом лежит в исходниках, в fs/drop_caches.c. Каждую такую очистку ядро отмечает в логе строкой с именем процесса, его PID и записанным значением, а замолкает, только если однажды передать ему число с выставленным битом 4. Переменная, которая за это отвечает, называется stfu, и по-моему это лучший комментарий к теме, какой только можно найти в ядре.
Заодно проверьте, не чистит ли вам кэш кто-нибудь ещё: в /proc/vmstat есть счётчики drop_pagecache и drop_slab, которые считают такие сбросы с момента загрузки, и в нормальной системе там должны стоять нули.
Preload из 2009 года
Раз уж речь зашла о кэше, следующим в скрипте ставится preload, демон, который следит, какие программы вы запускаете, и заранее подтягивает в память их бинарники и библиотеки.
В паре с предыдущим кроном он смотрится особенно трогательно: один раз в час выгребает кэш подчистую, второй тут же старательно набивает его обратно. Версию 0.6.4, последнюю у preload, дистрибутивы пересобирали ещё в 2009 году. Писался он под жёсткие диски, где запуск программы упирался в перемещения головки, и предзагрузка в простое экономила секунды.
На NVMe пара сотен мегабайт библиотек читается примерно за десятую долю секунды (200 МБ делим на 2 ГБ/с, получаем 0,1 с), а всё, что вы недавно запускали, ядро и так держит в страничном кэше, пока память не понадобится кому-то ещё.
Даже в описании пакета в Debian честно предупреждают, что загрузку системы preload не ускорит и что работает он демоном с правами root.
noatime и семь тысяч лет
Переходим к дискам, и первым тут идёт noatime в fstab, обязательный пункт любого гайда по SSD: якобы без него при каждом чтении файла ядро пишет на диск новое время доступа и сжигает ресурс ячеек.
Нюанс в том, что так было до 2009 года, когда в ядре 2.6.30 умолчанием стал relatime. С ним время доступа обновляется, только если старое значение раньше времени изменения файла или с прошлого обновления прошло больше суток, то есть не чаще раза в день на файл. Прикинем с огромным запасом, сколько relatime пишет на самом деле. Допустим, за день вы трогаете десять тысяч разных файлов, и на каждый ext4 пишет отдельный блок в 4 КБ в журнал и ещё один на место, то есть 8 КБ на файл: 10 000 × 8 КБ = 80 МБ в день.
Берём скромный бюджетный NVMe на терабайт с ресурсом 200 ТБ записи и делим: 200 000 000 МБ / 80 МБ = 2,5 млн дней, это около семи тысяч лет!!!!!!
Умножьте мою оценку на сто, получится почти семьдесят лет, и это всё ещё дольше, чем проживёт ваш ноутбук. Вдобавок relatime придуман так, чтобы не ломать программы, которым надо знать, читали ли файл после последнего изменения, вроде mutt, который так замечает новую почту.
Так что noatime поверх него экономит копейки и ломает то, что relatime специально сохранил.
Лифт на эшафот
Параметр elevator=noop в командной строке ядра пришёл из той же эпохи первых SSD, и логика там была такая: головки у SSD нет, сортировать запросы незачем, значит, планировщик ввода-вывода надо заменить на noop, который пропускает запросы в том порядке, в каком они пришли.
В ядре 5.0 старый блочный слой выкинули целиком вместе с noop, deadline и cfq, и остался только blk-mq. Место noop там занимает none, что справедливее, потому что никакого планировщика в этом режиме нет вовсе. С приходом blk-mq параметр elevator= перестал учитываться, и в 2019 году его убрали из ядра совсем, поскольку вместе со старым путём ввода-вывода умерла и его польза.
Через пару месяцев Ян Кара из SUSE вернул на его место заглушку, которая при виде elevator= пишет предупреждение. В описании коммита прямо сказано, зачем: пользователей это может удивить, а советы использовать параметр до сих пор встречаются в интернете.
Если параметр до сих пор прописан у вас в GRUB, загляните в dmesg: ядро там вежливо сообщает, что elevator= больше ни на что не влияет, и предлагает выбирать планировщик для каждого устройства через sysfs. Там же, в sysfs, видно, что реально работает на вашем диске, и у NVMe в квадратных скобках будет стоять none. Ядро само выбирает none для устройств с несколькими аппаратными очередями вроде NVMe и mq-deadline для всего, у чего очередь одна.
Получается, что elevator=noop все эти годы пытался включить то, что и так включено, причём способом, который ядро игнорирует ПОЛНОСТЬЮ.
Губернатор performance
Следующий пункт прибивает частотный губернатор к performance, чтобы процессор не экономил на вас. Идея родом из времён acpi-cpufreq и ondemand, когда частоту выбирала операционная система по таймеру и заметно запаздывала за нагрузкой.
На Intel начиная со Skylake частотой управляет драйвер intel_pstate вместе с самим процессором через HWP. Драйвер задаёт границы и предпочтение между энергией и производительностью, а конкретную частоту процессор выбирает сам и быстрее любого программного губернатора.
Путаницу вокруг этого признаёт даже документация ядра: у intel_pstate свои алгоритмы с названиями powersave и performance, которые совпадают с именами обычных губернаторов и работают по-другому, и powersave у intel_pstate, по той же документации, примерно соответствует schedutil и ondemand.
Если заглянуть в настройки частоты в sysfs, на современном Intel там будет драйвер intel_pstate и губернатор powersave, и так и задумано, потому что под этим названием скрывается обычный динамический режим. На современных AMD картина та же, только драйвер называется amd-pstate. Режим performance при включённом HWP выставляет предпочтение в чистую производительность и прижимает процессор к верхней границе частот.
Я померила это на своём ThinkPad с Core Ultra 7 155U: в простое turbostat показывал около 2,1 Вт на пакет в powersave и около 3,4 Вт в performance, а сборка ядра с defconfig за три прогона заняла в среднем 9 мин 14 с против 9 мин 06 с. Получается полтора процента выигрыша на длинной сборке ценой шестидесяти процентов лишнего потребления в простое, в котором ноутбук проводит большую часть жизни.
Если же хочется выжать всё на время сборки или игры, в GNOME и KDE для этого есть переключатель режима питания прямо в системном меню, который двигает те же ручки и одним кликом возвращает всё обратно.
Буферы TCP и BBR
Сетевой блок из шести строк переписывается из гайда в гайд без изменений, и цифры в нём говорящие. Число 87380 в середине tcp_rmem когда-то было значением по умолчанию, и его переписали как есть, чтобы поднять только потолок до 16 МБ.
С тех пор ядро поменялось дважды. В 2018 году начальный приёмный буфер подняли примерно с 87 КБ до 128 КБ, а в 2025-м Эрик Дюмазе из Google поднял потолок автотюнинга до 32 МБ, отметив в коммите, что прошлый раз это значение меняли в 2012 году.
В документации теперь так и написано: по умолчанию от 131072 байт до 32 МБ в зависимости от объёма памяти. Выходит, на свежем ядре эта строчка уменьшает и стартовый буфер, и максимальный. С остальными строчками история похожая: автотюнинг приёмного буфера включён по умолчанию, и буфер каждого соединения растёт сам до потолка.
А net.core.rmem_max касается только программ, которые сами задают размер буфера, и такой запрос заодно отключает сокету автотюнинг, так что браузеру от этой строчки ни тепло ни холодно. Сколько буфера вообще нужно, считается просто: скорость канала умножить на задержку. Гигабитный тариф и сервер в CDN за 10 мс дают 125 МБ/с × 0,01 с = 1,25 МБ, и даже старых 6 МБ на это хватало с запасом. Гигабит до сервера в ста миллисекундах даёт 12,5 МБ, которые в новые 32 МБ тоже влезают.
С BBR история ещё проще, и тут у меня вопрос залу: кто решает, с какой скоростью к вам едет файл с сервера?
Правильно, сервер, потому что управление перегрузкой работает на стороне отправителя, и десктопу, который в основном принимает, ваш BBR помогает разве что при git push и загрузке фоточек в облако. А очередь fq живёт в этом блоке с тех времён, когда BBR без неё не умел дозировать отправку пакетов, но начиная с ядра 4.13 TCP делает это сам.
Мёртвые души
Последний блок в скрипте подписан «для отзывчивости», и это особенно смешно, потому что ровно такие же значения sched_min_granularity_ns и sched_wakeup_granularity_ns стоят в серверном профиле tuned throughput-performance, под комментарием про настройки для серверов RHEL6 с максимальной пропускной способностью ввода-вывода. А sched_migration_cost_ns в пять миллисекунд нашёлся в профиле latency-performance.
Но главное здесь даже другое: эти три строчки не делают вообще ничего уже больше пяти лет, и если применить конфиг руками, sysctl честно ответит на каждую из них, что не может найти соответствующий файл в /proc/sys/kernel. В ядре 5.13 Питер Зейлстра перенёс эти ручки из sysctl в debugfs, объяснив в коммите, что хватит засорять sysctl недокументированными крутилками, которые на самом деле нужны только для отладки.
На жалобы в рассылке он ответил, что полагаться на отладочную инфраструктуру ради производительности в продакшене это ваша ошибка, а поддерживать людей, которые тыкают туда случайные значения, он принципиально не собирается. В ядре 6.6 CFS заменили на EEVDF, и большей части этих параметров теперь нет даже в debugfs.
При загрузке конфиг применяется без вашего участия, ругань sysctl вы увидите, только если запустите его руками, и поэтому такие строчки годами переезжают с машины на машину, как мёртвые души по ревизской сказке: числятся в конфиге и отсутствуют в /proc. После всех вычёркиваний от post-install.sh осталась одна команда, которая обновляет систему, и её я теперь просто набираю руками, потому что всё, что я годами пыталась подкрутить, приезжает вместе со свежим ядром.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.