LWN:写文件的时候不希望写入一半的时候掉电丢失,怎么办?

jujusharp  •  Jun 19, 2019 9:58:19 PM

原文地址


By Jake Edge
May 28, 2019


LSFMM

Christoph Hellwig在2019 Linux Storage, Filesystem, and Memory-Management Summit (LSFMM)会议上,分享了他关于文件系统原子操作的思考。如果application在写文件的时候出现了程序崩溃,人们肯定希望文件系统里的数据要么是旧数据,要么是新数据,肯定不希望看到新旧数据混杂在一起。这样就需要对文件有atomic write操作的能力。他介绍了在XFS里实现的atomic write,想看看其他文件系统开发者有什么想法。

目前,如果application希望对文件做atomic write,会有两种方法。一种是在user space加锁来保护,数据库方案里面通常会这样来做;另一种是重新写一个新文件,然后做“atomic rename"。可惜,应用程序开发者通常都没有正确使用fsync(),所以最终数据还是会有丢失。

在现代存储系统里面,哪怕存储设备硬件本身,在写操作的时候都不是立刻写入存储单元的。例如闪存设备都有一个flash translation layer (FTL)抽象层,会把写操作分发到flash的各个地方去,避免某些存储单元被写入太多过早损坏(wear leveling),所以它们实际上从来不会在原地址更新数据。对NVMe设备来说,每次更新它的一个逻辑块地址(LBA)的数据的时候,都是能确保是atomic操作的,不过这个接口地位比较尴尬,估计很少人会用。SCSI的接口更加好一些,在做atomic write的时候能够有错误报告,不过他也没见过哪个厂商真的实现了这个接口。

有的文件系统可以在写操作的时候写在别的地址 ,例如XFS, Btrfs,等等。这样就能很容易在文件系统层实现aotmic write。在5年前,HP Research就有一篇有意思的论文,介绍了如何增加一个特殊的open() flag来专门指定要atomic write。这篇论文还只是学术论文,没有真正处理各种corner case,以及针对现实生活中的限制来实现,不过想法很合理。

在这个系统中,用户写入文件的时候无论写入多少数据都没用,在明确调用commit操作之前都不会真正生效。当commit操作结束后,所有的改动就真正生效了。这里比较简单的一种实现方式就是在fsync()里面来恰当地调用commit操作,这样就不需要再加一个新的系统调用了。

不久之前,他开始在XFS里面用这种方式实现atomic write。他向社区发布出了一组patch,不过当时还有不少问题,因此他后来继续修改了这组patch。目前论文的原作者一直在跟他紧密交流希望能拿到代码,然后就能跟他合作再写一篇论文出来。此外还有一些人也希望使用这个功能。

Chris Mason问他这里的粒度是多大,是针对每个单独的write(),还是更多?Hellwig回答,在commit操作之前的所有write,都会等commit的时候一次写入。文件系统会负责对最多能写多少数据来设一个上限。对XFS来说,会根据这些write所涉及到的不连续区域的数量,来决定这个上限。

不光是传统的write()系统调用,mmap()映射出来的区域也一样适用。例如,人们现在更改B-Tree的多个节点的时候,很难做atomic update,而这个功能合入后,application可以简单的在文件mmap出来的内存区域做这些更改,然后简单做一次commit即可。如果application崩溃了,文件系统里存储的数据仍然保证是旧版本或者是新版本,不会混杂起来。

Ted Ts'o提到他的Android领域的朋友也在提需求想要这么一个功能,不过是针对每个文件系统级别的。他们希望每次对Android做版本更新的时候,ext4或者F2FS文件系统都可以通过一个magic option来加载上来并且关闭所有日志记录真正触发写入操作。等文件更新完毕然后就发一个ioctl()来开始把所有那些日志都刷到存储设备里。这个方案有点不美观,不过能实现90%的功能。最后,Ts'o也认为ext4会需要有一个atomic write功能,不过每次commit之前究竟能更新多少数据,这里可能更加受限制一点。

Hellwig表示了一些担忧,因为他此前也做过类似的实现方案,都是在内存里面做数据更新,不过最终发现每一批次能缓存的数据非常有限。Ts'o介绍了Android的情况,这里数据块都是会写入存储设备的,而内存中缓存的只是metadata相关的更新,一般也就缓存几分钟,这是个非常特殊的应用场景,不具有普适性。不过这个新的实现方法替代了此前的利用device-mapper的机制(那个太慢了)。

Chris Mason提到,只要interface设计的好,他很愿意让Btrfs支持这个功能。Hellwig也说对Btrfs来说实现这个功能会很直接。对他来说一个比较大的阻碍是怎么支持O_DIRECT。如果某个application先做了atomic write,然后又把内容读回来,最好是能把刚刚写入的数据读回来。一般的application都不会这么做,不过NFS确实有这种行为。Linux I/O的代码里面没有完全支持好这部分功能,所以他还需要做一些修改。

还有一些讨论是关于为什么利用fsync()的,为什么不用一个专用的系统调用,或者其他什么接口。Hellwig觉得用fsync()没有什么不好,毕竟这也是它的本来含义,不应该只做一部分工作,而不做完。Amir Goldstein问到是否有可能其他进程同时也对这个文件做fsync()操作,相当于是某种类型的攻击。 Hellwig说他本来是使用了一个open()的flag,不过后来有人提醒没有用过的flag可能不会在open()里面检查,所以利用flag来保证数据一致性不是一个很好的主意。在那种使用模式下,使用fsync()的接口仅仅会对那些被用这个flag打开的file descriptor才会做commit操作。后来他改成了使用inode的flag,这样更加合理一点,不过目前还没有处理好那些恶意的fsync()调用的问题。

0 回复
暂时没有回复,你也许会成为第一个哦