В этом уроке разберём, почему io в Linux непредсказуем, что такое грязные страницы (dirty pages linux), кто и когда их сбрасывает на диск, откуда берутся всплески задержек и при чём тут fsync в базах данных. Научимся это руками смотреть и мерить честно, причём не только классикой (strace, /proc/meminfo), но и современным стеком eBPF, который к 2026 году стал стандартом для такой диагностики.
Как устроен кеш страниц linux: чтение, запись и грязные страницы
Ядро Linux старается не дёргать диск лишний раз. Все обычные файлы оно держит в оперативной памяти в виде кеша - это и есть page cache. Память разбита на страницы по 4 КБ, и каждая прочитанная с диска страница файла остаётся в RAM, пока память не понадобится под что-то другое. В выводе free этот кеш отражается в колонке buff/cache.
С чтением просто. Первый раз страница не в кеше - ядро идёт на диск, это медленно. Дальше та же страница уже в RAM, и повторное чтение идёт из памяти. Вот почему вторая команда быстрее первой: данные просто не дошли до диска повторно. Это не магия и не "прогрелся процессор", это кеш страниц linux.
С записью хитрее, и тут зарыт корень всех проблем. Когда программа пишет в файл обычным write(), данные сначала ложатся в страницу в памяти, и эта страница помечается как грязная (dirty). Программе сразу говорят "записано", хотя на диске ещё ничего нет. Реальная запись на диск называется writeback и происходит позже, в фоне. Аналогия: ты сложил письма в почтовый ящик у подъезда (быстро, ящик - это память), а почтальон отвезёт их на почту (диск) когда-нибудь потом по расписанию. Пока письма в ящике, формально они "отправлены", но реально лежат рядом с тобой.
Грязные данные - это удобно (запись быстрая) и опасно (при потере питания ящик сгорит вместе с письмами). Поэтому весь вопрос производительности и надёжности io крутится вокруг того, когда и как dirty pages linux уезжают на диск.
Посмотреть текущее состояние можно прямо в /proc/meminfo:
Код: Выделить всё
$ grep -E 'Cached|Dirty|Writeback' /proc/meminfo
Cached: 5242880 kB
Dirty: 10240 kB
Writeback: 0 kB
WritebackTmp: 0 kB
- Cached - общий размер кеша страниц. Обычно это большая часть RAM, и это нормально (см. раздел про грабли).
- Dirty - сколько килобайт грязных страниц ждут сброса на диск. Норма для тихой системы - единицы-десятки МБ. Если тут стабильно сотни МБ или гигабайты - значит, в систему льют запись быстрее, чем диск успевает её принимать. Это первый признак, что грядут пики.
- Writeback - сколько прямо сейчас активно пишется на диск. Если поле постоянно ненулевое и большое - диск работает на пределе, идёт массовый сброс.
Код: Выделить всё
watch -n1 "grep -E 'Dirty|Writeback' /proc/meminfo"
vm dirty ratio: когда ядро начинает сбрасывать и откуда всплески latency
Сколько грязных страниц ядро терпит до сброса, задают два параметра. Это ключ к пониманию пиков задержки, поэтому разберём подробно.
Код: Выделить всё
$ sysctl vm.dirty_background_ratio vm.dirty_ratio
vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
- vm.dirty_background_ratio (мягкий порог). Когда доля грязных страниц переваливает за этот процент, в фоне просыпаются потоки writeback. В ps/top их видно как kworker (на современных ядрах это per-bdi flusher-потоки вида kworker/u*; старое имя pdflush ушло ещё в 2.6.32). Они тихо сбрасывают данные на диск, приложения при этом ничего не замечают - запись по-прежнему быстрая.
- vm.dirty_ratio (жёсткий порог). Если грязных страниц накопилось столько, что они перешагнули этот процент, ядро говорит "хватит" и начинает тормозить (throttle) сами процессы записи: твой write() блокируется и ждёт, пока часть данных не уедет на диск. Вот он, источник всплеска latency. Приложение, которое только что писало за микросекунды, внезапно зависает на сотни миллисекунд.
Есть ещё два таймера, они тоже влияют:
Код: Выделить всё
$ sysctl vm.dirty_expire_centisecs vm.dirty_writeback_centisecs
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
Отдельно про 2026: на современных дистрибутивах по умолчанию работает cgroup v2, и в нём writeback стал cgroup-aware. Это значит, что лимит грязных страниц и троттлинг считаются с учётом memory-контроллера конкретного cgroup (через memory.high и io-контроллер). На практике: один шумный контейнер, заливающий диск записью, уже не так легко тормозит весь хост, как было на cgroup v1. Но глобальные vm.dirty_* по-прежнему действуют как верхняя граница.
Практический вывод: если видишь периодические пики записи, не вини сразу диск. Сначала глянь Dirty в /proc/meminfo в момент пика и текущие dirty_ratio. Снижение dirty_background_ratio (например, до 5) и dirty_ratio (до 10) делает сбросы чаще, но мельче - пики становятся ниже и ровнее. Альтернатива при огромной RAM - перейти на абсолютные байтовые лимиты vm.dirty_bytes/vm.dirty_background_bytes (они переопределяют проценты), например 256 МБ фон и 1 ГБ жёсткий, чтобы порог не зависел от объёма памяти. Это классическая правка для серверов БД и нагруженного storage. Менять так:
Код: Выделить всё
sudo sysctl -w vm.dirty_background_ratio=5fsync linux: почему база данных сама создаёт себе задержки
Теперь самое интересное место, где сходятся производительность и надёжность. Грязные страницы - это риск потерять данные при сбое. Базам данных это недопустимо: если СУБД сказала клиенту "транзакция зафиксирована", данные обязаны пережить выдёргивание шнура. Поэтому БД не доверяют фоновому writeback. Они сами вызывают системный вызов fsync (или его младшего брата fdatasync), который говорит ядру: "брось всё, протолкни вот эти грязные страницы конкретного файла на диск прямо сейчас и не возвращай мне управление, пока диск не подтвердит запись".
- fsync - синхронизирует и данные файла, и его метаданные (размер, время изменения).
- fdatasync - синхронизирует данные и только критичные для целостности метаданные (например, размер файла, если он вырос). Не трогает несущественные вроде времени модификации, поэтому чуть быстрее - часто меньше операций над журналом ФС. БД нередко используют именно его.
Классический способ поймать fsync руками - strace, посчитаем, сколько времени процесс реально проводит в этом вызове:
Код: Выделить всё
$ sudo strace -f -e trace=fsync,fdatasync -T -p <PID БД> 2>&1 | head
[pid 2412] fdatasync(42) = 0 <0.008931>
[pid 2412] fdatasync(42) = 0 <0.011207>
[pid 2412] fdatasync(42) = 0 <0.009644>
Важно на 2026: strace тормозит трассируемый процесс в разы (каждый syscall перехватывается через ptrace со сменой контекста). На проде так замерять боевую БД - значит уронить ей производительность. Современный путь - eBPF, который трассирует в ядре почти без накладных расходов.
Современная диагностика на eBPF: syncsnoop, ext4slower, biolatency
К 2026 году экосистема eBPF (bcc-tools и bpftrace) - зрелая и стоит почти везде, где ядро 5.x+ (а это весь свежий Linux). Для задержек io она уже фактически заменила strace и многие сценарии perf. Ставится пакетом bpfcc-tools (Debian/Ubuntu/Astra) или bcc-tools (RHEL/Alma/RED OS); bpftrace - отдельным пакетом.
syncsnoop - ловит все вызовы семейства sync (sync, fsync, fdatasync, msync) по системе, не привязываясь к PID. Идеально, чтобы понять "кто и как часто синкает":
Код: Выделить всё
$ sudo syncsnoop-bpfcc
TIME PID COMM EVENT
03:12:41 2412 postgres tracepoint:syscalls:sys_enter_fdatasync
03:12:41 2412 postgres tracepoint:syscalls:sys_enter_fdatasync
Код: Выделить всё
$ sudo ext4slower-bpfcc 5
TIME COMM PID T BYTES OFF_KB LAT(ms) FILENAME
03:20:11 postgres 2412 S 0 0 11.84 0000000A.wal
03:20:12 postgres 2412 S 0 0 9.55 0000000A.wal
biolatency - гистограмма задержек на уровне блочного устройства. Отвечает на вопрос "а сам диск-то быстрый?":
Код: Выделить всё
$ sudo biolatency-bpfcc
usecs : count distribution
64 -> 127 : 1820 |**************** |
128 -> 255 : 3940 |********************|
256 -> 511 : 410 |**** |
1024 -> 2047 : 12 | |
Просмотр и прогрев кеша: vmtouch, pcstat и честный замер через drop_caches
Раз кеш так влияет на скорость, надо уметь видеть, что в нём лежит. Голыми средствами это не показать, нужны vmtouch или pcstat (ставятся отдельно; vmtouch есть в репозиториях многих дистрибутивов, pcstat - бинарь с GitHub, оба используют системный вызов mincore).
Код: Выделить всё
$ vmtouch /var/lib/pgsql/data/base/16384/2619
Files: 1
Directories: 0
Resident Pages: 384/2048 1536K/8192K 18.75%
Elapsed: 0.000812 seconds
- Resident Pages: 384/2048 - из 2048 страниц файла прямо сейчас в памяти (резидентны) 384. То есть в кеше лежит лишь часть файла.
- 1536K/8192K 18.75% - те же 384 страницы в килобайтах и доля файла в кеше. 18.75% значит, что при следующем полном чтении 81% данных поедет с диска.
Код: Выделить всё
vmtouch -t /path/to/fileКод: Выделить всё
vmtouch -e /path/to/fileКод: Выделить всё
pcstat /var/lib/pgsql/data/base/16384/*Теперь про честный замер. Если бенчмаркаешь чтение, а данные уже в кеше, ты меришь скорость памяти, а не диска - и получаешь красивые, но бессмысленные гигабайты в секунду. Чтобы померить именно холодное чтение, кеш надо сбросить через drop_caches. Важнейший нюанс: сначала sync, потом drop. drop_caches не трогает грязные страницы, он чистит только уже синхронизированный кеш, поэтому без sync ты рискуешь намерить ерунду (грязные страницы остались бы в памяти и читались бы из неё):
Код: Выделить всё
$ sync
$ echo 3 | sudo tee /proc/sys/vm/drop_caches
3
- 1 - сбросить только кеш страниц (page cache);
- 2 - сбросить кеш inode и dentry (метаданные файловой системы);
- 3 - сбросить и то, и другое.
Предупреждение: drop_caches - инструмент для лаборатории и разовой диагностики, не для прода в кроне. Сбросив кеш на боевом сервере, ты заставишь все сервисы заново читать данные с диска и получишь искусственную просадку производительности на ровном месте.
Типичные грабли и заблуждения
- "Память почти вся занята, надо срочно чистить." Нет. Кеш страниц специально занимает всю свободную память - это правильно и полезно. В выводе free смотри на колонку available, а не на used: ядро отдаст кеш под приложения мгновенно, как только понадобится. Заполненный кешем RAM - здоровая система, а не утечка.
- "Сделал write(), значит данные на диске." Нет, данные в грязной странице в памяти. На диск их гарантированно проталкивает только fsync/fdatasync. Выдернешь питание между write и fsync - потеряешь.
- Замер без сброса кеша. Меришь скорость диска, а на деле читаешь из RAM. Всегда sync, потом drop_caches перед холодным бенчмарком.
- drop_caches как "лекарство" от высокого потребления памяти в кроне. Лечишь несуществующую болезнь и портишь производительность. Кеш не утечка.
- Виню диск во всех пиках записи. Сначала Dirty в /proc/meminfo и vm dirty ratio - часто пики создаёт сам механизм writeback при больших порогах, а не железо. Проверь это через biolatency.
- Замеряю боевую БД через strace. strace кратно замедляет процесс. На проде бери eBPF (syncsnoop, ext4slower) - почти без накладных расходов.
- Создай файл и понаблюдай за грязными страницами: в одном окне , в другом
Код: Выделить всё
watch -n1 "grep -E 'Dirty|Writeback' /proc/meminfo". Смотри, как Dirty подскакивает на втором dd, а затем опадает после writeback.Код: Выделить всё
dd if=/dev/zero of=/tmp/bigfile bs=1M count=2048 oflag=direct,nonblock 2>/dev/null; dd if=/dev/zero of=/tmp/bigfile bs=1M count=2048 - Замерь холодное и горячее чтение: , затем
Код: Выделить всё
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches(запомни скорость), и сразу повтори dd ещё раз. Сравни цифры - вторая будет в разы быстрее, это кеш.Код: Выделить всё
dd if=/tmp/bigfile of=/dev/null bs=1M - Поставь vmtouch (или pcstat) и посмотри, сколько процентов /tmp/bigfile сидит в кеше после прогрева, затем выгрузи через и проверь снова.
Код: Выделить всё
vmtouch -e /tmp/bigfile - Если есть тестовая БД - запусти в одном окне , в другом погоняй коммиты, посмотри частоту fdatasync. Затем
Код: Выделить всё
sudo syncsnoop-bpfccи сравни задержки sync с гистограммойКод: Выделить всё
sudo ext4slower-bpfcc 1.Код: Выделить всё
sudo biolatency-bpfcc
- Чем отличается путь записи от пути чтения в page cache и в какой момент данные реально оказываются на диске?
- Что произойдёт с приложением, когда доля грязных страниц перешагнёт vm.dirty_ratio, и почему это видно как всплеск latency?
- Зачем база данных вызывает fsync/fdatasync, если фоновый writeback и так сбросит данные, и почему этот вызов медленный?
- Почему перед echo 3 в drop_caches обязательно делать sync, и в каких полях /proc/meminfo это отражается?
Кеш страниц делает чтение быстрым (повторное - из памяти), а запись отложенной (через грязные страницы и writeback) - отсюда вся непредсказуемость io. Грязные страницы и пороги vm.dirty_background_ratio/vm.dirty_ratio объясняют регулярные пики задержки записи: смотри Dirty и Writeback в /proc/meminfo, при огромной RAM думай про dirty_bytes. fsync/fdatasync - честный, но медленный способ дотащить данные до диска, главный источник задержек коммитов в БД. Ловить его в 2026 лучше через eBPF (syncsnoop, ext4slower, biolatency), а не strace, который кратно тормозит процесс. Для честного замера скорости диска всегда делай sync и drop_caches, но никогда не по расписанию в проде. Занятый кешем RAM - норма: ориентируйся на available, а не на used.