Сообщение «syncing to disk» в журнале базы данных или зависшая на несколько секунд операция сохранения файла почти всегда означают одно: приложение принудительно сбрасывает данные из оперативной памяти на физический носитель, и это намеренно медленный шаг. Когда программа вызывает write(), данные вовсе не попадают на диск мгновенно — они оседают в страничном кэше операционной системы, и фактическая запись откладывается. Именно поэтому внезапное отключение питания способно уничтожить файл, который редактор секунду назад показывал как «сохранённый».
В этой статье разберём, что технически происходит при синхронизации на диск, чем отличаются fsync, fdatasync и сброс буферов самого накопителя, почему базы данных вроде PostgreSQL и SQLite уделяют этому столько внимания и какие настройки влияют на надёжность записи. Материал пригодится разработчикам, администраторам и пользователям, которые хотят понять, откуда берутся «пропавшие» файлы после сбоя.
Что происходит между вызовом записи и реальным попаданием данных на диск
Когда приложение записывает файл, операционная система почти никогда не отправляет байты на устройство сразу. Сначала данные копируются в страничный кэш (page cache) в оперативной памяти. Это даёт огромный выигрыш в скорости: запись в RAM в разы быстрее записи даже на современный NVMe-накопитель, не говоря уже о жёстких дисках.
Ядро помечает такие страницы как «грязные» (dirty pages) и сбрасывает их на диск позже — по таймеру, при нехватке памяти или по явному запросу. Промежуток между логической записью и физической может составлять секунды и десятки секунд. Если в этот момент пропадёт питание или произойдёт kernel panic, содержимое кэша исчезнет безвозвратно.
Важно понимать и второй уровень буферизации: у самого накопителя есть собственный кэш записи. Данные, которые ОС считает записанными, могут ещё лежать во внутренней памяти SSD или HDD. Полноценная синхронизация должна протолкнуть данные через оба уровня.
fsync, fdatasync и родственные механизмы
Основной инструмент принудительной синхронизации в Unix-подобных системах — системный вызов fsync(). Он блокирует выполнение программы до тех пор, пока ядро не подтвердит, что данные и метаданные файла переданы на устройство хранения. Именно эту операцию выполняют базы данных при фиксации транзакции.
Существует несколько вариаций с разной степенью строгости:
- 🔹 fsync() — сбрасывает данные и метаданные файла (размер, время изменения); самый надёжный и самый медленный вариант.
- 🔹 fdatasync() — сбрасывает только данные и те метаданные, без которых файл нельзя прочитать; немного быстрее, подходит для журналов.
- 🔹 sync() — инициирует сброс всех грязных страниц системы, но не гарантирует завершение записи к моменту возврата.
- 🔹 O_SYNC / O_DSYNC — флаги открытия файла, при которых каждая операция записи автоматически синхронизируется; удобно, но замедляет потоковую запись.
В Windows аналогичную роль играет функция FlushFileBuffers, а в приложениях на высокоуровневых языках — методы вроде flush() с последующим вызовом синхронизации файлового дескриптора. Обратите внимание: обычный flush() буфера потока в языке программирования выталкивает данные лишь из буфера приложения в кэш ОС, но не на диск.
import os
f = open("data.txt", "w")
f.write("важные данные")
f.flush() # сброс буфера Python в ОС
os.fsync(f.fileno()) # принудительная запись на диск
f.close()
Почему синхронизация медленная и как это влияет на производительность
Каждый вызов fsync — это физическая операция. На жёстком диске головке нужно позиционироваться, на SSD — выполнить запись страниц NAND и обновить таблицы трансляции. Пока устройство не подтвердит завершение, приложение ждёт. Поэтому база данных, делающая fsync после каждой транзакции, обрабатывает их заметно медленнее, чем при отложенной записи.
Отсюда классический компромисс durability vs throughput: чем чаще синхронизация, тем надёжнее, но медленнее. Многие СУБД позволяют настроить этот баланс. Например, в PostgreSQL параметр synchronous_commit управляет ожиданием сброса WAL на диск, а в SQLite режим synchronous задаётся прагмой PRAGMA synchronous = FULL|NORMAL|OFF.
| Подход к записи | Скорость | Риск потери данных при сбое | Типичное применение |
|---|---|---|---|
| fsync после каждой операции | Низкая | Минимальный | Финансовые транзакции, журналы БД |
| Групповая синхронизация (group commit) | Средняя | Низкий | Нагруженные СУБД |
| Периодический сброс по таймеру | Высокая | Потеря данных за интервал | Логи приложений, кэши |
| Без явной синхронизации | Максимальная | Высокий | Временные файлы, пересоздаваемые данные |
⚠️ Внимание: отключение синхронизации ради скорости (например, PRAGMA synchronous = OFF или небезопасные настройки СУБД) делает базу уязвимой к повреждению при любом внезапном отключении питания. Применяйте такие режимы только к данным, которые можно восстановить из другого источника.
Синхронизация в базах данных: WAL и журналирование
Современные СУБД решают проблему надёжности через журнал предзаписи (WAL, write-ahead log). Идея проста: прежде чем изменять основные файлы данных, система записывает описание изменения в последовательный журнал и синхронизирует его на диск. После сбоя база «проигрывает» журнал и восстанавливает консистентное состояние.
Запись в журнал последовательная, а не случайная, поэтому она значительно дешевле по времени, чем синхронизация разрозненных страниц данных. Это позволяет достичь высокой надёжности без катастрофической просадки производительности. Дополнительную оптимизацию даёт group commit: несколько транзакций объединяются в один вызов fsync, распределяя его стоимость.
Если вы администрируете СУБД, проверьте, какие параметры отвечают за синхронизацию в вашей системе, и не меняйте их вслепую. Точные названия и допустимые значения различаются между версиями — сверяйтесь с официальной документацией конкретной версии вашей СУБД.
Почему fsync иногда «врёт»
Исторически встречались накопители и RAID-контроллеры, которые подтверждали завершение записи до фактического попадания данных в энергонезависимую память — ради красивых бенчмарков. Защититься от этого помогают устройства с конденсаторами защиты от потери питания (power-loss protection) у enterprise-SSD и батарейки у RAID-контроллеров. Проверить поведение конкретного диска можно только тестами с реальным отключением питания.
Практическая проверка и диагностика
Хотите увидеть разницу своими глазами? Сравните скорость записи файла с fsync после каждого блока и без него. Разрыв может достигать десятков и сотен раз в зависимости от накопителя. В Linux посмотреть объём грязных страниц помогает файл /proc/meminfo — поля Dirty и Writeback показывают данные, ожидающие сброса.
☑️ Проверка надёжности записи данных
Для наблюдения за активностью сброса в Linux пригодятся утилиты вроде iostat и pidstat, а для трассировки системных вызовов конкретного процесса — strace -e trace=fsync,fdatasync -p PID. Это помогает понять, как часто приложение реально синхронизирует данные, а не как заявлено в его документации.
⚠️ Внимание: тесты с отключением питания выполняйте только на тестовом стенде с копиями данных. Принудительное обесточивание рабочей системы само по себе способно повредить файловую систему, если синхронизация настроена неверно.
Типичные ошибки при работе с синхронизацией
Даже опытные разработчики регулярно наступают на одни и те же грабли. Вот что стоит проверить в первую очередь:
- ❌ Путать
flush()буфера потока с fsync — первый не гарантирует запись на диск. - ❌ Забывать синхронизировать каталог после создания или переименования файла.
- ❌ Вызывать fsync на неправильном файловом дескрипторе или игнорировать его код возврата.
- ❌ Полагаться на то, что «SSD и так быстрый» — без fsync данные всё равно остаются в кэше ОС.
- ❌ Отключать барьеры записи на файловой системе, не понимая последствий.
Отдельная ловушка — игнорирование ошибок fsync. Если устройство вернуло сбой записи, а приложение не обработало код возврата, данные могут быть потеряны молча. Всегда проверяйте результат вызова и предусматривайте сценарий повторной попытки или аварийного завершения.
Когда синхронизация не нужна
Не всякая запись требует немедленного сброса на диск. Временные файлы, промежуточные результаты вычислений, кэши, которые можно пересчитать, — всё это выгоднее писать без синхронизации, доверив ядру отложенную запись. Осознанный отказ от fsync там, где потеря данных некритична, освобождает ресурсы накопителя для действительно важных операций.
Правильный вопрос звучит не «как сделать всё максимально надёжным», а «каковы последствия потери последних секунд записи». Для лога отладки ответ один, для платёжной транзакции — совершенно другой. Проектируйте политику синхронизации исходя из цены данных.
Часто задаваемые вопросы
Что означает сообщение «syncing to disk» в логах приложения?
Приложение выполняет принудительный сброс буферов на накопитель — чаще всего через fsync или аналог. Это нормальная операция, но если она занимает заметное время и повторяется часто, стоит проверить нагрузку на дисковую подсистему и настройки синхронизации.
Чем flush отличается от fsync?
flush выталкивает данные из буфера приложения или библиотеки в операционную систему, но не дальше. fsync заставляет ОС передать данные на физическое устройство и дождаться подтверждения. Только второй вариант защищает от потери данных при отключении питания.
Почему после сбоя питания пропал файл, который я успел сохранить?
Скорее всего, редактор записал данные в кэш ОС и показал «сохранено», но физическая запись не успела выполниться. Защититься помогают редакторы, выполняющие fsync при сохранении, журналируемые файловые системы и источник бесперебойного питания.
Замедляет ли fsync работу SSD?
Да, каждая синхронизация — это ожидание подтверждения от накопителя. На SSD задержка меньше, чем на HDD, но при интенсивной частой синхронизации она всё равно ограничивает общую пропускную способность. Оптимизация обычно заключается в группировке операций, а не в отказе от надёжности.
Как проверить, делает ли моя программа fsync?
В Linux используйте strace с фильтром по вызовам fsync и fdatasync, подключившись к работающему процессу. Также можно изучить исходный код или конфигурацию приложения: для СУБД настройки синхронизации обычно описаны в документации конкретной версии.