TL; DR:如果Linux内核丢失了缓冲的I/O,请写,应用程序有什么办法可以找出来?

我知道您必须fsync()该文件(及其父目录)以确保持久性。问题是,如果内核由于I/O错误而丢失了待写的脏缓冲区,那么应用程序如何检测到该脏缓冲区并进行恢复或中止?

考虑数据库应用程序等,其中写入顺序和写入持久性可能至关重要。

丢了写?怎么样?

在某些情况下,Linux内核的块层可能会丢失write()pwrite()等成功提交的缓冲I/O请求,并出现以下错误:

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(...) and end_buffer_async_write(...) in fs/buffer.c )。

On newer kernels the error will instead contain "lost async page write",例如:
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(...) in mm/filemap.c 可能是 do_sync_mapping_range(...) in fs/sync.c ,而 sys_sync_file_range(...) 可能会调用它。如果无法写入一个或多个缓冲区,它将返回-EIO

如果按照我的猜测,如果此消息传播到fsync()的结果,那么如果应用程序出现紧急情况并从fsync()接收到I/O错误并知道如何在重新启动后重新执行工作时会出故障,那就足够了保障?

大概没有办法让应用程序知道文件中的哪些字节偏移量与丢失的页面相对应,因此如果知道如何可以重写它们,但是如果应用程序重复自文件上次成功fsync()以来的所有未完成工作,并且重写与文件丢失写入相对应的所有脏内核缓冲区,这些缓冲区应清除丢失页面上的所有I/O错误标志并允许下一个fsync()完成-对吗?

在没有其他无害的情况下,fsync()可能会返回-EIO,导致救援和重做工作过于激烈吗?

为什么?

当然,这种错误应该不会发生。在这种情况下,错误是由于dm-multipath驱动程序的默认值与SAN用来报告分配精简配置的存储失败的感知代码之间不幸的交互而引起的。但这不是发生这种情况的唯一情况-例如,我还看到了libvirt,Docker等使用的精简配置LVM的报告。诸如数据库之类的关键应用程序应尝试应对此类错误,而不是盲目进行,仿佛一切都很好。

如果内核认为可以在不死于内核 panic 的情况下丢失写入就可以了,那么应用程序必须找到一种应对方法。

实际的影响是,我发现了一个案例,其中SAN的多路径问题导致丢失的写丢失并导致数据库损坏,因为DBMS不知道其写失败。不好玩。

最佳答案

如果内核丢失写入,fsync()返回-EIO
(注意:早期部分引用了较旧的内核;下面进行了更新以反射(reflect)现代内核)

看起来像async buffer write-out in end_buffer_async_write(...) failures set an -EIO flag on the failed dirty buffer page for the file:

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

但是只有一次!

This comment on 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(...) clears the error bits when it tests them:
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(),直到成功并相信数据在磁盘上,那绝对是错误的。

我很确定这是在DBMS中发现的数据损坏的根源。它重试fsync()并认为如果成功,一切都会好起来的。

可以吗?

POSIX/SuS docs on fsync() 并没有真正指定这两种方式:



Linux's man-page for 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并在其上设置性能探测器时:
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()十个状态之间处于某种未定义状态。

测试

I've implemented a test case to demonstrate this behaviour

含义

DBMS可以通过进入崩溃恢复来解决此问题。普通的用户应用程序应该如何应对呢? fsync()手册页没有警告说它意味着“fsync-if-you-feel-like-it”,我希望很多应用程序都无法很好地应对这种行为。

错误报告
  • https://bugzilla.kernel.org/show_bug.cgi?id=194755
  • https://bugzilla.kernel.org/show_bug.cgi?id=194757

  • 进一步阅读

    lwn.net touched on this in the article "Improved block-layer error handling"

    postgresql.org mailing list thread

    关于c - 编写程序以应对导致Linux上的写入丢失的I/O错误,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/42434872/

    10-11 07:39