Приложение сохраняет файл за доли секунды только на первый взгляд: данные оседают в страничном кеше ядра, помечаются как грязные страницы и попадают на накопитель позже, когда фоновые потоки записи решат, что момент удобный. Пока буфер растет плавно, система летит, но стоит грязным страницам упереться в верхний лимит, и каждый пишущий процесс замирает, пока ядро не разгребет очередь: рабочий стол перестает отзываться, копирование встает на середине, база данных ловит скачки задержки. Поведение буфера записи настраивается, и не одной ручкой, а связкой глобальных лимитов, таймеров устаревания и раздельных квот по устройствам. Ниже разбираются четыре параметра каталога /proc/sys/vm, точечные квоты в /sys/class/bdi, наблюдение за грязными страницами в реальном времени и готовые профили для рабочей станции, сервера баз данных и резервного копирования. Настройка занимает минуты, а предсказуемость ввода-вывода остается навсегда.

Путь данных от приложения к диску и две ступени торможения записи

Любая обычная запись проходит через page cache: ядро копирует данные в страничный кеш и сразу возвращает приложению успех, а настоящая выгрузка на накопитель идет своим чередом в фоне. Страница, измененная в памяти и еще не совпадающая с содержимым диска, называется грязной, и ее объем виден в /proc/meminfo:

grep -E 'Dirty|Writeback' /proc/meminfo
sysctl vm.dirty_ratio vm.dirty_background_ratio

Поле Dirty показывает страницы, ожидающие выгрузки, поле Writeback те, что сбрасываются на диск прямо сейчас. Команда sysctl выведет текущие границы в процентах:

vm.dirty_background_ratio = 10
vm.dirty_ratio = 20

Типичные ядра используют именно эти значения. Тонкость, о которую спотыкаются при расчетах: проценты считаются не от всей установленной памяти, а от доступной, то есть от суммы свободных и освобождаемых страниц, которую показывает поле MemAvailable. У машины с 32 GiB RAM при плотном кешировании доступной памяти может быть вдвое меньше, и все границы автоматически сжимаются вместе с ней. Приложение способно форсировать выгрузку и без настройки: вызов fsync дожидается сброса конкретного файла, а команда sync запускает выгрузку всех грязных страниц системы.

Механизм торможения двухступенчатый, и понимание его ступеней объясняет, откуда берутся внезапные паузы. При пересечении фоновой границы просыпаются фоновые потоки записи ядра и начинают сбрасывать данные параллельно с работой приложений. При достижении жесткой границы сценарий меняется: пишущий процесс внутри функции balance_dirty_pages сам выгружает собственные страницы, то есть работает уже со скоростью накопителя. Отсюда знакомый эффект: система отзывчива, отзывчива, а потом на десять секунд замирает целиком, хотя процессор загружен на пять процентов.

Расчет лимитов vm.dirty_ratio на конкретном сервере и правка на лету

Формула простая: граница в байтах равна доступной памяти, умноженной на процент и деленной на сто. Пример строится на сервере с 16 GiB RAM, у которого MemAvailable показывает 12288 MiB. Фоновая граница составит 12288 10 / 100 = 1229 MiB, жесткая 12288 20 / 100 = 2458 MiB. Если массив выдает 200 МБ/с, слив 2458 MiB длится около тринадцати секунд, и все это время интенсивно пишущие процессы балансируют на грани торможения. Чем медленнее накопитель, тем опаснее жесткая граница, поэтому для слабых дисков ее снижают, а не поднимают: пауза в полсекунды незаметна, пауза в тридцать секунд выглядит зависанием.

Базовое значение доступной памяти проверяется одной строкой:

grep MemAvailable /proc/meminfo

На лету лимиты меняются без перезагрузки:

sysctl -w vm.dirty_background_ratio=8
sysctl -w vm.dirty_ratio=16

