最近工作中涉及,也有很多人问journal和barrier的问题,找到了篇不错的文章,今天把这篇文章翻译出来,方便自己,也方便他人。原文地址:https://lwn.net/Articles/400541/
以下是正文:
在刚刚结束的Linux Storageand Filesystem 峰会上通过了一个影响很大的决定:在内核的存储子系统中废除对barrier 的支持。这是个很受欢迎的决定,但是也很容易引起误解。现在,来自Tejun Heo 的一系列patch更好的展示了未来如何处理文件系统和块设备层之间的request 顺序问题。
为了达到用户对系统性能的期望,块设备层必须能够对用户的disk操作进行重排序。对于旋转的介质来说,如果不考虑request的顺序,而把相邻的request合并处理,便能够最小化磁头的寻道,从而获得很大收益。甚至在闪存介质的设备上,合并相邻的request也能获得不小的收益,尤其是可以避免“写放大”的问题。通常来说,把request合理地发到底层设备是IO调度层的工作,调度器可以自由地重新排序request,完全不用顾忌上层产生请求的需求。
需要注意的是这种重排序在存储设备内部也是可能发生的。Reqeust 有可能会在硬件内部的内存中暂存,过一段时间等到硬件合适的时候再执行。对于操作系统来说,这一层重排序是完全不可见的。
问题是,这种随意的排序并不总是安全的。经典的例子是对于日志型文件系统,假设操作是这样的顺序:
1. 发起一个新的transaction。
2. 所有的元数据更新被写入journal,当然根据配置,也可能会有一些数据被写入journal。
3. 紧挨着这个transaction再写入一个commit 块。
4. 然后就可以开始把journal中的数据写入文件系统本身。
5. 继续下一个transaction.
如果第3步完成之前系统crash掉了,所以写进journal的数据都会丢失,但是文件系统的一致性却并没有受到损坏。如果系统crash发生在第3步之后而写入到文件系统之前,那么这些修改会在下次mount的时候被再播放一遍,以此保证元数据和文件系统的一致性。Journal 机制就是这样保证了文件在crash时的一致性。
但是考虑如果request被重排序了会发生什么。如果commitblock在其他所有的数据之前被写入了journal然后系统发生了crash,这个不完整的journal就会被重放,从而毁掉文件系统。或者,如果一个transaction释放了一些disk块,而这些块又被重新使用了,并且这些被重新使用的块在前一个free transaction的commit block被写入之已经被写入了数据,系统crash又一次在这个错误的时间发生并且造成了损坏。所以,很明显,文件系统必须能够在一定程度上控制request被执行的顺序,否则,文件系统为保证一致性所做的所有机制都是徒劳。
多年以来,这个问题的答案是barrier request。当文件系统向块设备层提交request时,可以将这个request标记为barrier,表示块设备应该将barrier之前的request先执行完成之后再执行barrier之后的。Barrier的语义是保证到介质的request能够遵循正确的顺序,但是又不会过度限制对barrier之间request的重排序。
实际上,barrier 有很不好的名声,因为其对blockI/O的性能有致命的影响,以至于很多系统管理员宁愿承担文件系统不一致的风险也要把barrier 禁用掉。而同时底层硬件提供的tagged queue功能也能很好的实现barrier语义,这些底层特性又未必能被barrier request很好的利用。所以,事实上barrier 经常被简单实现为把IO 队列上barrier request 之前的request 执行完成,然后向硬件队列发一个flush请求保证数据确实被提交到了持久化介质上。把队列清空的操作会引起设备暂停从而减少发挥全部性能所必要的并发性,也就不奇怪人们认为使用barrier 是痛苦的了。
在这次会议的讨论中,存储和文件系统的开发者认识到块设备层barrier所提供的顺序语义太强了。文件系统应该保证特定request的顺序性,也就是说应该保证特定request是在其他的request完成之后再开始。不仅如此,文件系统还应该考虑许多其他类型request的顺序,基于此,块设备层的barrier语义就显得不是那么必要了。总结起来就一句话:文件系统应该保证请求的顺序性,因为他们才是掌握最多信息的人,而不应该是把这层语义下放到块设备层。
实现上,Tejun的patch把块设备层的hardbarrier特性删掉了,所有的文件系统如果再使用这种request会得到一个EOPNOTSUPP的错误码。如果文件系统想保证request的顺序性也很简单,他只需要等前一个request完成之后再下发后一个就可以了。所以底层块设备就可以自由地重排序request了。
虽然如此,还有一件事是块设备层不能做的,那就是当文件系统需要的时候把一些request真实地持久化到介质上。所以,伴随着barrier 被废除的是,“flush request”会替代他。在一个语义正确的设备上,flush request 包含两层语义:1. 这个request之前的所有request必须被刷下去,2. 当这个flush request完成的时候,这个request请求所包含的数据必必须被持久化到存储介质上。当然了,第二层语义也经常被叫做“force unit access”,就是我们看到的带FUA标志的request。
事实上日志型文件系统有可能把所有的日志写操作放进一个transaction中,然后等待完成。这种情况下,文件系统认为数据已经写到了设备,但事实上有可能在设备内部被cache起来了。接下来写commit block的时候,request带了两个标志:即有flush又有FUA。这个请求可以保证之前所有的journal 数据被持久化到了存储介质上,并且这个请求返回的时候也可以保证这个request本身所带的数据也安全地被持久化到了存储介质上。同时其他类型的请求,可以同时执行不受影响,避免了当年kernel中barrier引起的队列暂停。
这一系列的patch已经被接受了,但是还有一些工作要做,包括转变现在的文件系统到新的工作模式。Christoph Hellwig 在后面又贴了一系列的patch。还有大量的测试要做,毕竟文件系统的一致性问题可不是闹着玩的。所幸开发周期已经开始了,距离一下个版本2.6.37的merge 窗口还有一段相当长的时间。