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

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

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

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

Lost пишет? Как?

Блочный уровень ядра 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() файл будет подтвержден на диске.

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

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

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

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

Почему?

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

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

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

Ответ 1

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 на своем ноутбуке, но когда я создаю loopback 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, файл находится в состоянии undefined между последним успешным fsync() d или close() d это и его последнее состояние write() десять.

Test

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

Последствия

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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