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

Posted

技术标签:

【中文标题】编写程序以处理导致 Linux 上丢失写入的 I/O 错误【英文标题】:Writing programs to cope with I/O errors causing lost writes on Linux 【发布时间】:2017-07-15 01:30:55 【问题描述】:

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()s 文件以确认它在磁盘上时发现这一点。

看起来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 用来报告分配精简配置存储失败的感知代码之间的不幸交互引起的。但这并不是它们可能发生的唯一情况——我还看到了来自精简配置 LVM 的报告,例如 libvirt、Docker 等使用的。像数据库这样的关键应用程序应该尝试处理此类错误,而不是一味地装作万事大吉。

如果内核认为在不因内核恐慌而死的情况下丢失写入是可以的,应用程序必须找到应对方法。

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

【问题讨论】:

恐怕这需要 SystemFileTable 中的其他字段来存储和记住这些错误情况。并且用户空间进程有可能在后续调用中接收或检查它们。 (fsync() 和 close() 会返回这种历史信息吗?) @joop 谢谢。我刚刚发布了我认为正在发生的事情的答案,请注意进行完整性检查,因为您似乎比发布明显变体的人更了解正在发生的事情“write()需要close()或fsync( ) 以提高耐用性”而不阅读问题? 顺便说一句:我认为您真的应该深入研究内核源代码。日志文件系统可能会遇到同样的问题。更不用说交换分区处理了。由于它们存在于内核空间中,因此对这些条件的处理可能会更加严格。从用户空间可见的 writev() 似乎也是一个可以查看的地方。 [在克雷格:是的,因为我知道你的名字,而且我知道你不是一个彻头彻尾的白痴;-] 我同意,我不太公平。唉,你的回答不是很令人满意,我的意思是没有简单的解决方案(令人惊讶?)。 @Jean-BaptisteYunès 是的。对于我正在使用的 DBMS,“崩溃并输入重做”是可以接受的。对于大多数应用来说,这不是一种选择,它们可能不得不容忍同步 I/O 的可怕性能,或者只是接受定义不明确的行为和 I/O 错误的损坏。 【参考方案1】:

如果内核丢失写入,fsync() 返回 -EIO

(注意:早期部分引用了旧内核;下面更新以反映现代内核)

看起来像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() 并没有真正指定这一点:

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

Linux's man-page for fsync() 只是没有说明失败时会发生什么。

所以fsync()errors 的意思似乎是“我不知道你写的东西发生了什么,可能有效与否,最好再试一次确定”。

较新的内核

在 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 in 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()ten 状态。

测试

I've implemented a test case to demonstrate this behaviour.

意义

DBMS 可以通过进入崩溃恢复来解决这个问题。一个普通的用户应用程序到底应该如何处理这个问题? fsync() 手册页没有警告它意味着“fsync-if-you-feel-like-it”,我预计很多应用程序将无法很好地应对这种行为。 p>

错误报告

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.

【讨论】:

lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 是一个可能的竞争,因为它等待 pending&scheduled I/O,而不是 not yet scheduled I/O。这显然是为了避免额外的设备往返。 (我认为用户 writes() 在 I/O 安排好之前不会返回,对于 mmap(),这是不同的) 其他进程对同一磁盘上其他文件的 fsync 调用是否有可能得到错误返回? @Random832 与 PostgreSQL 这样的多处理数据库非常相关,这是个好问题。看起来可能,但我不太了解内核代码,无法理解。如果他们都打开相同的文件,你的 proc 最好合作。 @DavidFoerster:系统调用使用负 errno 代码返回失败; errno 完全是用户空间 C 库的构造。像这样忽略系统调用和 C 库之间的返回值差异是很常见的(就像上面的 Craig Ringer 所做的那样),因为错误返回值可靠地标识了哪个(系统调用或 C 库函数)被引用:“@ 987654382@ 和 errno==EIO" 指的是 C 库函数,而 "-EIO" 指的是系统调用。最后,Linux man pages online 是 Linux 手册页的最新参考。 @CraigRinger:回答你的最后一个问题:“当事务大小是一个完整文件时,通过使用低级 I/O 和fsync()/fdatasync();通过使用@987654387 @/msync() 当事务大小是页对齐记录时;否则通过使用低级 I/O、fdatasync() 和多个并发文件描述符(每个事务一个描述符和一个线程)到同一个文件" 。 Linux 特定的打开文件描述锁(fcntl()F_OFD_)与最后一个非常有用。【参考方案2】:

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

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

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

