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

139

TL; DR: Если ядро ​​Linux теряет буферизованную запись ввода-вывода , есть ли способ для приложения узнать?

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

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

Забыли пишет? Как?

Блок слой ли ядро в некоторых обстоятельствах теряют буферном запросы ввода / вывода , которые были успешно представленные write(), и pwrite()т.д., с сообщением об ошибке , как:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Смотрите end_buffer_write_sync(...)и end_buffer_async_write(...)вfs/buffer.c ).

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

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Поскольку приложение write()уже вернулось без ошибок, похоже, нет способа сообщить об ошибке обратно в приложение.

Обнаружить их?

Я не так хорошо знаком с исходными кодами ядра, но думаю, что он устанавливает AS_EIOбуфер, который не может быть записан, если он выполняет асинхронную запись:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

но мне неясно, может ли приложение узнать об этом и каким образом, когда оно позже отправит fsync()файл, чтобы подтвердить его наличие на диске.

Похоже, wait_on_page_writeback_range(...)вmm/filemap.c мощи, do_sync_mapping_range(...)вfs/sync.c которой зовут очередь sys_sync_file_range(...). Он возвращается, -EIOесли не удалось записать один или несколько буферов.

Если, как я предполагаю, это распространяется на fsync()результат, то, если приложение паникует и выходит из строя, если оно получает ошибку ввода-вывода fsync()и знает, как заново выполнить свою работу при перезапуске, этого должно быть достаточной защиты?

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

Существуют ли какие-либо другие, безобидные обстоятельства, при которых fsync()может произойти возвращение, -EIOкогда спасение и повторная работа были бы слишком радикальными?

Зачем?

Конечно, таких ошибок быть не должно. В этом случае ошибка возникла из-за неудачного взаимодействия между dm-multipathнастройками драйвера по умолчанию и сенсорным кодом, используемым SAN для сообщения о невозможности выделения хранилища с тонким предоставлением. Но это не единственное обстоятельство, при котором они могут произойти - я также видел отчеты об этом, например, из LVM с тонкой подготовкой, который используется libvirt, Docker и другими. Важное приложение, такое как база данных, должно пытаться справиться с такими ошибками, а не слепо продолжать работу, как будто все в порядке.

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

Практическое влияние состоит в том, что я обнаружил случай, когда проблема с несколькими путями в SAN вызвала потерянные записи, которые привели к повреждению базы данных, потому что СУБД не знала, что ее запись была неудачной. Не смешно.

Крэйг Рингер
источник
1
Боюсь, что для хранения и запоминания этих условий ошибки потребуются дополнительные поля в SystemFileTable. И возможность для процесса пользовательского пространства получать или проверять их при последующих вызовах. (возвращают ли fsync () и close () такую историческую информацию?)
joop
@joop Спасибо. Я только что опубликовал ответ о том, что, как мне кажется, происходит, не забудьте проверить работоспособность, поскольку вы, кажется, знаете больше о том, что происходит, чем люди, которые опубликовали очевидные варианты «write () требуется close () или fsync ( ) на прочность »не читая вопрос?
Крейг Рингер,
Кстати: я думаю, вам действительно стоит углубиться в исходники ядра. Журналируемые файловые системы, вероятно, столкнутся с такими же проблемами. Не говоря уже об обработке раздела подкачки. Поскольку они находятся в пространстве ядра, обработка этих условий, вероятно, будет немного более жесткой. writev (), который виден из пользовательского пространства, также кажется местом, где стоит поискать. [у Крэйга: да, потому что я знаю ваше имя, и я знаю, что вы не полный идиот; -]
joop
1
Согласен, я был не таким справедливым. Увы, ваш ответ не очень удовлетворительный, я имею в виду, что нет простого решения (удивительно?).
Жан-Батист Юнес, 02
2
@ Jean-BaptisteYunès Верно. Для СУБД, с которой я работаю, допустимо "сбой и повторный ввод". Для большинства приложений это не вариант, и им, возможно, придется мириться с ужасной производительностью синхронного ввода-вывода или просто соглашаться с плохо определенным поведением и повреждением при ошибках ввода-вывода.
Craig Ringer

Ответы:

93

fsync()возвращается, -EIOесли ядро ​​потеряло запись

(Примечание: ранняя часть ссылается на старые ядра; обновлено ниже, чтобы отразить современные ядра)

Похоже, что асинхронная запись буфера при end_buffer_async_write(...)сбоях устанавливает -EIOфлаг на странице сбойного грязного буфера для файла :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

который затем обнаруживается , wait_on_page_writeback_range(...)как вызывается do_sync_mapping_range(...)при вызове , sys_sync_file_range(...)как вызывается sys_sync_file_range2(...)для выполнения вызова библиотеки С fsync().

Но только один раз!

Этот комментарий к sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

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

Конечно, wait_on_page_writeback_range(...) при тестировании биты ошибок очищаются :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Поэтому, если приложение ожидает, что оно может повторять попытку fsync()до тех пор, пока оно не увенчается успехом, и полагает, что данные находятся на диске, это ужасно неправильно.

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

Это разрешено?

Документы POSIX / SuS наfsync() самом деле не указывают этого в любом случае:

Если функция fsync () дает сбой, выполнение невыполненных операций ввода-вывода не гарантируется.

На странице руководства Linuxfsync() ничего не говорится о том, что происходит в случае сбоя.

Похоже, что смысл fsync()ошибок - «не знаю, что случилось с вашими записями, возможно, сработало или нет, лучше попробуйте еще раз, чтобы убедиться».

Новые ядра

На 4.9 end_buffer_async_writeнаборы -EIOна страничке, просто через mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Что касается синхронизации, я думаю, что это похоже, хотя структура теперь довольно сложна. filemap_check_errorsв mm/filemap.cнастоящее время делает:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

который имеет примерно такой же эффект. Кажется, что все проверки ошибок проходят через filemap_check_errorsпроверку и очистку:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Я использую btrfsна своем ноутбуке, но когда я создаю ext4шлейф для тестирования /mnt/tmpи настраиваю на нем датчик производительности:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Я нахожу следующий стек вызовов perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Читка предполагает, что да, современные ядра ведут себя так же.

Похоже, это означает, что если fsync()(или предположительно write()или close()) возвращается -EIO, файл находится в каком-то неопределенном состоянии между тем, когда вы в последний раз успешно fsync()d или close()d, и его последним write()состоянием десять.

Контрольная работа

Я реализовал тестовый пример, чтобы продемонстрировать это поведение .

Последствия

СУБД может справиться с этим путем восстановления после сбоя. Как, черт возьми, обычное пользовательское приложение должно с этим справиться? На fsync()странице руководства нет предупреждений, что это означает «fsync-if-you-feel-like-it», и я ожидаю, что многие приложения не справятся с таким поведением.

Отчеты об ошибках

дальнейшее чтение

lwn.net затронул это в статье «Улучшенная обработка ошибок блочного уровня» .

Тема списка рассылки postgresql.org .

Крэйг Рингер
источник
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 - это возможная гонка, потому что она ожидает {ожидающего и запланированного ввода-вывода}, а не {еще не запланированного ввода-вывода}. Это очевидно, чтобы избежать лишних обращений к устройству. (Я предполагаю, что пользователь write () не возвращается, пока не будет запланирован ввод-вывод, для mmap () это другое)
joop
4
Возможно ли, что вызов fsync каким-либо другим процессом для какого-либо другого файла на том же диске возвращает ошибку?
Random832
4
@ Random832 Очень актуально для многопроцессорной БД, такой как PostgreSQL, поэтому хороший вопрос. Похоже, вероятно, но я недостаточно хорошо знаю код ядра, чтобы понять. Тем не менее, вашим процессам лучше сотрудничать, если у них обоих открыт один и тот же файл.
Крейг Рингер,
1
@DavidFoerster: системные вызовы возвращают сбои с использованием отрицательных кодов ошибок; errnoполностью является конструкцией библиотеки C пользовательского пространства. Различия в возвращаемых значениях между системными вызовами и библиотекой C обычно игнорируют (как это делает Крейг Рингер выше), поскольку возвращаемое значение ошибки надежно идентифицирует, к какому из них (системный вызов или библиотечная функция C) идет ссылка: « -1с errno==EIO"относится к библиотечной функции C, тогда как" -EIO"относится к системному вызову. Наконец, страницы руководства Linux в Интернете являются наиболее свежими справочниками по страницам руководства Linux.
Nominal Animal
2
@CraigRinger: Чтобы ответить на ваш последний вопрос: «Используя низкоуровневый ввод-вывод и fsync()/ fdatasync()когда размер транзакции представляет собой полный файл; используя mmap()/, msync()когда размер транзакции является записью, выровненной по странице; и используя низкоуровневый I / O,, fdatasync()и несколько параллельных файловых дескрипторов (один дескриптор и поток на транзакцию) в один и тот же файл в противном случае " . Блокировки описаний открытых файлов ( fcntl(), F_OFD_), специфичные для Linux , очень полезны с последним.
Nominal Animal
22

Поскольку функция write () приложения уже вернулась без ошибок, похоже, нет способа сообщить об ошибке обратно в приложение.

Я не согласен. writeможет вернуться без ошибки, если запись просто поставлена ​​в очередь, но об ошибке будет сообщено при следующей операции, которая потребует фактической записи на диск, то есть при следующей fsync, возможно, при следующей записи, если система решит очистить кеш и при минимум на последний файл закрыть.

Это причина, по которой приложению важно проверить возвращаемое значение close для обнаружения возможных ошибок записи.

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

Серж Бальеста
источник
4
Да, я думаю, это решает. Это действительно наводит на мысль, что приложение должно заново выполнить всю свою работу с момента последнего подтвержденного успешного файла fsync()или close()файла, если оно получает -EIOот write(), fsync()или close(). Что ж, это весело.
Крейг Рингер,
1

write(2) дает меньше, чем вы ожидаете. Страница руководства очень открыта о семантике успешного write()вызова:

Успешный возврат из write()не дает никаких гарантий, что данные были зафиксированы на диске. Фактически, в некоторых реализациях с ошибками это даже не гарантирует, что пространство было успешно зарезервировано для данных. Единственный способ быть уверенным - это вызвать fsync(2) после того, как вы закончите запись всех своих данных.

Мы можем сделать вывод, что успешное выполнение write()означает просто, что данные достигли средств буферизации ядра. Если сохранить буфер не удается, последующий доступ к дескриптору файла вернет код ошибки. Это может быть последнее средство close(). Страница руководства closeсистемного вызова (2) содержит следующее предложение:

Вполне возможно, что об ошибках предыдущей write(2) операции сначала сообщается в final close().

Если вашему приложению необходимо сохранить данные, напишите, оно должно использовать fsync/ fsyncdataна регулярной основе:

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

Fzgregor
источник
5
Да, я понимаю, что fsync()это необходимо. Но в конкретном случае, когда ядро ​​теряет страницы из-за ошибки ввода-вывода, произойдет fsync()сбой? При каких обстоятельствах он может потом добиться успеха?
Крейг Рингер,
Я тоже не знаю исходников ядра. Предположим, fsync()возврат -EIOпо вопросам ввода-вывода (что было бы хорошо в противном случае?). Таким образом, база данных знает, что часть предыдущей записи не удалась, и может перейти в режим восстановления. Разве это не то, что вам нужно? Какова мотивация вашего последнего вопроса? Вы хотите узнать, какая запись не удалась, или восстановить файловый дескриптор для дальнейшего использования?
fzgregor
1
В идеале СУБД предпочла бы не выполнять восстановление после сбоя (отключение всех пользователей и временная недоступность или, по крайней мере, доступ только для чтения), если это возможно. Но даже если бы ядро ​​могло сказать нам «байты с 4096 по 8191 из fd X», было бы сложно понять, что (пере) записать туда, не выполняя восстановления после сбоя. Так что я думаю , главный вопрос , есть ли еще невинные обстоятельства , при которых fsync()может вернуться , -EIOгде он находится в безопасности , чтобы повторить попытку, и если это можно сказать разницу.
Крейг Рингер,
Конечно, восстановление после сбоя - это последнее средство. Но, как вы уже сказали, ожидается, что эти проблемы будут очень редкими. Поэтому я не вижу проблем с восстановлением ни на одном -EIO. Если каждый файловый дескриптор используется только одним потоком за раз, этот поток может вернуться к последнему fsync()и повторить write()вызовы. Но все же, если они write()записывают только часть сектора, неизмененная часть все еще может быть повреждена.
fzgregor
1
Вы правы, что восстановление после сбоя, вероятно, разумно. Что касается частично поврежденных секторов, СУБД (PostgreSQL) сохраняет изображение всей страницы при первом прикосновении к ней после любой заданной контрольной точки именно по этой причине, так что все должно быть в порядке :)
Крейг Рингер
0

Используйте флаг O_SYNC при открытии файла. Это обеспечивает запись данных на диск.

Если это вас не удовлетворит, ничего не будет.

Toughmanwang
источник
18
O_SYNCэто кошмар для производительности. Это означает, что приложение не может делать ничего другого, пока выполняется дисковый ввод-вывод, если только оно не порождается потоками ввода-вывода. С таким же успехом можно сказать, что интерфейс буферизованного ввода-вывода небезопасен, и всем следует использовать AIO. Разве тихая потеря записи не может быть приемлемой для буферизованного ввода-вывода?
Крейг Рингер,
3
( O_DATASYNCтолько немного лучше в этом отношении)
Крейг Рингер,
@CraigRinger. Вам следует использовать AIO, если вам это необходимо и вам нужна какая-либо производительность. Или просто используйте СУБД; он сделает все за вас.
Деми
11
@Demi Здесь приложение представляет собой dbms (postgresql). Я уверен, вы можете себе представить, что переписывать все приложение для использования AIO вместо буферизованного ввода-вывода нецелесообразно. И в этом не должно быть необходимости.
Крейг Рингер,
-5

Проверьте возвращаемое значение close. close может завершиться ошибкой, тогда как буферизованная запись кажется успешной.

Малькольм МакЛин
источник
9
Ну, мы вряд ли хотим быть open()ING и close()ИНГ файл каждые несколько секунд. вот почему у нас есть fsync()...
Крейг Рингер,