持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
系统日志
我们已经讨论了文件系统各种数据结构的实现和,如文件、目录、以及其它元数据。但是不像其它的数据结构,文件数据必须持久化,即使在断电的过程和断电之后都能够保持安全,假如在传输数据的过程中断电,应该怎么办?这就是我们常说的crash-consistency problem,即崩溃一致性问题。
如何在崩溃的情况下依然安全的更新磁盘数据?操作系统可以在任意两个写操作之间崩溃,系统重启后希望上一次没有更新成功的数据再一次更新入磁盘中,关于这一方面我们主要讨论的是日志记录的方法,不过首先讲解一下老系统的方法,也就是文件系统检查器。
我们直接以一个例子开始,以一个典型的4KB追加写工作流为例,调用lseek()将文件偏移量移动到文件的末尾,然后对下列的结构进行操作
和我们之前学的一样,仅写入一个文件就会对三个结构进行更改,data bitmap、inodes和data block,但是一般写入操作在内存中驻留5-30秒之后再进行,三个写操作大概不是同步的,因此如果发生崩溃,即使任何一次写操作失败都是可能引发磁盘错误,具体的,假如只有实际的数据块写入成功,则问题不大,因为inode和bitmap都还没有更新;只有inode更改成功的话会造成文件系统不一致问题,因为bitmap告诉我们数据块5还没有分配,而inode告诉我们数据块5已经分配了。并且inode指向的是垃圾数据;只有bitmap写入成功,则会造成空间泄露问题,文件系统永远不会再使用第5块。其它的情况也类似,总之,崩溃会使文件系统结构处于混乱的状态,我们提出的解决方案必须覆盖所有的问题场景。
早期系统采用了一个麻烦的解决方案,File System Checker(FSCK),就像一个大型的修复工具包一样,fsck对任何有可能出现错误的地方进行排查,包括superblock、bitmap、inode和datablock。甚至是相关的inode,做法就是一步步的排查。这种做类似于把钥匙掉在卧室的地板上,然后开始搜索整个房子的钥匙恢复算法,从地下室开始,逐一搜索每个房间。这很有效,但很浪费。每次崩溃之后排查整个磁盘可能会花费数分钟甚至数小时。
现代系统从数据库管理系统中吸取灵感,采用一种称为日志系统或者提前写日志的方法。第一个这样做的文件系统是Cedar,许多现代文件系统使用了这种思想,包括Linux的ext3和ext4、reiserfs、IBM的JFS、SGI的XFS和Windows的NTFS。
我们从Linux ext3入手,看看日志系统是怎样实现的,简单来说,日志就是在写入操作的时候将一部分的写入信息存储在journal块中,用于缓存写入操作。每个写入操作都是一个事务transaction,一个事务就是一次更新的总体,内部包含有关这次更新的全部信息。
但是这样有一个问题,如果写入日志是一个个进行的,就很缓慢。所以我们希望成批的写入日志来提高效率,但是如果在这一过程中发生崩溃的话,则在恢复后重写进行操作时将错误信息也写入磁盘,这相当于没有日志。如何处理这个问题呢?①在ext4中,日志的写入是成批的,但是会在写入后总体更新前进行校验,如果校验不通过,则将这一段的更新全部抛弃。②而在ext4之前,日志的写入是分两阶段的,先将除TxE以外的所有消息写入日志块,确认可以完成写入后将TxE写入日志,这一阶段叫做日志提交。
我们也可以利用日志来提升写入操作的效率,具体的说,对相同位置的重复写入是很可能发生的,因为文件的改动通常都在同一个文件夹下进行。因此我们可以设立一个全局事务,缓冲所有更新,但是仅将数据块标记为dirty,将多个更新同时写入到块中。
日志的写入空间应当是有限的,当某一个事务完成后,应当进行标记,并在新事务进入时进行替换。因此我们可以设计一种日志超级块,并且在总体的workload中加入一个free步骤。具体的,加入超级块有两个作用:①检查哪些事务还没有被checkpointed减少恢复时间 ②记录哪些事务是free的,以提供后续更新覆盖
对于更新过程依然有一个问题,如果每一个写入操作都要在日志中进行缓存,那么磁盘写入速度将大打折扣,因为相当于每一次数据都要写入两次。为此,我们引入元数据日志记录,意思是占磁盘IO最大的数据块写入不进行两次,只进行一次直接写入,而其它的写入则要先缓存在日志中。这样做确实有效,但是我们又要解决另一个问题,什么时候写入数据块才能保持一致性?
一些文件系统的做法是将数据块的写入作为整个workload的开头,保证数据块的写入完成是一次完整更新的前提,如果数据块写入失败,则整个更新会被直接放弃。这种方式被称为元数据日志系统。在大多数系统中,元数据日志记录(类似于ext3的有序日志记录)比完整数据日志记录更受欢迎
在向日志(步骤2)发出写操作之前强制完成数据写操作(步骤1)是不需要正确性的,如上面的协议中所示,意味着它们的顺序不需要固定,也可以并发执行,这一点在之后会讲到。
最后一个需要解决的问题是,当崩溃发生并恢复时,整个日志中的更新会被重新执行一次,但是明显这是有问题的,先不说很多事务本身是已经完成的,也有很多事务本身是冲突的。这个问题有很多解决方案。例如,对于一个块可能永远不会重用,直到该块的删除被替换出日志,Linux ext3所做的是将一种新类型的记录添加到日志中,称为撤销记录。当重播日志时,系统首先扫描这些撤销记录;任何此类被撤销的数据都不会重放,从而避免了上述问题。
现在我们总结一下日志系统工作的全流程:
- 数据写入:将更新写入数据块,系统等待写入完成(或者不等);
- 日志元数据写入:将开始块和元数据写入日志块,系统等待写入完成;
- 日志提交:将结束块写入日志块,系统等待写入完成;
- 检查元数据:将元数据写入磁盘,系统等待写入完成;
- 释放空间:将事务标记为已完成,并释放空间;
如果从时间线来看的话,可以画出如下图所示:
当然关于一致性问题不止有这两个解决方案,还有:
- Soft Updates:这种方法小心地安排对文件系统的所有写操作,以确保磁盘上的结构永远不会处于不一致的状态。软更新需要对每个文件系统数据结构有复杂的了解,因此给系统增加了相当多的复杂性。
- Copy-On-Write:它被用于许多流行的文件系统,包括Sun的ZFS [B07]。这种技术永远不会覆盖现有的文件或目录;相反,它将新的更新放到磁盘上以前未使用的位置。在完成多次更新之后,COW文件系统会翻转文件系统的根结构,以包含指向最新更新结构的指针。
- Backpointer:系统中的每个块都添加了一个额外的反向指针;例如,每个数据块都对它所属的索引节点有一个引用。当访问一个文件时,文件系统可以通过检查forward指针(例如,inode或direct块中的地址)是否指向指向它的块来判断该文件是否一致。
- optimistic crash consistency:通过使用广义的事务校验和的形式,尽可能多地对磁盘进行写操作,并包括一些其他技术,以检测出现的不一致性。对于某些工作负载,这些乐观技术可以将性能提高一个数量级。然而,要真正发挥作用,需要一个稍微不同的硬盘接口。