Если от вашей системы требуется надёжность, отказоустойчивость и детерминированность, знания системных механизмов — не роскошь, а необходимость.
Работа с файлами в Python кажется простой — open, read, write. Но на практике, особенно в системах с высокими требованиями к отказоустойчивости, стабильности и логированию, за банальными строками кода может скрываться целый мир проблем.
Сегодня разберём, как знание внутренностей Linux может помочь избежать потерь данных и облегчить отладку. Все примеры будут на Python, но применимы к любым языкам, работающим через POSIX-интерфейсы.
1. Буферизация: write() — не значит «записал»
Начнём с простого:
with open("log.txt", "a") as f:
f.write("Hello\n")
На первый взгляд — всё хорошо. Но этот код не гарантирует, что строка реально ушла на диск. Почему?
Этапы прохождения данных:
Буфер Python (
f.writeсохраняет строку в user-space).Вызов
flush()передаёт буфер в ядро ОС.Вызов
fsync() / fdatasync()просит ОС сбросить буфер ядра на диск.
Чтобы реально гарантировать сохранение:
with open("log.txt", "a") as f:
f.write("Hello\n")
f.flush()
os.fsync(f.fileno())
Но и это не всегда помогает.
Если на сервере включено write caching на уровне диска/SSD, то fsync() может завершиться успешно, а данные — остаться в кэше контроллера и быть утеряны при сбое питания.
Решение:
Использование устройств с power-loss protection.
Отключение write cache (
hdparm -W 0 /dev/sdX).Использование
O_DIRECTилиO_SYNC, если готовы пожертвовать производительностью.
Также важно отметить, что O_DIRECT/O_SYNC и flush()/fsync()/fdatasync() ухудшают производительность, и иногда нужно искать баланс между скоростью и надёжностью.
fsync()/fdatasync()
Явный вызов для сброса данных из кэша ядра на диск.fsync(): гарантирует запись данных + метаданных (размер, время модификации).fdatasync(): записывает только данные (если метаданные не критичны).-
O_SYNC
Каждыйwrite()ждёт физической записи на диск перед возвратом управления. Гарантирует сохранение данных + метаданных для каждой операции.
Пример:fd = os.open("file.txt", os.O_WRONLY | os.O_SYNC) os.write(fd, b"data") # Блокирует выполнение до записи на диск. os.close(fd) -
O_DIRECT
Данные пишутся напрямую в устройство, минуя кэш ядра.
Не гарантирует, что данные попали на физический диск (могут остаться в кэше контроллера). Требует выровненных буферов (адрес памяти, размер блока, смещение в файле кратны 512 байт или 4 KiB).
Для надёжности после записи всё равно нуженfsync().
Пример:# В Python сложно из-за требований к выравниванию: buf = bytearray(4096) # Выровненный буфер buf[:4] = b"data" fd = os.open("file.txt", os.O_WRONLY | os.O_DIRECT) os.write(fd, buf) os.fsync(fd) # Обязательно! os.close(fd)
2. Кто мой враг: логротация и потеря дескриптора
Представим, у вас работает процесс, логирующий в файл:
with open("service.log", "a") as log:
while True:
log.write("ping\n")
log.flush()
os.fsync(log.fileno())
time.sleep(1)
Администратор сервера запускает logrotate. Что произойдёт?
Старый
service.logпереименуется.Новый пустой
service.logсоздастся.Процесс продолжает писать в старый (переименованный) файл, потому что дескриптор остался тем же!
Как отследить это?
Проверяйте
os.stat()иos.fstat()— еслиinodeу файла изменился, лог был заменён.Или используйте
watchdog/inotifyдля мониторинга.
3. Проблема с O_APPEND и конкурентной записью
В многопоточной среде или при нескольких процессах, пишущих в файл, может возникнуть путаница, если не использовать O_APPEND. Режим 'a' в open() автоматически включает O_APPEND.
with open("data.txt", "a") as f:
f.write("chunk\n")
Но что если вы используете низкоуровневый API (os.open) и забыли os.O_APPEND?
fd = os.open("data.txt", os.O_WRONLY)
os.lseek(fd, 0, os.SEEK_END)
os.write(fd, b"chunk\n")
В многопроцессной среде вызовы lseek и write могут быть неатомарными, и два процесса могут перезаписать друг друга.
Решение:
Использовать
O_APPEND, чтобы смещение определялось ядром атомарно.Или синхронизировать доступ к файлу (lock-файлы,
fcntl.flock).
4. Пропущенные данные при падении процесса
Сценарий:
Вы пишете логи.
Используете
with open(...)иflush/fsync/fdatasync.Но процесс аварийно завершился до закрытия файла.
Проблема: если сбой произошёл до flush() или fsync() — вы теряете строку.
Решение:
Оборачивайте запись в лог с
try/finally:
f = open("log.txt", "a")
try:
f.write("data\n")
f.flush()
os.fsync(f.fileno())
finally:
f.close()
Или используйте
atexit.register()иsignal-хендлеры, чтобы корректно завершать процесс.
5. Watchdog и inotify — не панацея
Многие пытаются отслеживать изменения файлов через inotify, но забывают об ограничениях:
inotifyне работает по сети (NFS).Число слотов ограничено (
/proc/sys/fs/inotify/max_user_watches).Некоторые события (например,
fsync) не отслеживаются вообще.
В сложных случаях лучше использовать периодическое сравнение mtime/inode/size.
6. Отладка: когда strace спасает
Если данные иногда не пишутся, а код вроде правильный, запускайте процесс под strace:
strace -e trace=write,fsync,open,close -f -tt -o trace.log python myscript.py
Что искать:
Есть ли
writeвообще?Что происходит между
writeиfsync?Были ли ошибки
EIO,ENOSPCи т.п.?
7. Файлы в tmpfs или /dev/shm — быстро, но опасно
Иногда данные записываются в файл, а на диск не попадают, потому что файл был в памяти (например, /tmp на tmpfs).
Проверьте монтирование:
mount | grep /tmp
Решение:
Использовать
tmpтолько для кэша.Критичные файлы писать в реальную FS.
Вывод
Использование O_SYNC, O_DIRECT, fsync() всегда снижает производительность. Важно выбирать метод, исходя из требований к данным:
Для журналов транзакций —
fsync()после каждой записи.Для временных данных — буферизация без
fsync().
Большинство разработчиков живут в мире, где:
Один поток пишет в файл.
Ошибки редки.
Утилиты работают «как всегда».
Но как только вы заходите в зону высоких требований (финансы, аудит, логирование, отказоустойчивость), любые мелочи начинают "стрелять". Там важен каждый байт, каждый системный вызов и каждый дескриптор.
Чем глубже ваше понимание системы, тем легче вам и вашим пользователям.
А значит, вы — тот, кого сложно заменить.
Комментарии (13)

