LINUX.ORG.RU

Проблемы надёжной доставки данных до постоянного хранилища и fsync()

 , ,


1

2

Когда речь идёт об организации надёжного хранения данных на диске, возникает проблема: надо как-то узнавать о том, что данные на диск не смогли записаться, и принимать по этому поводу какие-то меры.
Есть, конечно, и другая проблема: даже если данные на самом деле записались, они всё равно могут позже потеряться из-за аппаратного сбоя. Но это другая тема и тут мы не будем её рассматривать.

Для начала простые факты по теме, начиная с самых очевидных.

  1. Надо проверять значения, возвращаемые функциями записи, ведь там может вернуться ошибка или неполная запись.

  2. Даже если fwrite() сообщил об успешном выполнении и записал столько же байт, сколько мы ему запросили — это ещё ничего не гарантирует, ведь stdio кеширует записи и откладывает их по возможности на потом — то есть если после успешного fwrite() программа тупо упадёт (сегфолт, kill -9 или просто вызов _exit()), то запись молча потеряется. А ещё fwrite() по той же причине не сообщит нам даже от такой очевидной проблеме как disk full. Для того, чтобы принудительно отослать этот отложенный кеш ядро (и узнать ядерную диагностику ошибок, если она будет), есть функция fflush(), но, в целом, сражаться с надёжностью stdio-функций выходит себе дороже, поэтому в ответственных местах лучше ими не пользоваться, а пользоваться нативным вводом-выводом ОС. Про него следующий пункт.

  3. Даже если системный вызов write() сообщил об успехе — это всё ещё мало что гарантирует. Единственное, что гарантируется — это то, что ядро наши данные приняло к сведению и по крайней мере краш процесса уже не станет причиной их потери. Но данные, скорее всего, всё так же где-то закешированы для отложенной записи, на этот раз в ядре. Так что если упадёт вся система (kernel panic или аварийное выключение железа без участия ОС) — данные скорее всего всё так же потеряются. Хотя, по сравнению с writeback-кешированием stdio тут есть и ещё одно улучшение: ядро, даже без нашей явной команды, данные рано или поздно всё-таки запишет. Так что опасения про падение системы тут относятся только к некоторому недолгому времени после write(). Ещё одной причиной незаписи данных может быть неисправность накопителя или связи с ним. Некоторые программисты ожидают, что в случае такой неисправности write() вернёт им EIO или что-то подобное, но этого не случится из-за того, что физическая запись откладывается. Хотя, иногда может и случиться: если операционная система на момент write() уже обнаружила проблему связи с диском, то может вернуть и EIO, но рассчитывать на такую удачу конечно же не стоит. Своевременная (прямо в write()) диагностика disk full тут уже более вероятна, но всё ещё не гарантирована.


Решение указанной проблемы с write() уже не такое тривиальное, как вышеописанные баяны. И, поскольку главная ожидаемая опасность это всё-таки краш приложения, а проблемы с ОС и железом считаются чем-то выходящим за рамки ответственности прикладного программиста, на это часто забивают. Во многих случаях оправданно, потому как после такого сбоя программу, скорее всего, просто запустят заново с нуля. Да что уж говорить, даже на проблемы, указанные в п.2, часто тоже забивают по той же причине. Однако если мы всё-таки хотим минимизировать риск оставить после себя некий файл (или файлы) в неконсистентном состоянии, надо что-то предпринимать.

Сразу скажу, что 100% идеального решения нет, writeback-кеши могут обнаружиться много где в совершенно неподконтрольных приложению местах: в самом накопителе, в рейд-контроллере, в инфраструктуре сетевого хранилища, в гипервизоре виртуалки. Возможность их «потерять» зависит от качества изделия, содержащего кеш. У накопителя, в целом, есть возможность всё записать при признаках пропадания питания, у хороших контроллеров вообще есть своё автономное питание, позволяющее сохранить незаписанные данные на длительный срок. Но закончим о внешних проблемах.

