MAATRIX / Блог / Что реально делает sync и почему файл ещё не на диске

Что реально делает sync и почему файл ещё не на диске

MAATRIX

Приложение вызвало write(), получило в ответ 0 (успех), но при отключении питания через секунду после этого данные всё равно пропали — и это не баг файловой системы, а нормальное поведение Linux, о котором мало кто задумывается, пока не столкнётся с ним на проде. Разберём, что на самом деле происходит между вызовом write() и моментом, когда байты физически лежат на накопителе, и почему между этими двумя событиями может быть куда больше времени, чем кажется.

Путь одного байта: от буфера приложения до диска

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

Буфер приложения. Многие библиотеки ввода-вывода (stdio в C — fprintf, fwrite) сами буферизуют данные в памяти процесса и не вызывают системный вызов write() на каждую операцию — копят до заполнения буфера или явного fflush(). Этот уровень вообще не касается ядра: если процесс упадёт до fflush(), данные потеряются, даже не добравшись до ОС.

Page cache ядра. Когда данные доходят до системного вызова write(), ядро Linux не пишет их на диск немедленно, а копирует в page cache — область RAM, где кешируются страницы файлов — и помечает эти страницы dirty (изменённые, ещё не сброшенные на диск). Именно в этот момент write() возвращает успех. С точки зрения ядра работа сделана: данные в памяти, метаданные обновлены. Но физической записи ещё не было.

Физическая запись на диск. Dirty-страницы попадают на носитель позже, асинхронно, механизмом ядра — об этом ниже. Между тем, как страница помечена dirty, и тем, как она реально записана, может пройти от миллисекунд до десятков секунд — в зависимости от настроек и нагрузки.

Отсюда и главный источник путаницы: write() возвращает успех, как только данные оказались в page cache, а не когда они физически на диске. Это архитектурное решение, а не недоработка — без него дисковый ввод-вывод был бы синхронным и узким местом для любой операции.

Page cache и dirty pages: зачем это нужно

Идея page cache простая: RAM на порядки быстрее любого накопителя, поэтому ядро агрессивно использует свободную память как кеш для файлов — и на чтение, и на запись. При записи данные сначала оседают в RAM (становясь dirty), а на диск сбрасываются пачками, что позволяет объединять мелкие записи в более крупные последовательные операции, переупорядочивать их для оптимального доступа к носителю и не блокировать процесс на каждой отдельной операции записи.

Посмотреть текущий объём dirty-страниц:

grep -i dirty /proc/meminfo
Dirty:             1024 kB
Writeback:            0 kB

Dirty — сколько памяти занято изменёнными, но ещё не сброшенными на диск страницами. Writeback — сколько страниц прямо сейчас активно пишутся на носитель. Если Dirty стабильно растёт — стоит проверить через iostat, успевает ли диск за нагрузкой (подробнее — в статье про диагностику медленного диска на VPS). Общий объём памяти под page cache виден в free -h, колонка buff/cache — это нормально: ядро отдаст её приложениям, если она понадобится.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

write(), fsync() и sync: разные уровни гарантий

write() копирует данные из буфера процесса в page cache ядра. Успех означает только «данные приняты ядром», а не «данные на диске». Это самый быстрый вариант и самый слабый по гарантиям сохранности при сбое.

fsync(fd) заставляет ядро сбросить на диск все dirty-страницы конкретного файла и дождаться подтверждения от накопителя, что запись физически завершена. Именно этим механизмом приложения (в первую очередь СУБД) добиваются гарантии «данные точно на диске» перед тем, как подтвердить операцию клиенту.

Более лёгкий вариант — fdatasync(fd): то же самое, но без гарантии синхронизации метаданных файла (например, времени модификации), если они не влияют на чтение данных. Это немного быстрее за счёт меньшего числа операций, но там, где важны точные метаданные (скажем, изменившийся размер файла), нужен полноценный fsync().

sync — команда и системный вызов уровня всей системы: сбрасывает dirty-страницы всех файлов на всех примонтированных файловых системах.

sync

Раньше sync набирали вручную перед выключением машины — сегодня это делает штатный shutdown, но команда осталась и иногда используется перед операциями, требующими консистентного состояния диска (например, перед извлечением съёмного носителя). Для критичных данных конкретного файла надёжнее полагаться на fsync() внутри приложения, а не на ручной sync со стороны.

Отдельно стоит O_SYNC/O_DSYNC — флаги при открытии файла, после которых каждая запись сама ведёт себя как write() + fsync(). Удобно, когда не хочется явно вызывать fsync() после каждой записи, но и дороже по производительности — теряется главное преимущество page cache, асинхронный батчинг.

Почему «write вернул успех» — не гарантия, а начало пути

Возврат write() без ошибки означает: ядро приняло данные, они в page cache, помечены dirty и рано или поздно будут сброшены на диск. Это железная гарантия для всего, что видит сама система — любое чтение того же файла, даже другим процессом, увидит уже новые данные, потому что чтение тоже идёт через page cache. С точки зрения консистентности внутри работающей системы всё корректно.

Проблема — только в одном сценарии: аварийное завершение работы системы до того, как dirty-страницы физически попали на диск (отключение питания, kernel panic, аппаратный сбой). В этот момент содержимое RAM исчезает безвозвратно, и после перезагрузки на диске окажется более старая версия файла — та, что была там до последних записей.

