Файловый кеш и грязные страницы: почему запись «мгновенная»
Приложение вызывает write(), получает мгновенный ответ «успешно» и продолжает работать дальше — а физический диск в это время может ещё даже не начать двигать головку или отправлять команду на флеш-память. Это не иллюзия и не баг: так по умолчанию устроена запись в Linux, и разница между «данные подтверждены» и «данные физически на диске» — источник и огромного выигрыша в производительности, и одного из самых неприятных сценариев потери данных при аварийном отключении питания.
Содержание
Что происходит на самом деле при вызове write()
Когда процесс вызывает системный вызов write(), данные почти никогда не идут напрямую на физическое устройство. Ядро Linux помещает их в page cache — область оперативной памяти, где кешируется содержимое файлов. Именно там, в буфере в RAM, данные оказываются в первую очередь, и именно оттуда write() возвращает управление приложению с кодом успеха.
Страница кеша, в которую только что записали новые данные, но которые ещё не оказались физически на диске, называется «грязной» (dirty page). Ядро помечает такую страницу специальным флагом и берёт на себя обязательство рано или поздно сбросить её на постоянное хранилище — но не обязательно прямо сейчас.
Важно разделять два разных действия, которые на первый взгляд кажутся одним:
- Запись в page cache — быстрая операция в памяти, занимает микросекунды, именно её видит приложение как «write() завершился».
- Запись на физический диск (writeback) — асинхронная операция, которая происходит когда ядро решит, что накопилось достаточно грязных страниц или истёк таймер, и может занять миллисекунды или больше в зависимости от типа накопителя и нагрузки.
Между этими двумя моментами может пройти заметное время — от долей секунды до десятков секунд в зависимости от настроек ядра. Всё это время данные существуют только в одном экземпляре — в оперативной памяти сервера.
Зачем вообще так сделано
Решение кешировать запись, а не писать синхронно каждый раз, не случайность и не недоработка — это осознанный компромисс, который даёт два реальных выигрыша.
Первый — отзывчивость приложений. Оперативная память на порядки быстрее любого физического накопителя, даже самого быстрого NVMe SSD: задержка обращения к RAM измеряется наносекундами, а задержка записи на диск — микросекундами и миллисекундами, то есть разница на три-четыре порядка. Если бы каждый write() дожидался физического подтверждения от устройства, отзывчивость любого приложения, часто пишущего в файлы (логирование, промежуточные файлы, обычная работа с диском), просела бы драматически.
Второй — возможность ядру оптимизировать саму физическую запись. Пока данные накапливаются в page cache в виде грязных страниц, планировщик ввода-вывода получает пространство для манёвра:
- Объединение мелких записей. Несколько последовательных мелких изменений в одном файле можно собрать в одну более крупную операцию записи — это эффективнее, чем гонять накопитель множеством мелких запросов.
- Устранение избыточной работы. Если один и тот же участок файла переписывается несколько раз подряд (типичная ситуация для временных файлов и логов с ротацией), физически на диск в итоге уйдёт только последнее состояние — ядро не станет писать промежуточные версии, которые всё равно тут же перезаписываются.
- Приоритизация. Неприоритетную фоновую запись можно отложить, если в этот момент система занята более срочным вводом-выводом — например, обслуживанием интерактивного чтения.
В сумме это даёт заметный выигрыш и в скорости отклика приложений, и в реальной нагрузке на накопитель — при том же объёме данных физических операций записи становится меньше, а каждая из них эффективнее.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКритический риск: что теряется при внезапном отключении питания
Вся эта конструкция держится на одном допущении — что рано или поздно грязные страницы благополучно доедут до диска. Штатная перезагрузка это допущение не нарушает: ядро при корректном shutdown/reboot успевает сбросить накопившийся кеш и размонтировать файловые системы, дождавшись подтверждения от устройств.
Внезапное отключение питания или аварийная перезагрузка ломают эту гарантию полностью. Если сервер обесточился, а часть грязных страниц ещё не была физически записана на диск, — эти данные физически теряются. Не откладываются, не восстанавливаются автоматически при следующей загрузке — исчезают вместе с содержимым оперативной памяти, как и всё остальное, что в ней было.
Здесь и кроется неприятный психологический эффект: приложение, которое написало файл и получило от write() подтверждение успеха, искренне «уверено», что данные в безопасности. Формально оно право — на уровне контракта ОС и приложения запись действительно завершена. Но эта уверенность не распространяется на физический уровень: если то самое подтверждение пришло из page cache, а не после реальной записи на носитель, при обрыве питания именно эти данные и пропадут первыми.
Практическое следствие этого механизма разобрано в статье про виртуалку, которая не стартует после сбоя питания — один из типичных сценариев такого инцидента: часть метаданных файловой системы или содержимого файла виртуального диска оставалась в грязных страницах хоста в момент отключения и физически не успела дойти до накопителя, из-за чего гостевая ФС или сам образ оказываются в промежуточном, несогласованном состоянии. Это не единственная причина повреждений после обрыва питания, но одна из самых частых — и её стоит держать в уме при любой диагностике подобных сбоев.
Явный контроль: системный вызов fsync()
Для сценариев, где важна гарантия физической сохранности данных именно в момент записи, а не когда-нибудь потом, в Linux есть явный механизм обхода отложенного кеширования — системный вызов fsync() (и его более узкий вариант fdatasync(), который синхронизирует только содержимое файла, без части метаданных вроде времени доступа).
fsync() принудительно заставляет ядро немедленно инициировать сброс конкретных грязных страниц, относящихся к указанному файловому дескриптору, на физическое устройство — и, что важно, дожидается реального подтверждения от самого устройства о том, что данные записаны, прежде чем вернуть управление вызывающему процессу. Именно это ожидание реального подтверждения, а не просто постановка в очередь на запись, и делает fsync() надёжной точкой синхронизации.
Именно поэтому fsync() — стандартный инструмент в базах данных для критичных транзакций. Когда СУБД коммитит транзакцию и сообщает клиенту «данные сохранены», она обязана быть уверена, что это утверждение выдержит внезапное отключение питания сразу после коммита — а единственный способ дать такую гарантию поверх обычного файлового кеша это дождаться fsync() для журнала транзакций (WAL) до того, как ответить клиенту об успехе. Без этого шага коммит СУБД был бы такой же иллюзией мгновенности, как и обычный write() в файл — быстрым, но без гарантии на случай сбоя.
Есть и обратная сторона: частые вызовы fsync() — это дорого именно потому, что они принудительно возвращают приложение к темпу физического диска, отменяя весь выигрыш от асинхронного кеширования. Поэтому fsync() используется точечно — там, где цена потери данных выше цены задержки, а не на каждую операцию подряд.
# пример: как это выглядит в системных вызовах приложения (strace)
strace -e trace=write,fsync,fdatasync -p <PID>
В выводе будет видно, что обычных write() может быть много и они идут быстро один за другим, а вызовы fsync()/fdatasync() — заметно реже и с более длинными паузами: это и есть момент, когда процесс реально ждёт физического подтверждения от накопителя.
Похожие, но не идентичные инструменты: sync и O_DIRECT
Кроме fsync() на конкретный файл, есть смежные механизмы, с которыми его иногда путают:
sync(команда или системный вызовsync(2)) — сбрасывает на диск все грязные страницы во всей системе, а не одного файла. Это грубее и медленнееfsync()на конкретном файловом дескрипторе, зато используется как ручной или скриптовый способ «сбросить всё, что накопилось», например перед плановым обслуживанием.- Флаг
O_SYNC/O_DIRECTпри открытии файла — заставляет каждую последующую операцию записи в этот файловый дескриптор вести себя синхронно (в случаеO_SYNC) или вовсе обходить page cache (в случаеO_DIRECT, актуально для СУБД, которые сами управляют собственным кешем в памяти и не хотят задваивать кеширование с ядром). - Точки монтирования с опцией
sync— редко применяемый на практике режим, при котором вообще все записи на файловую систему становятся синхронными; это резко снижает производительность и обычно используется только на специфичных носителях (например, съёмных), где риск обрыва высок, а нагрузка низкая.
Для большинства обычных приложений (веб-сервер, статические файлы, логи без критичных гарантий) весь этот арсенал не нужен — page cache и стандартная отложенная запись работают штатно и эффективно. Явная синхронизация нужна там, где есть конкретное требование «эти байты должны пережить внезапное отключение питания», и это требование стоит формулировать осознанно, а не полагаться на то, что раз write() вернул успех — значит, всё физически сохранено.
Как посмотреть текущий объём грязных страниц
Практический вопрос, который стоит задавать не только в теории, а регулярно на живом сервере: сколько данных прямо сейчас находится в состоянии «подтверждено приложению, но ещё не на диске» — то есть какой объём потенциально может быть потерян, если именно в эту секунду случится обрыв питания.
Ответ даёт файл /proc/meminfo:
cat /proc/meminfo | grep -i dirty
Типичный вывод выглядит так:
Dirty: 8524 kB
Writeback: 0 kB
Dirty— объём страниц кеша, содержащих данные, ещё не сброшенные на физический носитель. Это и есть тот самый «потенциальный объём потерь» на текущий момент.Writeback— объём страниц, которые прямо сейчас находятся в процессе физической записи (уже переданы устройству, но подтверждение о завершении ещё не получено).
Число в Dirty — не константа, а постоянно меняющаяся величина: оно растёт по мере того, как приложения пишут в файлы, и падает, когда ядро сбрасывает накопившееся на диск (это делает фоновый механизм writeback, управляемый в первую очередь параметрами vm.dirty_ratio и vm.dirty_background_ratio в sysctl). На сервере с активной записью на диск это значение стоит проверять не разово, а периодически — резкий и устойчивый рост Dirty при том, что накопитель явно не успевает его сбрасывать, обычно говорит о медленном диске или узком месте в подсистеме ввода-вывода, а заодно показывает, насколько велик риск в моменте, если именно сейчас пропадёт питание.
# посмотреть текущие пороги, при которых ядро начинает агрессивнее сбрасывать грязные страницы
sysctl vm.dirty_ratio vm.dirty_background_ratio vm.dirty_expire_centisecs
vm.dirty_background_ratio определяет процент оперативной памяти, при заполнении которого грязными страницами ядро начинает фоновый сброс без остановки приложений; vm.dirty_ratio — более жёсткий порог, при достижении которого пишущий процесс начинает сам блокироваться на записи, пока часть кеша не будет сброшена. Понимание этих порогов помогает объяснить, почему на сервере с большим объёмом RAM и интенсивной записью объём потенциально теряемых данных в момент отключения может быть заметно больше, чем на сервере с настройками по умолчанию и скромным объёмом памяти — грязным страницам просто есть, где накапливаться, прежде чем сработает порог принудительного сброса. Общую картину того, куда вообще уходит память сервера и почему занятая кешем RAM — это не утечка, разбирает статья куда девается «свободная» память и page cache.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если я вижу большое значение Dirty в /proc/meminfo — это уже проблема?
Само по себе нет — рост Dirty при активной записи это нормальная, ожидаемая часть механизма кеширования. Настораживать должно устойчиво высокое значение, которое не снижается со временем, — это может означать, что диск не успевает за темпом записи приложений, и стоит проверить реальную скорость и загрузку накопителя отдельно.
Можно ли вообще отключить page cache для записи, чтобы не рисковать?
Технически да — через O_DIRECT при открытии файла или монтирование с опцией sync, но для подавляющего большинства нагрузок это резко ухудшит производительность без реальной необходимости. Разумнее использовать точечный fsync() только там, где нужна гарантия, а не отключать кеш целиком.
Помогает ли журналируемая файловая система (ext4, XFS) от потери данных из грязных страниц при обрыве питания?
Журнал файловой системы защищает структуру самой ФС (метаданные, целостность каталогов и inode), но не гарантирует сохранность содержимого файлов, которое всё ещё было в грязных страницах и не дошло до диска. Это разные уровни гарантий, и путать их не стоит — подробнее разница между целостностью ФС и целостностью данных внутри файлов разобрана в статье что реально делает sync и почему файл ещё не на диске.
Почему для баз данных так важна не только скорость диска, но и явный fsync на каждый коммит?
Потому что без fsync() на журнал транзакций (WAL) само подтверждение коммита клиенту опиралось бы только на page cache — а значит теряло бы гарантию при обрыве питания. Как именно WAL, checkpoint и коммит упираются в latency диска, а не в его объём, разобрано в статье почему важна скорость диска для баз данных.
Есть ли способ узнать заранее, сколько времени займёт сброс всех грязных страниц на конкретном сервере?
Точного числа заранее нет — оно зависит от объёма Dirty в моменте и от реальной скорости записи конкретного накопителя под текущей нагрузкой. Ориентировочно можно прикинуть, разделив значение Dirty из /proc/meminfo на измеренную последовательную скорость записи диска, но это именно оценка «сверху», а не гарантированная цифра — реальная скорость writeback зависит ещё и от того, насколько фрагментированы грязные страницы по файлу.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →