持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情
日志结构文件系统
承上一章节的内容,这次我们讨论“Copy-On-Write”类型的文件系统,也称作日志型文件系统,它最初的开发是基于以下事实:
- 系统内存增长,文件系统的性能很大程度上取决于写性能,而写入操作的大小会使用到大量内存;
- 随机IO与顺序IO性能差距很大,如果能以顺序的方式使用磁盘,能够获得巨大的性能优势;
- 现有的文件系统在许多常见的工作负载上表现不佳,FFS会导致短寻路,和很多的旋转延迟;
- 不支持RAID,具体的,RAID-4和RAID-5都有small-write-problem,现有的文件系统不会试图避免这种最坏情沉下的RAID写行为。
高效的系统思想往往都很简单,真正产生作用的是不计其数的细节,真正构建一个工作系统,必须考虑所有棘手的情况。正所谓,the devil is in the detail
实现利用顺序写性能优势的文件系统是Log-structured file system,简称LFS,也称为日志型文件系统。LFS 首先在内存段中缓冲所有更新(包括元数据),这样的一组缓存数据称作segment,当segment已满时,它被写入磁盘,通过一长段顺序写入到磁盘的一个未使用的部分。也就是说,LFS 从不覆盖现有数据,而是总是将段写入空闲位置。因为段很大,所以可以有效地使用磁盘(或RAID),由此文件系统的性能达到了顶峰。
如何将文件系统的所有写入都转换为一系列对磁盘的顺序写入?由一个最简单的写入来说,将数据块写下之后,将inode写在它旁边,这种同一次写入的数据挨着写的做法就是LFS的基础思想。仅这一点,就和我们之前所架构的FFS有根本的不同。
在这种情况下,我们必须要对写入进行缓存,为什么呢?试想一下,假如两个紧挨着的块是分两次写入的话,很有可能在中间会遭到旋转,仔细想想!我们需要将大块的顺序写数据缓存到内存中。segment越大,写入操作就越高效,试想一下,假如完全不需要变道,写入的效率就是磁盘传输的带宽,这是非常快的。
虽然说segment越大越好,但是实际上缓存多少是合适呢?其实这无非就是一个计算多大缓存能够将定位开销摊开(摊销)的问题,具体的计算过程不重要,举例来讲,一个定位时间为10ms,峰值带宽为100MB/s的磁盘,如果想达到90%的有效带宽。按照计算就大概需要9MB大小的buffer。
在以前的FFS中,inode被组织在一个数组中,但是在LFS中,inode是随意写入的,我们已经把索引节点分散到整个磁盘上了!更糟糕的是,我们从未在适当的位置覆盖,因此最新版本的inode(即我们想要的那个)一直在移动。
解决问题的关键在如何找到inode,我们直接再加一个间接层,具体的,再添加一个imap结构,始终指向inode的最新位置,当inode位置更新,它所指的位置跟着更新。那么imap应该在磁盘的什么位置呢?它当然可以存在一个固定的位置,但是这样和inode就没有什么区别了,依然会增大磁盘开销。因此,我们在每次更新文件时将imap更新到新数据写入位置的附近。
我们常可以发现,间接的方法非常实用,只需要增加一点点的开销就能换取更好的架构
但是总是需要一个固定位置的数据让磁盘开始扫描的,这个数据结构就是checkpoint-region,简称CR,它记录各个文件所占块的大小和imap所在位置。因此,在LFS中读写一个文件的流程就是CR->imap->inode->datablock。具体如下图所示
我们只讨论了文件在LFS中的变化,那么目录呢,幸运的是,目录的结构基本上与经典款相同,因此不需要多做改变。如下图所示。
我们通过LFS还顺带解决了另一个问题,通过inode映射,即使inode的位置可能会改变,但是这种改变永远不会反映在目录本身,也就是说不会出现目录的递归式更新问题。
接下来进入到一个重头戏,在LFS里面,系统写入新数据时只在磁盘空余的位置写,也就是说旧版文件都是垃圾文件,需要进行收集并回收。举例来说,典型有如下图所示的两种垃圾数据。
一种处理方式是留着这些数据,并允许用户恢复旧的文件版本(例如,当他们不小心覆盖或删除一个文件时),这样做会非常方便,这样的文件系统称为版本控制文件系统,因为它跟踪文件的不同版本。
但是对于LFS来说,只有最新版的文件是有用的,所以需要设计一种能够收集并清理旧版文件的功能,首先能够想到的就是定期扫描整个磁盘,然后将所有的垃圾文件清除。但是不说这样性能很差的问题,如果是一个个清除的话,会有很多内部碎片产生。因此我们的策略是利用segment,定期的把各个段少量的不可覆盖数据汇总起来,再写入新的段,之前的所有空间就可以覆盖了。这样一段一段的清除垃圾数据能够最大程度的利用磁盘的传输带宽。
因此问题就落在了怎样检测哪些块是可覆盖的,而哪些是不可覆盖的。我们再次添加一个间接结构,在每个段的头部添加一个segment summary block,用于记录段中每个数据块D,inode号(它属于哪个文件)和它的偏移量(它是文件的哪个块)。如果想查看一个块是否可覆盖,就只需要在正常的查找过程中添加一个和segment summary block,进行一次验证。
通过例如版本号等方法,确定数据块是否可覆盖的过程可以更高效
在上面描述的机制之上,LFS必须包括一组策略,以确定何时清理以及哪些块值得清理。决定什么时候清洗更容易;可以是周期性的,或者在磁盘空闲的时间,或者当磁盘已满,而不得不清理空间时。
决定清理哪些段则更有挑战性,关于这一点有许多研究者提出了许多方法,这里挺复杂的,而且设计自由度很高,所以不展开讲了。
最后一部分是崩溃恢复,这里涉及到两个方面,一是在写入段的时候崩溃,二是在写入CR的时候崩溃
对于第一个方面
因为LFS每30秒左右写一次CR,文件系统的最后一个一致性快照可能非常旧。因此,在重新引导时,LFS可以很容易地恢复,只需读取检查点区域、它所指向的imap块以及随后的文件和目录;但是,最后许多秒的更新将会丢失。
为了改进这一点,LFS试图通过一种数据库社区中称为前滚的技术来重建其中的许多段。其基本思想是从最后一个检查点区域开始,找到日志的结尾(包含在CR中),然后使用它读取下一个片段,并查看其中是否有任何有效的更新。如果存在,LFS相应地更新文件系统,从而恢复自上一个检查点以来写入的大部分数据和元数据。详情可以查阅原文中的论文。
对于第二个方面
为了确保CR更新是自动发生的,LFS实际上保留了两个CR,一个在磁盘的两端,并交替地对它们进行写操作。当使用inodemap和其他信息的最新指针更新crs时,LFS也实现了一个谨慎的协议;具体来说,它首先写出一个报头(带有时间戳),然后是CR的主体,最后是最后一个块(也带有时间戳)。如果系统在CR更新期间崩溃,LFS可以通过查看不一致的时间戳对来检测到这一点。LFS将始终选择使用具有一致时间戳的最新CR,从而实现CR的一致更新。