Ошибки, которые были неизвестны на момент возврата из write(), могут быть сообщены ядром при close(), о чём есть например намёки в man 2 close как в Linux так и в FreeBSD (и, вероятно, других ОС). С другой стороны, в том же man 2 close Linux-а прямо заявлено, что close не гарантирует сброс кешей записи (если что, POSIX ничего на этот счёт не требует) и считать его точкой, где можно забрать все pending ошибки, не следует. Есть и другая проблема: даже если close вернёт ошибку, не очень понятно что с ней делать — дескриптор всё равно уже закрыт (кроме EINTR, который ужасно некроссплатформенно может оставить дескриптор открытым, например в HP-UX, и этим сделать чрезвычайно проблемным корректное кроссплатформенное использование close() в мультитреде).

Стоит упомянуть флаг O_DIRECT для open(), который вроде бы должен запрещать отложенную запись и заставлять ядро все ошибки сразу сообщать приложению, но он не стандартный и не кроссплатформенный, хотя и есть и в Linux и в FreeBSD и ещё в некоторых системах, но единство семантики никем не гарантируется, в частности например в Linux до 2.4.10 он молча игнорировался. При этом он точно снижает скорость работы (если не игнорируется) и имеет ряд платформенно-зависимых и даже файлосистемно-зависимых оговорок по его использованию (см. man 2 open).

Системные вызовы fsync(), fdatasync(), флаги O_SYNC, O_DSYNC, O_FSYNC для open(). Самое поддерживаемое тут fsync(). fdatasync() отсутствует в FreeBSD до версии 11.1 и, хоть и присутствует, но делает то же самое что и fsync() в Linux версий 2.2 и раньше. O_SYNC по идее означает что после каждого write() делается неявный fsync(), O_DSYNC - после каждого write() делается неявный fdatasync(). O_FSYNC это BSD синоним к POSIX стандартному O_SYNC. O_DSYNC не поддерживается в FreeBSD. fsync() означает: записать на физический носитель (точнее, отправить железу или сетевому хранилищу) все ещё не отправленные данные и метаданные, связанные с файлом. fdatasync() отправляет только данные, но не метаданные. При этом эти функции вернут все ошибки, которые могут возникнуть в ходе выполнения отложенных записей. Если делались переименования файлов и подобное, обязателен ещё fsync() на директорию, их содержащую.


Вроде бы, вот оно решение проблемы (в рамках софта), но не тут то было. Во-первых, на старых Linux-ах и на необычных файловых системах эти функции могут вообще саботировать свою работу. Во-вторых, fsync по факту тоже может молча прятать ошибки.

Начнём с того, что POSIX не особо строго что-то требует от fsync(), поэтому проблемное поведение вполне в него укладывается. А именно, его требования таковы:

  1. передать железу всё связанное с файлом, что пока что стоит в очереди, и не возвращаться из функции пока это не будет сделано;

  2. вернуть -1 и errno если в ходе записей произошла ошибка;

  3. собственные errno: EBADF, EINVAL, EINTR, EIO, кроме того могут быть любые от read()/write(); этот разрешённый EINTR дополнительно усугубляет проблему; EBADF/EINVAL тут не важны т.к. это по сути ошибки не самого fsync() а некорректного аргумента ему от приложения.

Пункты 1 и 2 в целом аналогичны в манах от Linux-а и FreeBSD.

Пункт 3 (список ошибок) отличается. Что в Linux-е: EINTR в списке нет — это радует (см. ниже почему), добавилось EROFS для попыток fsync для сокета/пайпа (аналог EINVAL), добавилось ENOSPC/EDQUOT (а это значит, что write() и правда может не знать про закончивийся диск). FreeBSD: EINTR тоже нет, зато добавилось EINTEGRITY (то ли в 11.х то ли в 12.х) — «обнаружена битая файловая система».

