编写程序来处理在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(...) )。
在较新的内核中,错误将包含“丢失的异步页写”,如:
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()的结果,那么如果应用程序在fsync()发生I / O错误并且知道如何在重新启动时重新执行它的工作时出现混乱并保留,那应该是足够的保障?
应用程序很可能无法知道文件中的哪些字节偏移与丢失的页面相对应,因此如果它知道如何重写它们,但如果应用程序重复自上次成功执行fsync()以来的所有待处理工作,并重写任何对应于文件丢失写入的脏内核缓冲区,这应该清除丢失页面上的任何I / O错误标志,并允许下一个fsync()完成 - 对吗?
那么在fsync()可能会返回的情况下是否还有其他无害的情况? -EIO救助和重做工作过于激烈?
为什么?
当然,这种错误不应该发生。 在这种情况下,错误是由dm-multipath驱动程序的默认值与SAN用于报告分配精简配置存储失败的检测代码之间的dm-multipath交互产生的。 但是,这不是唯一可以发生的情况 - 例如,我也看到了来自精简配置LVM的报告,如libvirt,Docker等使用的情况。 像数据库这样的关键应用程序应该尝试应对这样的错误,而不是盲目地继续进行,就好像一切正常。
如果内核认为可以在不致死于内核恐慌的情况下丢失写入,那么应用程序必须找到一种方法来应对。
实际影响是,我发现一个存在SAN的多路径问题导致丢失写入导致数据库损坏的情况,因为DBMS不知道写入失败。 不好玩。
如果内核丢失写入, 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()返回-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()并认为一切都会很成功。
这是否允许?
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 ,但是当我在/mnt/tmp上创建ext4 loopback进行测试并在其上设置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()时,文件处于某种未定义状态write()十状态。
测试
我已经实现了一个测试用例来演示这种行为。
启示
DBMS可以通过进入崩溃恢复来解决这个问题。 一个普通的用户应用程序究竟应该如何处理? fsync()手册页不会提示它的意思是“fsync-if-you-feel-like-it”,我希望很多应用程序不能很好地处理这种行为。
错误报告
进一步阅读
lwn.net在文章“改进的块层错误处理”中谈到了这一点。
postgresql.org邮件列表线程。
由于应用程序的write()将无错地返回,因此似乎无法将错误报告给应用程序。
我不同意。 如果写入是简单排队的, write可以无误地返回,但是错误将在下一个操作上报告,这将需要在磁盘上进行实际写入,这意味着在下一个fsync ,如果系统决定刷新缓存并至少在最后一个文件关闭。
这就是为什么应用程序测试close的返回值以检测可能的写入错误至关重要的原因。
如果你真的需要能够做出巧妙的错误处理,你必须假设自从上一次成功的fsync以来所写的所有内容都可能失败,并且至少在fsync失败了。
write (2)提供的比你想象的要少。 man页面对write()调用的语义非常开放:
write()成功返回不能保证数据已被提交到磁盘。 事实上,在一些错误的实现中,它甚至不保证空间已经被成功地保留用于数据。 唯一可以确定的方法是在写完所有数据后调用fsync (2)。
我们可以得出结论, write()的成功仅仅意味着数据已经到达了内核的缓冲设施。 如果持久化缓冲区失败,则对文件描述符的后续访问将返回错误代码。 作为最后的手段可能是close() 。 close (2)系统调用的手册页包含以下语句:
先前的write (2)操作中的错误很可能会在最后的close ()中首先报告。
如果您的应用程序需要坚持数据写入,它必须定期使用fsync / fsyncdata :
fsync()将由文件描述符fd引用的文件的所有修改的核心内数据(即修改的缓冲区缓存页fsync()转移(“刷新”)到磁盘设备(或其他永久性存储设备),使得所有改变的信息即使在系统崩溃或重新启动后也可以恢复。 这包括写入或刷新磁盘缓存(如果存在)。 呼叫会阻止,直到设备报告传输已完成。
上一篇: Writing programs to cope with I/O errors causing lost writes on Linux
下一篇: How to get writes via an mmap mapped memory pointer to flush immediately?