Это не проблема диска, файловой системы или конкретной программы — это прямое следствие того, что page cache существует в энергозависимой памяти. Единственный способ гарантированно пережить такой сбой — явно синхронизировать данные (fsync()) до того, как приложение сообщит об успешном завершении операции.

Когда fsync действительно нужен, а когда можно без него

Базы данных — обязательно. Любая СУБД с транзакционным журналом (write-ahead log) обязана делать fsync() записи в журнал до подтверждения транзакции клиенту — иначе смысл транзакционности теряется. PostgreSQL (synchronous_commit) и MySQL/InnoDB (innodb_flush_log_at_trx_commit) по умолчанию делают синхронный fsync() на каждый commit ради надёжности. О том, как это ограничивает скорость записи, — в статье почему важна скорость диска для баз данных.

Критичные конфиги и состояние — да. Файлы, потеря последних изменений в которых означает реальную проблему: конфигурация после важных изменений, файлы блокировок, журналы аудита, персистентные очереди (Redis с appendonly и appendfsync always, RabbitMQ с persistent-очередями).

Логи, временные файлы, кеши — обычно нет. Если потеря последних секунд лога при сбое не критична, платить производительностью за fsync() на каждую запись смысла нет — асинхронная запись через page cache здесь правильный компромисс.

Периодический fsync — промежуточный вариант: не на каждую операцию, а раз в интервал или объём данных (так делает checkpoint в PostgreSQL). Это ограничивает окно потенциальной потери разумным промежутком, не убивая производительность полностью.

Технически fsync можно и отключить целиком (fsync = off в PostgreSQL) ради скорости — но это прямой обмен надёжности на скорость: при сбое возможна не просто потеря последних изменений, а повреждение внутренней структуры данных, которое потребует восстановления из бэкапа. Оправдано только для заведомо некритичных, легко пересоздаваемых данных.

dirty_ratio, dirty_background_ratio и потеря питания

У ядра есть встроенные пороги, определяющие, когда оно само начинает сбрасывать dirty-страницы, не дожидаясь явного fsync():

sysctl vm.dirty_ratio vm.dirty_background_ratio
  • vm.dirty_background_ratio (в процентах от памяти) — при достижении порога ядро запускает фоновый сброс dirty-страниц, не блокируя процессы, которые продолжают писать. Мягкий порог, незаметный для приложений.
  • vm.dirty_ratio — более высокий порог; при его достижении процесс, пытающийся писать ещё больше данных, сам блокируется, пока не освободится место под новые dirty-страницы. Это уже заметно — запись внезапно «тормозит».

Есть и варианты в абсолютных байтах — vm.dirty_bytes/vm.dirty_background_bytes, взаимоисключающие с процентными. Более высокие значения этих порогов дают лучшую производительность на всплески записи (больше шансов объединить мелкие записи в крупные), но увеличивают и объём данных, теряемых при внезапном сбое, и потенциальную длительность паузы при вынужденном flush. Конкретные значения по умолчанию зависят от дистрибутива и версии ядра — их стоит проверять sysctl на своей системе, а не полагаться на общие цифры; на серверах с СУБД под интенсивной записью часто есть смысл их снизить, но это тонкая настройка, которую стоит тестировать под конкретную нагрузку.

При потере питания риск не ограничивается только page cache: собственный кеш записи накопителя (буфер контроллера SSD/NVMe) тоже может держать данные, ещё не попавшие в энергонезависимую память носителя, даже после успешного fsync() на уровне ОС. Серверные накопители с защитой от потери питания (конденсаторы, дающие контроллеру время дописать буфер при обесточивании) закрывают именно этот риск — это стоит уточнять у поставщика оборудования, если данные критичны. Более общий разбор рисков отключения питания — в статье про резервный блок питания и отказоустойчивость, а если сбой уже случился и часть данных повреждена — пригодится восстановление после сбоя диска.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Если сервер годами не выключался аварийно, значит ли это, что fsync не нужен?

Нет — вопрос не в частоте сбоев, а в цене одного такого события. Питание, ядро и гипервизор всё равно иногда падают непредсказуемо, а откат транзакции в базе на несколько минут назад может быть куда дороже постоянных небольших накладных расходов на fsync().

Почему cp большого файла иногда «зависает» в конце?

Часто это финальный fsync()/close(), синхронно ждущий сброса накопившихся dirty-страниц — до этого момента cp быстро копировал данные в page cache, и казалось, что всё идёт мгновенно, а реальная запись на диск проявляется именно в конце.

Отличается ли поведение dirty pages на SSD/NVMe от HDD?

Сама механика page cache в ядре одинакова для любого накопителя — разница только в скорости самой операции flush. На NVMe пауза от накопленного flush обычно короче, но природа риска (потеря несинхронизированных данных при сбое) от типа диска не зависит.

Как понять, сколько времени займёт flush большого объёма dirty-данных?

Точное время зависит от объёма накопленных страниц и реальной скорости записи конкретного накопителя под конкретной нагрузкой — универсальной цифры здесь нет, но общий ориентир: чем выше vm.dirty_ratio и интенсивнее запись, тем больше данных накопится к моменту принудительного flush и тем дольше он может длиться.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →