it-roy-ru.com

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

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

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

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

Потерянный пишет? Как?

Уровень блоков ядра Linux может при некоторых обстоятельствах потерять буферизованные запросы ввода-вывода, которые были успешно отправлены 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()s файл, чтобы подтвердить, что он находится на диске.

Это выглядит как 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 привела к потере записей, которые произошли, что привело к повреждению базы данных, потому что СУБД не знала, что ее записи завершились неудачно. Не смешно.

118
Craig Ringer

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(...) для реализации вызова библиотеки C 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 или (недокументированный в man-странице) -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 docs на fsync() на самом деле это не так или иначе:

В случае сбоя функции fsync () ожидаемые операции ввода-вывода не гарантируются завершенными.

man-страница Linux для fsync() просто ничего не говорит о том, что происходит при сбое.

Таким образом, кажется, что значение ошибок 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 и устанавливаю на ней тест perf:

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()ten.

Тестовое задание

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

Последствия

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

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

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

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

ветка списка рассылки postgresql.org .

83
Craig Ringer

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

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

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

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

22
Serge Ballesta

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

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

1
toughmanwang

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

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

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

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

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

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

1
fzgregor