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

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

(见 ABC2中的 ABC0和 end_buffer_async_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()文件确认它在磁盘上时发现这一点。

它看起来像 ABC1中的 wait_on_page_writeback_range(...)可能由 ABC3中的 do_sync_mapping_range(...)转而由 sys_sync_file_range(...)调用。如果无法写入一个或多个缓冲区,则返回 -EIO

如果正如我猜测的那样,这会传播到 fsync()的结果,那么如果应用程序在从 fsync()获得一个 I/O 错误并且知道如何在重启时重新做它的工作时惊慌失措并退出,这应该是足够的保护措施了吧?

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

那么,是否存在其他无害的情况,使得 fsync()可能返回 -EIO,而在这种情况下,跳出和重做工作会过于激烈?

为什么?

当然,这样的错误不应该发生。在这种情况下,错误产生于 dm-multipath驱动程序的缺省值与 SAN 用于报告分配瘦配置存储失败的检测代码之间的不幸交互。但是这并不是 可以发生的唯一情况——例如,我还看到了从瘦供应 LVM 发出的关于 可以的报告,libvirt、 Docker 等使用了这种报告。像数据库这样的关键应用程序应该尝试处理这样的错误,而不是盲目地继续下去,好像一切都很好。

如果 内核认为丢失写操作不会因内核恐慌而死亡,那么应用程序必须找到应对的方法。

实际的影响是,我发现了一个案例,SAN 的多路径问题导致丢失写操作,最终导致数据库损坏,因为 DBMS 不知道它的写操作已经失败。一点都不好玩。

12094 次浏览

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

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

write(2)提供的内容比您预期的要少:

write()的成功回报并不能保证 数据已经提交到磁盘。事实上,在一些错误的实现中, 它甚至不能保证空间被成功地保留下来 唯一可以确定的方法是在您之后调用 fsync(2) 已经写完了你所有的数据。

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

以前的 write(2)操作中的错误很可能是 第一次报告在最后的 close()。

如果您的应用程序需要保存数据写出,它必须使用 fsync/fsyncdata定期:

fsync()传输(“刷新”)所有修改过的内核数据 缓冲区缓存页面)文件描述符 fd 引用的文件 磁盘设备(或其他永久存储设备) 所有更改的信息都可以检索,即使在 system crashed or was rebooted. This includes writing through or 如果存在,则刷新磁盘缓存 设备报告转移已经完成。

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

如果内核丢失了写操作,则 fsync()返回 -EIO

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

看起来像 ABC0故障中的异步缓冲区写出在失败的文件脏缓冲区页上设置 -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(...)调用 fsync()实现 C 库调用。

但只有一次!

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()将报告成功,即使页面从未被写入。

确实如此:

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(),并认为当它成功时一切都会好起来。

这样可以吗?

无论如何,fsync()上的 POSIX/SuS 文档实际上都没有指明这一点:

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

在同步方面,我认为它是相似的,虽然结构现在是相当复杂的遵循。mm/filemap.c中的 filemap_check_errors现在可以:

    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上进行测试并在其上设置 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

我在 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()0状态之间处于某种未定义状态。

测试

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

暗示

DBMS 可以通过输入崩溃恢复来处理这个问题。一个普通的用户应用程序究竟应该如何处理这个问题呢?fsync()手册页没有给出任何警告,它的意思是“ fsync-if-you-feel-like-it”,我预计 很多的应用程序不会很好地处理这种行为。

窃听器报告

进一步阅读

Net 在文章“改进的块层错误处理” 中提到了这一点。

Postgreql.org 邮件列表帖子

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

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

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

如果您真的需要能够做聪明的错误处理,您必须假定自上次成功 fsync 以来编写的所有内容都失败了,并且在所有这些内容中,至少有一些内容失败了。