Механизм целиком выглядит так:

  1. пока грязные страницы ниже фоновой границы, ядро ничего не предпринимает, и приложение пишет со скоростью памяти;
  2. после пересечения фоновой границы просыпаются фоновые потоки и выгружают данные на диск параллельно с работой приложений;
  3. при достижении жесткой границы пишущий процесс сам выгружает собственные страницы и работает со скоростью диска, что и дает внезапные паузы.

Считать байты в уме каждый раз не нужно: после правки границ достаточно снова посмотреть на поле Dirty под нагрузкой, и новые числа сразу проявятся в поведении счетчика.

Абсолютные лимиты в байтах вместо процентов и постоянная запись настроек

Проценты удобны, но крупным машинам дают непропорционально огромный буфер: на сервере со 128 GiB RAM при dirty_ratio 20 жесткая граница способна добраться до десятков гигабайт. Для точных гарантий существуют парные параметры vm.dirty_background_bytes и vm.dirty_bytes, задающие границы прямо в байтах. Задать можно только один параметр из каждой пары: при записи байтов соответствующий процент читается как 0, и наоборот. Минимум для vm.dirty_bytes равен двум страницам, то есть 8192 байтам при размере страницы 4 KiB, все, что меньше, ядро игнорирует и сохраняет прежнее значение.

Ноутбук с 16 GiB RAM и медленным SSD получает аккуратный буфер двумя командами:

sysctl -w vm.dirty_background_bytes=134217728
sysctl -w vm.dirty_bytes=536870912

Фоновая выгрузка здесь стартует со 128 MiB, жесткий потолок равен 512 MiB, и гигабайтная очередь с внезапным замиранием интерфейса не появится в принципе. Чтобы настройки пережили перезагрузку, их переносят в файл каталога /etc/sysctl.d, например /etc/sysctl.d/99-dirty-writeback.conf:

vm.dirty_background_bytes = 134217728
vm.dirty_bytes = 536870912
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 200

Применение без перезагрузки выполняет команда sysctl --system, она читает файлы каталога по порядку и записывает значения в ядро. Правило выбора формы простое: проценты берут для однородных машин, где важна пропорция, байты там, где нужна точная гарантия размера буфера независимо от объема памяти и ее занятости.

Таймеры устаревания и периодичности задают окно потери данных

Лимиты определяют, сколько данных может лежать в памяти, а таймеры определяют, как долго. Параметр vm.dirty_expire_centisecs задает возраст страницы в сотых долях секунды, после которого она считается устаревшей и подлежит выгрузке при ближайшем пробуждении потоков записи. Типичное значение по умолчанию равно 3000, то есть тридцати секундам. Параметр vm.dirty_writeback_centisecs задает период этих пробуждений, типично 500, пять секунд. Худший случай для отдельной страницы получается сложением двух величин: тридцать пять секунд данные живут только в памяти, и при отключении питания они исчезают вместе с ней, даже если файловая система целая.

Окно сокращается двумя числами:

sysctl -w vm.dirty_expire_centisecs=1000
sysctl -w vm.dirty_writeback_centisecs=100

Теперь данные старше десяти секунд уйдут на диск не позже чем через секунду, а всплеск записи растянется во времени и станет менее заметным. Оборотная сторона в том, что на жестких дисках частые мелкие сбросы съедают производительность на перемещении головок, поэтому для классических HDD период уменьшают осторожно и проверяют результат на нагрузке. Полный ноль в vm.dirty_writeback_centisecs отключает периодическую выгрузку совсем: данные уходят только при достижении лимитов, закрытии файлов и вызове sync, что экономит энергию, но растягивает очередь до предела, и такую настройку оправдывают лишь машины с гарантированным питанием. Для монтирований с опцией lazytime существует отдельный таймер /proc/sys/vm/dirtytime_expire_seconds, он управляет сбросом изменившихся временных меток, а не самих данных.

Раздельные квоты устройствам в /sys/class/bdi защищают от медленных и капризных хранилищ

Каждое хранилище в ядре описано структурой backing device, и у каждого есть каталог в /sys/class/bdi с именем вида MAJ:MINOR. Соответствие устройств и номеров показывает lsblk:

