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

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 

(请参见fs/buffer.c end_buffer_write_sync(...)end_buffer_async_write(...) )。

在较新的内核上,错误将包含“丢失的asynchronous页面写入” ,如:

 Buffer I/O error on dev dm-0, logical block 12345, lost async page write 

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

检测他们?

我不熟悉内核源代码,但是我认为它将AS_EIO设置在缓冲区中,如果它正在执行asynchronous写入,则写入失败:

  set_bit(AS_EIO, &page->mapping->flags); set_buffer_write_io_error(bh); clear_buffer_uptodate(bh); SetPageError(page); 

但是我不清楚这个应用程序是否能够在应用程序中find这个文件,以及如何在以后的fsync()文件中确认这个文件在磁盘上。

它看起来像mm/filemap.cdo_sync_mapping_range(...)可能是由do_sync_mapping_range(...)调用的fs/sync.c中的sys_sync_file_range(...) 。 如果一个或多个缓冲区不能被写入,它将返回-EIO

如果正如我猜测,这将传播到fsync()的结果,那么如果应用程序恐慌和退出,如果它从fsync()得到一个I / O错误,并知道如何重新工作时重新启动,那应该是足够的保障?

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

那么在fsync()可能返回的情况下是否还有其他无害的情况? -EIO救援和重做工作太激烈了?

为什么?

当然,这样的错误不应该发生。 在这种情况下,错误是由于dm-multipath驱动程序的默认值与SAN用于报告分配精简configuration存储失败的检测代码之间的dm-multipath交互而产生的。 但是,这不是唯一可以发生的情况 – 例如,libvirt,Docker等公司也使用了自动精简configurationLVM的报告。 像数据库这样的关键应用程序应该尝试应对这样的错误,而不是盲目地继续进行,好像一切正​​常。

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

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

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

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

它看起来像在end_buffer_async_write(...)失败中的asynchronous缓冲区写出在失败的脏缓冲区页面上为该文件设置了一个-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. 

build议当fsync()返回-EIO或(在联机帮助页中没有logging) -ENOSPC ,它将清除错误状态,所以即使页面从未写入,后续的fsync()也会报告成功。

当然, wait_on_page_writeback_range(...) 在testing时会清除错误位

 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()并认为一切顺利。

这是允许的吗?

fsync()上的POSIX / SuS文档并没有真正指定这一点:

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

Linux的fsync()的手册页没有提到失败时会发生什么。

所以看起来fsync()错误的意思是“不知道你的写作发生了什么,可能已经工作或没有,最好再试一次,以确保”。

较新的内核

在4.9上, -EIO在页面上设置-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进行testing和清除:

  if (test_bit(AS_EIO, &mapping->flags) && test_and_clear_bit(AS_EIO, &mapping->flags)) ret = -EIO; return ret; 

我在我的笔记本电脑上使用了btrfs ,但是当我在/mnt/tmp上创build一个ext4环回testing并在其上build立一个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 -Tfind以下调用堆栈:

  ---__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()十状态。

testing

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

启示

DBMS可以通过进入崩溃恢复来解决这个问题。 一个普通的用户应用程序到底应该如何处理? fsync()手册页不会给出警告,意思是“fsync-if-you-feel-like-it”,我期望很多应用程序不能很好地处理这种行为。

错误报告

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

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

这就是为什么应用程序testingclose的返回值以检测可能的写入错误至关重要的原因。

如果你真的需要能够聪明地进行error handling,那么你必须假定自从上一次成功的fsync以来写的所有东西都可能失败了,至less在fsync失败了。

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

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

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

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

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

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

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

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

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