baldr
23.05.2025 07:45Про logrotate - проблема описана правильная, но среди решений нет варианта "наорать на администратора и рассказать ему о
copytruncate", хотя в голосовании видно что автор знает об этом.

outlingo
23.05.2025 07:45Если на сервере включено write caching на уровне диска/SSD, то fsync() может завершиться успешно, а данные — остаться в кэше контроллера и быть утеряны при сбое питания
Неверно. Этот os.fsync() это вызов функции операционной системы, а та при флашинге кэша активно отправляет записи с флагами PRE_FLUSH и FUA (force unit access) который соответственно сбрасывают кэши и пишут мимо кэша. То, что вы написали, бывает только если администратор умышленно стреляет себе в голову.

nv13
23.05.2025 07:45У нас было примерно два сервиса, один из которых управлял записью по nfs от одной железки, потом нотифицировал второй, который читал заголовок файла и логгировал его в базу. Пока не ввели пуллинг на консистентность заголовка на неск. десятков секунд - постоянно были ошибки валидации. И ничего кроме пуллинга было сделать нельзя

pae174
23.05.2025 07:45Как отследить это?
Проверяйте
os.stat()иos.fstat()— еслиinodeу файла изменился, лог был заменён.Или используйте
watchdog/inotifyдля мониторинга.
Зачем что-то проверять периодически внутри сервиса если есть механизм сигналов. Просто сделайте обработчик сгнала и в нем переоткрывайте логи. Так что если админ в какой-то момент переименует лог-файл то он же и пошлет сигнал на переоткрытие.
vadimr
Вообще держать лог открытым – плохая практика. Открыл – дописал строчку – закрыл.
baldr
Не соглашусь с вами. Открытие-закрытие - это накладные расходы. Писать мы можем часто - 100 строк в секунду или даже чаще, из разных потоков.
А ещё это может быть не просто текстовый лог, а, например, WAL-файл базы данных, который всё время открыт.
vadimr
Если вы пишете телеметрию 100 раз в секунду, то для этого лучше воспользоваться каким-нибудь специализированным средством, а не текстовым логом. Текстовый лог такого объёма не имеет практического смысла.
На мой взгляд, текстовый лог осмыслен тогда, когда его можно читать с консоли в реальном времени.
baldr
Снова не соглашусь с вами. И nginx тоже не согласится.
vadimr
По счастью, я далёк от веба, но если вы дисковый кеш будете синхронизировать 100 раз в секунду, то об эффективности вы можете забыть гораздо быстрее, чем при простом переоткрытии файла.
baldr
Я возражал, в первую очередь, на ваши замечания о целесообразности таких логов и необходимости постоянно открывать-закрывать.
Синхронизация - да, это отдельная проблема, и это проблема. Надо выбирать - либо скорость, либо консистентность. nginx не очень заботится о том что потеряется пара секунд логов, это проблема админов.
А вот с базой данных все сложнее. Помню как на меня наорал DBA, когда я спросил почему нельзя ставить Oracle на виртуалку хотя бы в тестовых окружениях.
outlingo
Любая система виртуализации имеет настройки кэширования, и они безопасны. Небезопасные режимы надо включать руками с приседаниями. Сделано специально с учетом среднего уровня специалистов :-)
baldr
Из того разговора я успел понять что Oracle - не просто любая база данных и они на <censored> вертели идею ловить непонятные ошибки в базе из-за того что какой-то <censored> с горы хочет сэкономить на железе.