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


138

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

(请参阅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()的结果,那么,如果应用程序出现了I / O错误fsync()并且知道重启后如何重新执行工作时会慌乱并急于解决,那应该是足够的保障吗?

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

难道还有其他无害的情况fsync()可能会回来-EIO,而救援和重做工作将过于激烈吗?

为什么?

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

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

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


1
恐怕这将需要SystemFileTable中的其他字段来存储和记住这些错误情况。用户空间进程有可能在后续调用中接收或检查它们。(fsync()和close()返回此类历史信息吗?)
joop

@乔普谢谢。我刚刚发布了我认为正在发生的事情的答案,请记住进行健全性检查,因为与发布“ write()需要close()或fsync( ),而不阅读问题?
Craig Ringer

顺便说一句:我认为您确实应该深入研究内核资源。带日志记录的文件系统可能会遇到相同的问题。更不用说交换分区处理了。由于这些条件存在于内核空间中,因此这些条件的处理可能会更加严格。在用户空间中可见的writev()似乎也可以找到。[在Craig:是的,因为我知道你的名字,并且我知道你不是一个完全的白痴;-]
joop

1
我同意,我不太公平。your,您的回答不是很令人满意,我的意思是没有简单的解决方案(令人惊讶吗?)。
Jean-BaptisteYunès17年

1
@Jean-BaptisteYunès对。对于正在使用的DBMS,“崩溃并输入重做”是可以接受的。对于大多数应用程序来说,这不是一个选择,它们可能不得不忍受同步I / O的可怕性能,或者只是接受定义不明确的行为和对I / O错误的破坏。
Craig Ringer

Answers:


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

然后通过检测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()return -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()直到成功并相信数据在磁盘上,那绝对是错误的。

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

可以吗?

上的POSIX / SuS文档fsync()并没有真正指定这两种方式:

如果fsync()函数失败,则不能保证未完成的I / O操作已经完成。

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_errorsmm/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()ð它和它的最近write()10状态。

测试

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

含义

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

错误报告

进一步阅读

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

postgresql.org邮件列表线程


3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598是一个可能的竞赛,因为它等待{pending&scheduled I / O},而不是{尚未预定的I / O}。显然,这是为了避免设备往返。(我认为用户writes()在安排I / O之前不会返回,对于mmap(),这是不同的)
joop

3
是否有其他进程对同一磁盘上其他文件的fsync调用会返回错误?
Random832 '17

3
@ Random832与像PostgreSQL这样的多处理数据库非常相关,所以很好的问题。看起来可能差不多,但是我对内核代码的了解不够深,无法理解。如果您的proc无论如何都打开了相同的文件,则最好进行合作。
Craig Ringer

1
@DavidFoerster:系统调用使用负的errno代码返回失败;errno完全是用户空间C库的构造。这是常见的忽略系统调用和C库这样之间的返回值的差异(如克雷格林格氏那样,上文),因为错误返回值可靠地识别哪个(系统调用或C库函数)被称为:“ -1errno==EIO“”是指C库函数,而“ -EIO”是指syscall。最后,在线Linux手册页Linux手册页的最新参考。
名义动物

2
@CraigRinger:要回答您的最后一个问题:“ 当事务大小为完整文件时,使用低级I / O和fsync()/ fdatasync();当事务大小为页面对齐的记录时,使用mmap()/ msync();通过低级I / O,fdatasync()和多个并发文件描述符(每个事务一个描述符和一个线程)到同一文件,否则”。在Linux特有的打开文件描述锁(fcntl()F_OFD_)是与最后一个非常有用的。
名义动物

22

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

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

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

如果您确实需要执行巧妙的错误处理,则必须假定自上次成功以来编写的所有内容都fsync 可能已失败,并且至少在某些方面已经失败。


4
是的,我认为这很重要。确实,这表明应用程序应该重新做所有的工作自上次确认成功的fsync()或者close()文件的,如果它得到一个-EIOwrite()fsync()close()。好吧,那很有趣。
Craig Ringer

1

write(2)提供的内容少于您的预期。手册页对成功write()调用的语义非常开放:

从的成功返回write()并不能保证数据已提交到磁盘。实际上,在某些错误的实现中,它甚至不能保证已经为数据成功保留了空间。确保唯一的方法是fsync在完成所有数据写入后调用(2)。

我们可以得出结论,成功write()仅意味着数据已到达内核的缓冲设施。如果持久化缓冲区失败,则对文件描述符的后续访问将返回错误代码。作为最后的手段close()close(2)系统调用的手册页包含以下句子:

很有可能write首先在final close()上报告了先前(2)操作的错误。

如果您的应用程序需要保留数据,则必须定期使用fsync/ fsyncdata

fsync()将文件描述符fd引用的文件的所有修改后的核心数据(即,针对该文件的修改后的缓冲区高速缓存页)传输(“刷新”)到磁盘设备(或其他永久存储设备),以便可以检索所有更改后的信息即使系统崩溃或重新启动后。这包括写入或刷新磁盘高速缓存(如果存在)。呼叫将阻塞,直到设备报告传输已完成。


4
是的,我知道这fsync()是必需的。但是在特定情况下内核由于I / O错误而丢失页面fsync()失败吗?在什么情况下才能成功?
Craig Ringer

我也不知道内核源代码。让我们假设I / O问题的fsync()回报-EIO(否则会有什么用?)。因此,数据库知道先前的某些写入失败,并且可以进入恢复模式。这不是您想要的吗?您最后一个问题的动机是什么?您是否想知道哪个写入失败或恢复文件描述符以供进一步使用?
fzgregor

理想情况下,如果DBMS可以避免,则最好不要进入崩溃恢复(启动所有用户并暂时无法访问或至少是只读的)。但是即使内核可以告诉我们“ fd X的4096到8191字节”,也很难弄清楚在不进行崩溃恢复的情况下(重新)写入什么内容。所以我想主要的问题是是否有更多无辜的情况,其中fsync()可以返回-EIO安全的重试,如果有可能分辨。
Craig Ringer

确保崩溃恢复是万不得已的方法。但是正如您已经说过的,这些问题预计将非常罕见。因此,我看不到任何恢复的问题-EIO。如果每个文件描述符一次仅由一个线程使用,则该线程可以返回到最后一个fsync()并重做write()调用。但是,即使这些write()仅写入扇区的一部分,未修改的部分仍可能损坏。
fzgregor

1
没错,进入崩溃恢复可能是合理的。至于部分损坏的扇区,由于这个原因,DBMS(PostgreSQL)会在任何给定的检查点之后初次触及整个页面时存储整个页面的图像,因此应该没问题:)
Craig Ringer

0

打开文件时,请使用O_SYNC标志。这样可以确保将数据写入磁盘。

如果这不能令您满意,那将一事无成。


17
O_SYNC是一场表演的噩梦。这意味着在发生磁盘I / O时,应用程序无法执行其他任何操作,除非它从I / O线程中派生出来。您可能还说缓冲的I / O接口是不安全的,每个人都应该使用AIO。当然,在缓冲的I / O中不能接受静默丢失的写操作吗?
Craig Ringer

3
O_DATASYNC在这方面稍好一些)
Craig Ringer

@CraigRinger 如果您有此需要并且需要任何性能,则应使用AIO。或者只是使用DBMS;它为您处理一切。
黛咪

10
@Demi这里的应用程序是dbms(postgresql)。我相信您可以想象,重写整个应用程序以使用AIO而不是缓冲的I / O是不切实际的。也没有必要。
Craig Ringer

-5

检查关闭的返回值。关闭可能会失败,而缓冲写入似乎会成功。


8
好了,我们几乎要成为open()荷兰国际集团和close()荷兰国际集团的文件每隔几秒钟。这就是我们拥有fsync()... 的原因
Craig Ringer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.