lsblk -o NAME,MAJ:MINOR
ls /sys/class/bdi

В списке встретятся 8:0 для диска, имя с суффиксом -fuseblk для файловых систем FUSE поверх блочного устройства и отдельный каталог default для неблочных файловых систем. По умолчанию ядро раздает общий буфер записи пропорционально средней скорости выгрузки каждого устройства, и честность схемы ломается на медленных или зависающих устройствах: FUSE-контейнер или тормозящий сетевой ресурс NFS способен занять весь буфер и задержать чужие данные. Помогают квоты:

echo 2 > /sys/class/bdi/0:52/max_ratio
echo 1 > /sys/class/bdi/0:52/strict_limit
echo 10 > /sys/class/bdi/8:0/min_ratio

Первая строка ограничивает капризный ресурс двумя процентами буфера, вторая заставляет ядро проверять личную квоту заранее, не дожидаясь глобальной фоновой границы, третья резервирует диску данных не меньше десяти процентов, чтобы резервное копирование не голодало. Ограничение strict_limit заметно работает, когда max_ratio меньше ста процентов. Значения min_ratio по всем устройствам в сумме не должны превысить ста процентов, иначе ядро отклонит запись нового значения. Для тонкой подстройки существуют min_bytes и max_bytes, а также min_ratio_fine и max_ratio_fine, выражающие квоту в миллионных долях, они появились в конце 2022 года и выручают, когда двух процентов много, а одного мало.

Наблюдение и нагрузочный тест показывают разницу до и после настройки

Разница между удачной и неудачной настройкой видна глазами, без синтетических испытаний. В одном терминале запускают наблюдение:

watch -n1 "grep -E 'Dirty|Writeback' /proc/meminfo"

Во втором создают нагрузку обычной буферизованной записью:

dd if=/dev/zero of=/mnt/data/test.bin bs=1M count=8192
iostat -dx 1

До настройки Dirty взлетает до жесткой границы, условно 4096 MiB, затем писатель замирает, а очередь сходит медленно: при диске на 120 МБ/с сброс 4096 MiB занимает свыше тридцати пяти секунд, и все это время интерфейс едва дышит. После снижения лимитов Dirty держится в пределах заданного потолка, кривая скорости в iostat выравнивается, всплески пропадают. Детальнее глобальной картины смотрят статистику отдельного устройства в /sys/kernel/debug/bdi/8:0/stats, где строки BdiWriteback, BdiReclaimable и BdiDirty-Thresh показывают личный буфер устройства, его невыгруженный хвост и личную долю общего лимита. Для доступа к статистике при необходимости монтируют debugfs командой mount -t debugfs none /sys/kernel/debug, а утилита vmstat 1 дополняет картину столбцом bo, показывающим выгруженные килобайты в секунду.

Готовые профили для рабочей станции, базы данных и резервного копирования

Три проверенных набора закрывают большинство сценариев. Рабочая станция с SSD, для которой важна отзывчивость, получает фон 128 MiB, жесткий потолок 512 MiB, устаревание пятнадцать секунд и пробуждение раз в две секунды, то есть значения из файла примера выше. Сервер баз данных с 64 GiB RAM получает точные байтовые границы и короткие таймеры:

vm.dirty_background_bytes = 268435456
vm.dirty_bytes = 1073741824
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100

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

sysctl -w vm.dirty_background_ratio=20
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_expire_centisecs=6000
sysctl -w vm.dirty_writeback_centisecs=1000

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

Слишком низкие лимиты тоже вредят: ядро начинает сбрасывать крошечными порциями, нагрузка на накопитель растет, а скорость последовательной записи падает. В многоарендных системах добавляется еще один слой: современные ядра ведут учет грязных страниц по группам cgroup, доля группы в выгрузке регулируется весом io.bfq.weight, а реально выгруженные байты видны в файле io.stat группы. Торможение записи не дефект, а защитный клапан: оно существует, чтобы очередь не росла бесконечно, и грамотная настройка просто переносит паузы туда, где их никто не заметит.