В чём проблема: fsync() действительно отправляет те блоки, что стоят в очереди, и действительно возвращает -1 если с их отправкой возникли проблемы. Но вот те блоки, которые ядро ОС уже отправило раньше, и про которые уже сообщило приложению ошибку, он заново слать не обязан, и эту старую ошибку он помнить тоже не обязан. В частности, Linux именно так и делал как минимум в районе 2018 года (и, по косвенным признакам, исправлять это не планировалось, так что, вероятно, и сейчас так) — старую ошибку забывал, на что наткнулись в 2018 году разработчики PostgreSQL [1] и пришли к выводу, что единственный вариант безопасно обработать ошибку fsync() - это паника (не ядра, базы).

Поясню подробнее. Допустим, программа сделала write(), затем сделала fsync(), из которого было получено -1 как сигнал ошибки (хорошо, что хотя бы не EINTR, который есть только в POSIX, но допустим ENOSPC — ошибка явно не фатальная, впрочем, ошибок-то может быть несколько, но fsync() вернёт только одну из них), что ей делать дальше? [2] Можно наивно попытаться сделать fsync() ещё раз, но, как уже выяснено, второй fsync может тупо забыть про недописанные первым блоки и сказать что всё хорошо, ничего не сделав. Можно усложнить обработчик: сделать ещё раз write() (для чего придётся дублировать весь writeback-кеш в памяти приложения до тех пор, пока не убедимся что он записан), и ещё раз fsync(). Тут шансов на успех больше, однако всё равно без гарантий: у ядра в памяти уже лежат обновлённые блоки, на диске этих блоков нет, но этот факт должным образом нигде не зафиксирован. Хорошо, если это блоки данных (хотя кто мешает файловой системе сравнить новое записываемое с содержимым кеша и «передумать» записывать его на диск т.к. вроде уже записано?), а если это ещё и метаданные — то способов напрямую повлиять на ситуацию с неудавшейся и забытой записью практически нет. В Linux до 4.13 было ещё хуже — даже первый fsync() мог вернуть «всё хорошо», если какая-то другая программа его успела перед этим сделать на тот же файл. В версиях 4.13-4.16 хотя бы это исправили.

Даже если конкретно в Linux-е это исправят (или исправили), это не решает общую проблему: POSIX действительно не требует от fsync() консистентного поведения, и на разных немейнстримных системах может быть то же самое. Радует, что аналогичное поведение в FreeBSD таки было посчитано багом ещё в 1999 году и тогда же исправлено. Поведение других систем можно посмотреть в [4].

Ссылки:

[1] Тема в мейллисте PostgreSQL: https://postgrespro.com/list/thread-id/2379543

[2] Обширное исследование на тему проблем fsync в Linux с ext4/xfs/btrfs (англ): https://ramalagappan.github.io/pdfs/papers/cuttlefs.pdf https://www.usenix.org/system/files/atc20-rebello.pdf

[3] Тема на stackoverflow: https://stackoverflow.com/questions/42434872/writing-programs-to-cope-with-i-o-errors-causing-lost-writes-on-linux

[4] Статья в вики PostgreSQL https://wiki.postgresql.org/wiki/Fsync_Errors

★★★★★

Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 2)

У накопителя, в целом, есть возможность всё записать при признаках пропадания питания

Это вряд ли - будет аварийно парковаться, а не пытаться быстренько разбросать по блинам до 512 МБ (131072 4K-блоков).

Разве что, производитель добавил ещё энергонезависимый кэш (флэш), и диску хватает энергии перебросить в него данные 1-в-1. А уже при возобновлении подачи питания данные из него сбрасываются на блины (например, persistent write cache (PWC) у TOSHIBA MG04/05/06/07/08/09/10, начиная с 2TB 512e / 6 TB).

gag ★★★★★
()

… старую ошибку забывал, на что наткнулись в 2018 году разработчики PostgreSQL [1]

В 2017-ом. И, оказывается, не разработчики, а один контрибьютер. А годом спустя он уже поднял шум в списке рассылок разработчиков.

gag ★★★★★
()

Радует, что аналогичное поведение в FreeBSD таки было посчитано багом ещё в 1999 году и тогда же исправлено.

Да-да. А потом через 18 лет в 2017 году выяснилось, что всё таки от этих буферов надо избавляться, особенно, когда ФС отмонтирована, а не держать их пока вся память не исчерпается.

gag ★★★★★
()
Ответ на: комментарий от gag

Если устройство уже пропало, то записать их уже точно не получится, а открытые на устройстве файлы можно спокойно фейлить. Это совсем другая ситуация. Речь была про обычное EIO которое со временем может починиться, файлы останутся безразрывно открытыми, но ОС тем не менее решила выкинуть блоки.

пока вся память не исчерпается

Там не в памяти дело было, всё намного хуже - могло и kernel panic случиться. Впрочем у FreeBSD вообще не так всё гладко с обработкой полностью пропавшего подмонтированного устройства, даже до сих пор вроде можно панику поймать иногда, но уже редко. Ну, серверная специфика - принято избыточность делать на всё важное (миррор например где отвал одного из двух дисков не повлияет на работу ОС), и мало кому нужна была ситуация «у нас развалилось абы как сделанное железо но система критичная и должна продолжать работать».

firkax ★★★★★
() автор топика

O_DIRECT + fsync

Тогда пейдж кэш и повтор записей не у дел

При ошибке fsync падать и восстанавливаться с нуля

Либо повторять все записи самостоятельно, да

vitalif ★★★★★
()

Вот видишь, к чему приводит переусложнение систем и комплексов.

Mirage1_
()

Как вариант – делать алгоритмы, не восприимчивые к такого рода вещам. Т.е. никогда не рассчитывать, что данные реально запишутся на диск, а продолжать работать с тем, что получилось. Понятное дело, что это общий подход, а конкретная реализация уже будет зависеть от конкретной решаемой задачи.

Но, если задачу ставить так, что бы на все сто запись на накопитель должна быть гарантированной, тогда тут нужно учитывать и делать всю цепочку: либы/ОС/драйверы/кэши/канал связи/сам накопитель, и что бы конечный элемент этой цепочки отвечал прикладной программе, что «записал» и «записал точно то, что просили», и все промежуточные элементы этой цепочки обновили свои скэшированные данные.

Vic
()
Последнее исправление: Vic (всего исправлений: 1)
Ответ на: комментарий от Vic

Но, если задачу ставить так, что бы на все сто запись на накопитель должна быть гарантированной

Вас ещё не просили хранить критичные данные в нескольких geolocations? На каждом коммите?

bugfixer ★★★★
()
Ответ на: комментарий от Vic

Да это вроде как и не тема данной статьи.

А Вы молодец - я правильно понимаю что сноски тянут на решение проблемы? Или под «статьёй» что-то ещё подразумевалось?

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Эти вопросы лучше адресовать автору заметки (раз вам не нравится слово статья, размещенная в www.linux.org.ru/articles/).

Не претендую на истину, но на сколько я смог во всем этом разобраться за годы работы программистом, проблемы, описанные в данной заметке на лоре, не имеют общего технического решения, потому что они логические и встречаются повсеместно, а не только в ОС на базе Linux.

Да и для многих это вовсе не проблемы, а так, технические особенности, которые, если нужно, то можно учесть при создании программы. Но зачастую так заморачиваться не нужно.

Vic
()
Ответ на: комментарий от Vic

проблемы, описанные в данной заметке на лоре, не имеют общего технического решения

Шутите? Да конечно имеют. Нужна гарантированность записи? Мы вам её предоставим. Вопрос сколько вы за это готовы платить, и сколько готовы ждать подтверждения конкретной транзакции.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Посмею себе заметить, что вы слишком эмоциональны и, похоже, не понимаете всю глубину проблемы, описанной в данной заметке.

Vic
()
Ответ на: комментарий от Vic

вы слишком эмоциональны

Ну, пусть будет так - вам виднее :)

не понимаете всю глубину проблемы, описанной в данной заметке

Дык, просветите!

ПыСы. У меня зарождается подозрение что вы являетесь виртуалом господина @firkax - слишком уж рьяно его точку зрения отстраиваете…

bugfixer ★★★★
()
Ответ на: комментарий от gag

Насколько мне известно, fsync посылает в накопитель специальную команду, по которой накопитель должен записать указанные данные в постоянную память. Конечно в теории накопитель может этого не делать, но это уже вредительство и я про такое не знаю, не думаю, что такое реально происходит, т.к. практически любая ФС при таком подходе будет рассыпаться очень часто после потери питания. Это всё же не происходит на практике.

vbr ★★★
()
Последнее исправление: vbr (всего исправлений: 1)

Сначала ты пишешь, что

Есть, конечно, и другая проблема: даже если данные на самом деле записались, они всё равно могут позже потеряться из-за аппаратного сбоя. Но это другая тема и тут мы не будем её рассматривать.

А потом зачем-то её всё же рассматриваешь.

то есть если после успешного fwrite() программа тупо упадёт (сегфолт, kill -9 или просто вызов _exit()), то запись молча потеряется.

ya-betmen ★★★★★
()
Ответ на: комментарий от ya-betmen

Аппаратный сбой - это имелось ввиду когда сектор на диске превратился в бэд, или вообще диск сдох. А всякие сегфолты и даже паники это програмные сбои, хоть они и могут быть следствием проблем с железом.

firkax ★★★★★
() автор топика
Ответ на: комментарий от bugfixer

Это не «плач», это предупреждение тем кто не в курсе, что не стоит доверять успешным ответам в этих случаях и что ответственность за целостность данных, если она действительно важна, лежит таки на приложении (ну или на СУБД, которая тоже приложение), а не на ОС, как обычно принято считать.

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от bugfixer

В заметке все очень хорошо написано, мне к ней добавить нечего, чтобы вам было как-то более понятнее.

Vic
()
Последнее исправление: Vic (всего исправлений: 1)
Ответ на: комментарий от firkax

Я про концепт. У тебя есть ось, под ней буфер, под ней фс, под ней вфс, под ней контроллер диска, под ним вендор наверняка понавертел ещё какого треша (не ручаюсь за правильную вложенность).

На каждом из уровней есть вероятность факапа.

ya-betmen ★★★★★
()
Ответ на: комментарий от ya-betmen

Да, всё так. Но все эти проблемы можно пытаться нейтрализовать во время записи. А одну единственную нельзя - когда данные уже физически записались на магнитную поверхность или ещё куда, ответственная программа давно про эту операцию забыла и может быть вообще выключена (а может выключен и весь комп), а диск вдруг размагничивается или вообще ломается от механической атаки на него. Тут речь, очевидно, не о доставке данных до носителя, а о глобальном планировании их хранения (бекапы итд). В заголовке же статьи указано, что речь именно про доставку.

firkax ★★★★★
() автор топика
Ответ на: комментарий от vbr

fsync посылает в накопитель специальную команду

Вариантов не так много: глобальный FLUSH CACHE (EXT), или FUA bit на конкретном write (правда из того что я вижу - последнее на [S]ATA’шных дисках всё ещё в ядре по дефолту выключено).

Конечно в теории накопитель может этого не делать, но это уже вредительство

Диск не должен, а вот HW RAID контроллеры (привет любителям софтовых RAID, не удержался) при наличии battery backup делают это целенаправленно :) Да, да, да - и это один из основных источников «ускорения», особенно с DB-like write loads.

bugfixer ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.