崩溃一致性:FSCK和日志
崩溃是什么
-
持久性:文件系统的数据存储在硬盘上,因此必须长期存在
-
不一致:如果某个操作的完成有多个步骤,其中某一步做完后系统崩溃,则会发生不一致的情况
- 参考数据库的事务处理
-
例子:向VSFS的一个文件追加数据,那么文件系统需要进行多次写操作:
- 需要修改文件的inode
- 需要写文件的data块
- 如果空间不足需要分配新数据块,那么需要写dbitmap区域
其中的任两步操作之间都可能会系统崩溃,例如:
- 只写入了数据块
- 这种情况对于文件系统一致性毫无影响,仿佛写入没有发生过一样。只不过用户体验不好
- 只写入了inode更新
- 这种情况下,由于数据块没有写入,因此如果根据inode读数据区会读到垃圾
- 只写入了bitmap更新
- 这种情况下,问题很大,空间实际上没有被使用,但是显示已分配,故会导致空间泄露(并且如果不做处理,则会永远永远泄露,因为磁盘是长久存储)
- 等等
-
理想:文件系统从一个一致性状态,原子地移动到另一个状态
fsck的解决方法
- fsck:file system checker,一种早期系统用来处理崩溃一致性问题的方法
- 目标:确保文件系统元数据一致性
- 思路:不处理不一致的发生,而是在发生后修复他们(重启的时候)
- fsck原理:在系统启动时运行fsck程序,处理不一致的情况
- 超级块检查:检查超级块是否合理
- 空闲块检查:扫描inode、间接块、双重间接块等等,以这些信息去更新bitmap(也就是说,比起bitmap,系统更信任inode的内容)
- inode状态:如果存在问题并且不易修复,则fsck直接清除该inode,并且更新相应的inode位图
- inode链接:检索引用计数,如果计算的计数与inode中的计数不一样,则更新inode中的计数;如果没有目录引用inode,则将该文件移动到lost + found目录
- 检查重复:如果两个inode引用同一个数据块,则会清除明显不对的inode,或者复制一份
- 坏块检查:扫描指针,如果指针显然不正确,则会删除该指针
- 目录检查:检查每个目录是否有.和…,引用的每个inode是否已经分配等等
- fsck的问题
- 太慢。每次启动系统都需要全面扫描一遍磁盘
- 很没必要。文件系统崩溃只是极少发生的情况,而且出问题的一般都是少数文件,因此每次都扫描所有文件的代价太大了
日志的解决办法
-
思想:借鉴数据库的日志思想,每次写磁盘之前,都先写日志
- 实现思路:为磁盘加上一个journal段,专用于存放日志
- 实现思路:为磁盘加上一个journal段,专用于存放日志
-
数据日志:
-
日志类型
- 物理日志:日志内容即写入的数据内容。本笔记中说的都是物理日志
- 逻辑日志:日志内容指引数据如何修改。可以节省空间,但是更复杂
-
操作
- 每次写磁盘数据之前,都先写TxB(起始标识)、待写数据、TxE(结束标识)进磁盘日志区
- 写完TxE后,将数据写入磁盘数据块(这个步骤称为加检查点)
-
流程:日志写入、日志提交(写入TxE)、加检查点(实际写入磁盘)
-
一些小问题
- 如果一次写入TxB、待写数据、TxE,可能会出现问题:磁盘会对写入进行优化,使得写入不一定按顺序进行,可能导致TxE先于待写数据写入磁盘
- 解决方法:不能一次写入,先写TxB和待写数据,最后再写TxE
- 写缓冲的存在使得顺序可能发生问题
- 解决方法:禁用写缓冲
- 写入日志的效率低
- 原因:先写TxB和待写数据,最后再写TxE,可能造成额外的磁盘旋转
- 解决:TxB和TxE中都存一个日志内容的校验和。在执行恢复操作时,如果校验不匹配,则不执行该事务(因为该事务出错了)
- 如果一次写入TxB、待写数据、TxE,可能会出现问题:磁盘会对写入进行优化,使得写入不一定按顺序进行,可能导致TxE先于待写数据写入磁盘
-
-
恢复操作
-
操作:崩溃后,根据日志恢复:
- 如果日志不完整(没有TxE),则跳过该日志
- 如果日志完整,则重做日志
- 即使该事务已经执行完毕,重做也不影响,仅仅是进行了冗余操作。由于崩溃不常发生,故可以接受
-
优化1:使用全局事务缓存
-
优化2:使日志有限
-
问题:过长的日志会使得崩溃后的恢复操作消耗很大时间
-
解决思路:加检查点后的事务,已经真实地写入了磁盘,可以安全的从journal中删除
-
解决方法:将日志视为循环数据结构,一旦事务被加检查点,则释放其空间并且允许重用
- 引入日志超级块,标记日志事务是否已经加检查点(空间是否可以重用)
- 引入日志超级块,标记日志事务是否已经加检查点(空间是否可以重用)
-
-
问题:效率太低,每个数据块都需要写入磁盘两次(日志一次,数据块一次)
- 由于崩溃是少见情况,故这样的代价不值得
-
-
元数据日志
-
思路:对数据日志进行了改良,只存元数据,不存数据
-
问题1:虽然可以保证一致性,但是可能出现元数据更新成功而数据更新不成功的情况,可能会导致指针指向垃圾数据的情况
- 解决思路:先写入数据,然后再写入日志元数据
-
步骤
- 数据写入磁盘
- 日志元数据写入
- 日志提交
- 加检查点元数据
- 释放(已经加检查点的日志)
注意:其中1和2顺序不限定,但是3必须在1、2都完成后再做
-
问题2:删除文件情况下的块复用问题
- 描述:因为目录被视为元数据,故目录更新也要写入日志中。此时如果删除了该目录导致对应的位置空出,然后又新建并写入(原目录所在位置)一个文件,由于文件的数据操作不写日志,如果此时发生崩溃,那么恢复后文件的内容会被之前的目录内容覆盖(因为目录内容是写入日志里的)
- 解决:可以永远不重复使用块,直到块的删除加上检查点;Linux ext3中的做法是添加撤销操作到记录中
-
-
总结:数据日志和元数据日志的时间线
其他解决思路
- 软更新:对文件系统所有写入操作进行排序,确保永远不会产生不一致的情况
- 写时复制(COW)
- 基于反向指针的一致性