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

时间:2017-02-24 09:19:57

标签: c linux linux-kernel posix

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(...)调用{{3}}。如果无法写入一个或多个缓冲区,则返回-EIO

如果正如我猜测的那样,这会传播到fsync()的结果,那么如果应用程序因fsync()出现I / O错误而发生恐慌并挽救并知道如何在重新启动时重新开始工作,那应该是足够的保障吗?

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

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

为什么?

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

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

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

5 个答案:

答案 0 :(得分:83)

如果内核丢失了写,则

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(...)调用fsync()调用,以实现C库调用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. 。< / p>

但只有一次!

This comment on sys_sync_file_range

fsync()

建议当-EIO返回-ENOSPC或(在联机帮助页中未记录)fsync()时,它将清除错误状态,以便随后{{1}即使页面从未写过,也会报告成功。

果然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()错误的含义是&#34;不知道你的写作发生了什么,可能已经或不起作用,最好再试一次确保&#34;。

较新的内核

在页面上的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环回测试并在其上设置一个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,则文件在您上次成功{{}之间处于某种未定义状态1}} d或fsync() d及其最近的close()十州。

测试

I've implemented a test case to demonstrate this behaviour

启示

DBMS可以通过进入崩溃恢复来解决这个问题。一个普通的用户应用程序应该如何处理这个? write()手册页没有发出任何警告,表示&#34; fsync-if-you-like-like-it&#34;我希望很多的应用程序不会很好地应对这种行为。

错误报告

进一步阅读

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

postgresql.org mailing list thread

答案 1 :(得分:22)

  

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

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

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

如果您确实需要能够进行巧妙的错误处理,那么您必须假设自上次成功fsync 以来所写的所有内容都可能失败,并且至少在某些情况下失败了。

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

write(2)提供的比你想象的少。手册页对于成功write()调用的语义非常开放:

  

write()成功返回并不能保证       数据已提交到磁盘。事实上,在一些错误的实施,       它甚至不能保证已成功保留空间       对于数据。唯一可以确定的方法是在你之后拨打fsync(2)       完成了所有数据的写作。

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

  

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

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

  

fsync()转移(&#34;刷新&#34;)所有已修改的核心内数据(即,已修改)       缓冲区缓存页面)文件描述符fd引用的文件       到磁盘设备(或其他永久存储设备)所以       即使在之后,所有已更改的信息都可以被检索到       系统崩溃或重新启动。这包括通过或写作       刷新磁盘缓存(如果存在)。呼叫阻塞直到       设备报告传输已完成。

答案 4 :(得分:-5)

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