编写程序以应对导致Linux上丢失写入的I/O错误

Cra*_*ger 138 c linux posix linux-kernel

TL; DR:如果Linux内核丢失了缓冲的I/O写入,那么应用程序是否有任何方法可以找到?

我知道你有fsync()文件(和它的父目录)的耐用性.问题是,如果内核由于I/O错误而丢失了待写入的脏缓冲区,应用程序如何检测并恢复或中止?

想想数据库应用程序等,其中写入顺序和写入持久性可能是至关重要的.

失去写?怎么样?

Linux内核的模块层可以在某些情况下失去缓冲已成功提交的I/O请求write(),pwrite()等等,有这样的错误:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0
Run Code Online (Sandbox Code Playgroud)

(见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
Run Code Online (Sandbox Code Playgroud)

由于应用程序write()已经无错误地返回,因此似乎无法将错误报告给应用程序.

检测它们?

我不熟悉内核源代码,但我认为它设置AS_EIO在缓冲区上,如果它正在执行异步写入,则无法写出:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);
Run Code Online (Sandbox Code Playgroud)

但是我不清楚应用程序是否或如何在以后查找fsync()文件以确认它在磁盘上时会发现这一点.

它看起来像wait_on_page_writeback_range(...)mm/filemap.c借着do_sync_mapping_range(...)fs/sync.c被称为转用sys_sync_file_range(...).-EIO如果无法写入一个或多个缓冲区,则返回.

如果,正如我猜测的那样,这会传播到fsync()结果,那么如果应用程序发生了恐慌,并且如果它从I/O错误中获得I/O错误fsync()并且知道如何在重新启动时重新执行其工作,那应该是足够的保护措施吗?

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

那么还有其他任何无害的情况fsync()可能会-EIO在拯救和重做工作过于激烈的情况下返回吗?

为什么?

当然这种错误不应该发生.在这种情况下,错误源于dm-multipath驱动程序的默认值与SAN使用的感知代码之间的不幸交互,以报告分配精简配置存储的失败.但这并不是它们可能发生的唯一情况- 我也看到过来自精简配置LVM的报告,例如libvirt,Docker等等.像数据库这样的关键应用程序应该尝试应对这些错误,而不是盲目地继续进行,好像一切都很好.

如果内核认为丢失写入而不会因内核恐慌而死,则应用程序必须找到应对的方法.

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

Cra*_*ger 91

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);
Run Code Online (Sandbox Code Playgroud)

然后由检测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.
Run Code Online (Sandbox Code Playgroud)

建议当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;
Run Code Online (Sandbox Code Playgroud)

因此,如果应用程序期望它可以重新尝试fsync()直到它成功并且相信数据在磁盘上,那就非常错误.

我很确定这是我在DBMS中发现的数据损坏的根源.它重试fsync()并认为一旦成功就会很好.

这是允许的吗?

对POSIX/SUS文档fsync()真的不指定此两种方式:

如果fsync()函数失败,则无法保证已完成未完成的I/O操作.

Linux的man-pagefsync()只是没有说明失败后会发生什么.

所以似乎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);
Run Code Online (Sandbox Code Playgroud)

在同步方面,我认为它是相似的,尽管结构现在非常复杂.filemap_check_errorsmm/filemap.c现在:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
Run Code Online (Sandbox Code Playgroud)

这有很多相同的效果.错误检查似乎都经过filemap_check_errors了测试和清除:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

我发现以下调用堆栈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
Run Code Online (Sandbox Code Playgroud)

通读表明,现代内核的行为相同.

这似乎意味着,如果fsync()(或推测write()close())回报-EIO,该文件是两者之间的某个不确定的状态,当你持续成功fsync()d或close()ð它和它的最近write()10个状态.

测试

我已经实现了一个测试用例来演示这种行为.

启示

DBMS可以通过进入崩溃恢复来应对此问题.一个普通的用户应用程序应该如何处理这个?该fsync()手册页没有给出警告,它的意思是"FSYNC-IF-你感觉样它",我想到很多应用程序将不会与这种行为很好地应对.

错误报告

进一步阅读

lwn.net在文章"改进的块层错误处理"中提到了这一点.

postgresql.org邮件列表线程.

  • http://lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598是一种可能的竞赛,因为它等待{待定和预定的I/O},而不是{尚未安排的I}/O}.这显然是为了避免额外的往返设备.(我假设用户write()在调度I/O之前不会返回,对于mmap(),这是不同的) (3认同)
  • 是否有可能某个其他进程对同一磁盘上的某个其他文件的fsync调用会返回错误? (3认同)
  • @ Random832对于像PostgreSQL这样的多处理数据库非常相关,所以很好的问题.看起来可能,但我不太了解内核代码.如果他们都打开了相同的文件,那么你的procs最好是合作. (3认同)
  • @CraigRinger:回答你的最后一个问题:*"当事务大小是一个完整的文件时,使用低级I/O和`fsync()`/`fdatasync()`;使用`mmap()`/`msync ()`当事务大小是页面对齐的记录;并且通过使用低级I/O,`fdatasync()`和多个并发文件描述符(每个事务一个描述符和一个线程)到同一个文件,否则"*.特定于Linux的打开文件描述锁(`fcntl()`,`F_OFD_`)对于最后一个非常有用. (2认同)

Ser*_*sta 22

由于应用程序的write()已经返回而没有错误,因此似乎无法将错误报告给应用程序.

我不同意.write如果写入只是排队,则可以无错误地返回,但是将在下一个需要在磁盘上进行实际写入的操作上报告错误,这意味着下一次fsync,如果系统决定刷新缓存,则可能在下一次写入时报告至少在最后一个文件关闭.

这就是为什么应用程序必须测试close的返回值以检测可能的写入错误.

如果你真的需要能够进行聪明的错误处理,你必须假设自上次成功以来所写的所有内容都fsync 可能失败,而且至少在某些方面都失败了.

  • 是的,我认为钉它.这确实表明应用程序应该重新完成自上次确认成功的`fsync()`或`close()`之后的所有工作,如果它从`write()`得到`-EIO`,` fsync()`或`close()`.嗯,这很有趣. (3认同)

小智 -1

打开文件时使用 O_SYNC 标志。它确保数据写入磁盘。

如果这不能让你满意,那就什么都没有了。

  • “O_SYNC”对于性能来说是一场噩梦。这意味着应用程序在磁盘 I/O 发生时无法执行任何操作,除非它产生 I/O 线程。你不妨说缓冲I/O接口不安全,大家都应该使用AIO。缓冲 I/O 中肯定不能接受静默丢失的写入吗? (20认同)
  • @Demi这里的应用程序是一个dbms(postgresql)。我确信您可以想象重写整个应用程序以使用 AIO 而不是缓冲 I/O 是不切实际的。也没有必要。 (13认同)
  • (`O_DATASYNC` 在这方面只是稍微好一点) (4认同)