如果你真的需要能够进行巧妙的错误处理,你必须假设自上次成功fsync 可能以来编写的所有内容都失败了,并且至少有一些失败了.

【讨论】:

是的,我认为这很重要。如果应用程序从write()fsync()close() 获得-EIO,这确实表明应用程序应该重新完成自上次确认成功的文件fsync()close() 以来的所有工作。嗯,这很有趣。【参考方案3】:

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

write() 成功返回并不能保证 数据已提交到磁盘。事实上,在一些错误的实现中, 它甚至不能保证空间已成功保留 为数据。唯一可以确定的方法是在您之后致电fsync(2) 已完成所有数据的写入。

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

以前的write(2) 操作中的错误很可能是 首发于决赛close()。

如果您的应用程序需要持久写入数据,则必须定期使用fsync/fsyncdata

fsync() 传输(“刷新”)所有修改的内核数据(即修改 buffer cache pages for) 由文件描述符 fd 引用的文件 到磁盘设备(或其他永久存储设备),所以 即使在 系统崩溃或重新启动。这包括通过写作或 刷新磁盘缓存(如果存在)。调用阻塞,直到 设备报告传输已完成。

【讨论】:

是的,我知道fsync() 是必需的。但是在内核因 I/O 错误而丢失页面的特定情况下fsync() 会失败吗?之后在什么情况下才能成功? 我也不知道内核源码。让我们假设 fsync() 在 I/O 问题上返回 -EIO(否则会有什么好处?)。所以数据库知道一些先前的写入失败并且可以进入恢复模式。这不是你想要的吗?你最后一个问题的动机是什么?您想知道哪个写入失败或恢复文件描述符以供进一步使用吗? 理想情况下,如果可以避免,DBMS 将不希望进入崩溃恢复(启动所有用户并暂时无法访问或至少只读)。但是,即使内核可以告诉我们“fd X 的 4096 到 8191 字节”,如果没有进行崩溃恢复,也很难弄清楚在那里(重新)写什么。所以我想主要的问题是是否有更多无辜的情况,fsync() 可能会返回-EIO,在该情况下 可以安全重试,以及是否可以区分。 当然崩溃恢复是最后的手段。但正如你已经说过的,这些问题预计将非常罕见。因此,我认为在任何-EIO 上进行恢复都没有问题。如果每个文件描述符一次只被一个线程使用,该线程可以返回到最后一个fsync() 并重做write() 调用。但是,如果那些write()s 只写了一个扇区的一部分,那么未修改的部分可能仍然是损坏的。 你是对的,进入崩溃恢复可能是合理的。至于部分损坏的扇区,出于这个原因,DBMS(PostgreSQL)在任何给定的检查点之后第一次触及整个页面时都会存储整个页面的图像,所以应该没问题:)【参考方案4】:

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

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

【讨论】:

O_SYNC 是性能的噩梦。这意味着应用程序在磁盘 I/O 发生时不能做任何事情else,除非它产生 I/O 线程。你还不如说缓冲 I/O 接口是不安全的,每个人都应该使用 AIO。在缓冲 I/O 中肯定不能接受静默丢失的写入吗? O_DATASYNC 在这方面只是稍微好一点) @CraigRinger 如果您有此需求并需要任何类型的性能,您应该使用 AIO。或者只使用 DBMS;它会为你处理一切。 @Demi 这里的应用程序是一个 dbms (postgresql)。我相信您可以想象重写整个应用程序以使用 AIO 而不是缓冲 I/O 是不切实际的。也没有必要。【参考方案5】:

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

【讨论】:

好吧,我们几乎不想每隔几秒就成为open()ing 和close()ing 文件。这就是为什么我们有fsync() ...

以上是关于编写程序以处理导致 Linux 上丢失写入的 I/O 错误的主要内容,如果未能解决你的问题,请参考以下文章

与多线程服务器的繁忙循环相比,Java 互斥体导致输入丢失?

linux c语言编程

应用重新运行后,通过 WriteToFile 命令写入的图像丢失

通过Python处理许多连接[关闭]

在 iOS 上捕获/分割视频并通过 HLS 重新加入会导致音频丢失

如何在 Linux 上使用标准 I/O 函数在多个进程中写入文件?