从 NILFS2 看 Log-Structure 文件系统

NILFS,BTRFS,Ext4 是最近 Linux 最引人注目的几个文件系统,他们各有特色,代表了 Linux 文件系统不同的发展方向和设计理念。在不同的应用场景下,人们采用会不同的文件系统。本文从 NILFS2 的使用和实现出发,探讨 Log-Structure 这种文件系统设计方法,希望能对理解这种技术有所帮助。

刘 明 (ovis_poly@sina.com), 软件工程师, 上海交通大学电子与通信工程系

刘明,从事嵌入式软件开发,热爱开源软件。喜欢学习和使用 linux,目前致力于数据库方面的工作和研究



2009 年 11 月 19 日

NILFS2 的特性

NILFS2 是一种 Linux 文件系统,人们称它为 New Implementation Log-Structure File System。我一时之间也不知道如何将 Log-Structure 翻译为中文。如果说 Log 是日志,那么可能会和 Ext3 等日志文件系统有所混淆。因此还是直接使用 Log-Structure File System 这个称呼吧。

进入 NILFS2 的细节之前,我想应该先从使用的角度来认识它。文件系统主要的功能是管理文件,NILFS2 也不例外。将 NILFS2 和其他形形色色的文件系统区别开的是它所有的一些独一无二的特性。

不间断的快照(continuously snapshot)

不间断快照应该是 NILFS2 最吸引人的特性。它使得 NILFS2 用户能够方便地恢复被误删除的文件,或者将被误修改的文件恢复到修改之前的内容。

其他一些文件系统也支持 snapshot,但往往需要人工干预,即用户须使用 FS 自带的命令来创建快照。然而,误操作往往不可预料,不可能在误操作之前恰好创建一个快照,所以在其他文件系统中,恢复文件需要非常专业的人员和工具。

NILFS2 的用户则幸运地多,因为系统能自动备份对文件的所有操作。因此 NILFS2 不仅能及时恢复被删除的文件,还能恢复任意修改之前的文件内容。甚至可以说,在 NILFS2 文件系统中,用户不再需要专门的版本管理工具来管理文件的不同版本。而且这一切还是自动的!

对于系统管理员,NILFS2 的不间断快照功能使得 Online backup 等日常操作更加方便,无需再学习复杂的备份和恢复命令,进而从这些繁杂的日常事务中解脱出来。

高效的错误恢复

长期以来,文件系统设计人员最关注的问题之一便是尽量减少系统崩溃后文件系统的检查和恢复时间。无论哪一种文件系统,当系统硬件崩溃时,文件系统便很可能处于不一致状态。因此 reboot 之后,便需要运行 fsck。

Ext3 和其他许多 Linux 文件系统采用日志技术减少 fsck 时间。NILFS2 是一种 Log-Structure 文件系统,其 fsck 时间更短,而且无论磁盘有多大,文件有多少,NILFS2 的 fsck 时间都是一定的。

快速 fsck 有什么意义呢? ChunkFS 的设计者 Valerie Aurora 曾描述过这样一件事情,有一次,Linux Archive 服务器遭遇意外,硬件 reboot 后,执行 fsck 花费了 1 个星期的时间,在这一周内,网站都无法提供访问,人们想,即使用备份来重新恢复数据也没有这么慢吧。在文件系统容量越来越大的时代,快速 fsck 这个特性非常有吸引力。

文件访问效率高

许多研究表明,文件系统的整体效率主要由写操作的效率决定。因为文件系统读操作的效率由 cache 设计的好坏决定。在 Linux 中,cache 由 VFS 统一管理,因此提高写操作的效率能提高整体文件系统的效率。

在 Phoronix 网站公布的文件系统测试中,NILFS2 的性能在多项测试中并不占优,但在数据库应用中 NILFS2 的表现比较优异。比如图 1 所示,在 SQLite test 中 NILFS2 表现出了很高的性能。

图 1. SQLite 测试
图 1. SQLite 测试

Phoronix 在其他的测试中多次使用大文件(几个 G 的大小)。传统文件系统读些大文件时也能很好的利用 IO 带宽。因为大文件意味着磁盘 IO 连续的几率大,seek 时间比较少,因此 NILFS2 并没有优势,反而在多项测试中垫底。

而在 SQLite 测试中,基本上是小批量的写操作,因此在该测试中 NILFS2 表现出了很大的优势。同样在 PostGreSQL 的测试中,NILFS2 也表现不错,比 Ext3/4 慢,但比 btrfs 和 XFS 快很多。

在 SSD 上,NILFS2 则具有绝对的性能优势。

2008 年在 Linux Storage & Filesystem Workshop 大会上,Shin, Dongjun 发表的论文比较了几种文件系统在 SSD 上的 benchmark,结果显示 NILFS2 的读写效率领先于其他的文件系统。图 2 和图 3 分别显示了各种文件系统在 SSD 上的性能比较。

图 2. SSD 上小文件的测试
图 2. SSD 上小文件的测试
图 3. SSD 上大文件的测试
图 3. SSD 上大文件的测试

NILFS2 的使用

从 2.6.30 开始,NILFS2 已经合并入内核。假如您希望在较老的内核下尝试 NILFS2 也非常简单。NILFS2 是一个内核模块,您只需编译后加载模块文件就可以使用它了,无需对内核进行 patch 和重新编译。可以使用下面的命令创建一个 NILFS2 文件系统。

 insmod nilfs2.ko 
 mkfs – t nilfs2 /dev/sda8 
 mount – t nilfs2 /nilfs /dev/sda8

NILFS2 的命令很精简,我不想浪费读者的阅读时间,您可以查阅 NILFS2 源码自带的 README 文件,写得很好。

但我在想,您或许还从来不曾见过一个能够不间断地生成快照的文件系统吧,没准儿还不知道这个特性到底意味着什么。因此我还是打算做一个演示,也许能加深您对所谓 continuously snapshot 的印象。

将下面的内容输入文件 /nilfs/poem:

 Shoot for the moon. 
 Even if you miss, 
 You'll land among the stars.

然后我犯了一次错误,将 nilfs 目录删除了。突然想起有重要的东西保存在那里,于是觉得很后悔。如何弥补呢?假如是 EXT2,或者 btrfs,基本上无计可施。但对于 NILFS2,这就不算什么大事儿。

 [root@localhost nilfs]# lscp 
                 CNO        DATE     TIME  MODE  FLG   NBLKINC       ICNT 
                   1  2009-09-27 12:27:13   cp    -         11          3 
                   2  2009-09-27 12:28:16   cp    -         14          4 
                   3  2009-09-27 12:28:31   cp    -         14          5 
                   4  2009-09-27 12:28:36   cp    -         12          5 
                   5  2009-09-27 12:28:41   cp    -         14          3 
 [root@localhost nilfs]# chcp ss 3

假设我在 12:28:31 左右保存了 poem 文件,然后在 12:28:41 删除了该文件。则需将 12 点 28 分 31 秒的 checkpoint 变成 snapshot,然后 mount 该 snapshot:

 #mount – t nilfs2 – r – o cp=2 /dev/sda8 /recover 
 #cd /recover 
 #cat nilfs/test1 
 Shoot for the moon. 
 Even if you miss, 
 You ’ ll land among the stars.

文件 poem 又恢复了。


NILFS2 的实现

很多喜欢 Linux 的人都喜欢追根究底。NILFS2 如何实现这一切的?这个问题时常困扰着我,在这里,我试图将自己的理解写出来。

简述 Log structure files system

NILFS2 是一种 Log-Structure File System。最早的 Log-Structure File System 由 TCL/TK 语言的创始人 John Kenneth Ousterhout 在 Sprite 操作系统中实现。其基本思想是将底层设备当作一种只能追加写 (append) 的设备。将文件修改顺序追加写入磁盘,而不覆盖旧数据。顺序写入能避免很多寻道 (seek) 操作。seek 是一种机械操作,很难提高速度。因此减少 seek 能极大提高文件系统的写效率。

图 4 演示了 NILFS2 的追加写入,作为对比,图 5 演示了 EXT2 的随机写入。

图 4. NILFS2 的写入操作
图 4. NILFS2 的写入操作
图 5 ext2 的写入操作
图 5 ext2 的写入操作

文件 File1 的内容从 version1 修改为 version2,文件系统会执行两个写操作,一是修改存放文件内容的 block;二是修改存放文件 metadata 内容的 block。

图 5 演示 EXT2 文件系统。File1 文件的内容存放在磁盘 block A 内,File1 的 metadata 存放在 block B 中。Block B 存放在磁盘的固定位置,因此和 block A 不可能顺序排放。修改文件需要写 block A 和 B,如此便需要一个 seek 操作。

在 NILFS2 中,当文件修改时,data 和 metadata 都采用追加的方式写到 disk 的末尾,如图 4 所示。文件本来的 data 存放在 block A,metadata 存放在 block B。NILFS2 并不直接修改 Block A 和 B,而是在 disk 末尾,顺序写入新的 block C 和 D。C 和 D 顺序排放,没有 seek 操作。

追加写入的信息只包括修改的内容。比如文件 File1 占用了 2 个 disk block,但在这次修改中,只有 block B 的内容被修改,那么 NILFS 只会把修改原来 Block B 的内容写入 Block C,另外一个 Block 则原封不动,不会将文件 File1 的所有 block 都追加到磁盘末尾。这种只记录修改数据的操作一般被称为 Log。

下图描述了文件内容增加后的情形。

图 6. 文件内容增加
图 6. 文件内容增加

文件本来存储在磁盘 block A 和 B 中,当增加新的数据后,NILFS2 需要将新数据写入 block C。请注意,NILFS2 只将新的 block C 写入 Log,而未修改的 A 和 B 并不写入 Log。Inode 还指向原来的 A 和 B。

假如写操作并不增加文件大小,而只是修改了以前的内容,比如对保存在 block B 上的内容进行修改,那么新的磁盘结构如下:

图 7. 文件内容修改
图 7. 文件内容修改

Inode 将指向新的数据块 C,而原来的 block B 则不再被引用。

垃圾收集

Log-Structure 文件系统设计中最困难的部分就是垃圾收集。您已经了解到,文件系统的任何修改都将被顺序写入 Log 的末尾,日积月累,终究会写到整个磁盘的最后一个 block。到那个时候,NILFS2 必须回收一部分磁盘空间以便新的数据写入。回收磁盘空间就是垃圾收集的职责。

垃圾收集是现代编程语言的重要研究课题,早在 Lisp 语言中,人们就实现了垃圾收集,用来自动回收程序所分配的内存。虽然传统的 GC 算法主要目的是回收内存,但假如我们将磁盘和 RAM 都当作存储设备,那么他们之间除了读写速率和方式不同之外没有太大的差别。

NILFS2 文件系统借鉴了编程语言的 Garbage Collection 研究成果,采用 Copy Compact 算法来自动回收磁盘空间。

内存 Copy Compact 算法将整个内存空间分成两部分,第一部分给用户使用。当需要进行垃圾收集时,GC 将第一部分中有用的数据拷贝到第二部分,Copy 完成后,两个内存块的角色进行交换。用户便开始使用第二部分分配内存。这样来回切换,从而完成 GC 的工作。

文件系统需要回收磁盘空间时,假如也将整个磁盘分成两部分实在太浪费,并且需要被拷贝的 block 将非常多,效率很低,因为文件数据往往是长期存在,不像内存数据一样经常变化。为此 NILFS2 将磁盘分成多个大小相同的 Segment,在 Segment 间采用 Copy Compact 算法。每次只将某个 segment 中有用的数据 copy 到新的 segment,然后将该 segment 标记为空闲。

NILFS2 的 GC 由用户态的精灵进程 cleanerd 负责。用户调用 mount 时,内核将启动 cleanerd 进程。Cleanerd 通过 ioctl 接口进入内核执行垃圾清理工作。cleanerd 具体的流程涉及到 checkpoint,因此我将在后续章节中再介绍 NILFS GC 的详细流程。

Metadata 文件和 Super Root Block

任何文件系统都需要使用大量的元数据。我们所熟悉的很多其他文件系统,比如 ext2 使用磁盘的固定区域保存文件系统的 metadata。NILFS2 则使用特殊的 metadata 文件保存 metadata。

目前,NILFS2 使用四种元数据文件。

  • Inode file (ifile) 保存文件的 inode,相当于 ext2 的 inode table;
  • Checkpoint file (cpfile) 用来保存所有的 checkpoint 数据;
  • Segment usage file (sufile) 保存着 segment 的磁盘使用情况;
  • Data address translation file (DAT) 将数据块的虚拟地址影射为真正的物理地址,这是为了实现物理数据可重定位。

除了 Metadata 文件之外,NILFS2 还有一个重要元数据,Super Root Block( 简称 SR)。SR 存放在每个 Log 的固定位置。其中存储了三个特殊的 inode,分别指向元数据文件 DAT,cpfile 和 sufile。

各种数据结构之间的关系

前面介绍了很多磁盘数据结构,下图描绘他们之间的关系。

图 8. 各种数据结构的关系
图 8. 各种数据结构的关系

有了这张图,我们便可以尝试描述 NILFS2 文件管理的基本思路。

文件 /nilfs/oldfile 存放在 NILFS2 上。读取 oldfile 的过程如下:

将 NILFS2 文件系统 mount 到 /nilfs 目录时,Linux 会调用 nilfs_get_sb 函数,在内存中构建 NILFS2 的 VFS superblock 数据结构。

从 superblock,NILFS 可以得到最近一个 checkpoint 的 Super Root block。从 SR 中进一步得到 cpfile 的 inode。通过 cpfile 的 inode,可以找到 cpfile 的内容,在其中查找 ifile 所在的 inode block,从而得到 ifile。

NILFS2 的根目录拥有一个固定的 inode 号。用这个 inode 号在 ifile 中查找便可以得到根目录的 inode。

Root dentry 中保存了 /nilfs 的 inode,通过 inode,便可以得到 /nilfs 这个目录文件在磁盘上存放的 block,读取这些 block 便可以得到 /nilfs 的目录文件的内容。目前 NILFS2 的目录采用顺序表结构。

图 9. /nilfs 目录文件的内容
图 9. /nilfs 目录文件的内容

在这个 dir 文件表中可以找到文件 oldfile 的 inode number:oldfile_ino。用它作为索引,在 ifile 中查找 oldfile 的 inode 在磁盘上的具体存储地址。

ifile 中保存了文件系统中所有文件的 Inode,为了提高查找效率,ifile 采用 BTree 保存信息。用 inode number 作为 Key 在 BTree 中查找,便可得到相应文件的 inode。在本例中,用 oldfile_ino 在 ifile 中查找,便能找到 oldfile 所对应的 inode。根据 inode,便可以找到 oldfile 所有的数据 block。进而读取文件的内容。

磁盘布局

NILFS2 将磁盘平均分成多个 segment,除了第一个 segment 之外,其他的 segment 大小都相同。第一个 segment 之所以特殊,是因为这里是磁盘的开头,superblock 存放于此。因此第一个 segment 的大小需要减去 superblock 所占用的空间。如图所示:

图 10. segment 布局
图 10. segment 布局

在每一个 Segment 内,NILFS2 顺序追加 LOG。因此在磁盘上,segment 由连续的 log 组成。每个 log 有一个 summary block;之后是 payload,即文件系统的修改记录,这里不仅存储了普通文件的修改记录,还有 metadata file 的修改记录;最后有一个 SR。

Checkpoint 和 Snapshot

追加写 log 的设计使得 snapshot 的实现非常容易。因为 NILFS2 从来不覆盖原始数据,所以 snapshot 可以很方便地产生。图 11 试图描述 NILFS2 snapshot 的实现原理。

图 11. 快照
图 11. 快照

图 11 中引入了新的术语 checkpoint,checkpoint 是一种特殊的 snapshot,是对文件系统某个一致状态的快照。Checkpoint 这个概念是数据库系统中最先使用的,在事务处理中,使用 Log 记录所有的修改操作,当事务失败,便可以根据 Log undo 所有的修改操作,从而达到回滚或灾难恢复的目的。但随着时间推移,日志会越来越长,以至于恢复的时候需要 undo 的操作过多。checkpoint 就是这些日志中的某个点,在这个点上,数据库处于一致状态,因此数据库恢复时,一旦遇到一个 checkpoint,便可以停止读取日志。Checkpoint 之前的老日志记录也可以被删除以便节省磁盘空间。在 NILFS2 中的 checkpoint 也很类似,表示了文件系统某个一致性状态的点。

在 NILFS2 中,checkpoint 自动产生,当文件系统被修改时,新的 Log 被追加到 Disk 末尾。每个 Log 中都会插入一个 SR 数据块,其中保存了 checkpoint 的相关数据。在图 11 中,通过 checkpoint A 便可以找到文件 File1 的旧数据 Block A 和 Block B。同理,通过 checkpoint B 便可以读到文件 File1 的新数据。

Snapshot 和 Checkpoint 不同。Checkpoint 由 NILFS2 自动建立,但也会自动被删除。当磁盘空间不够,或者 checkpoint 的年龄已经比较老的时候,checkpoint 会被系统的垃圾收集机制自动删除,以便释放新的磁盘空间。Snapshot 由用户建立,用户可以将一个 checkpoint 转换为 snapshot,也可以建立一个 Snapshot,系统不会自动回收 Snapshot。其他方面 snapshot 和 checkpoint 是相同的。

事务处理和 segment construction

很多关于文件的操作都是由多个子操作组成,每个子操作只修改一个特定的元数据,只有所有的子操作都完成,文件操作才算成功;任何子操作失败,就应该回滚到文件系统之前的状态。这些子操作便是一个事务。

事务提交之后,文件系统便处于一致性状态。如前所述,这便是一个 checkpoint。

创建 checkpoint 在 NILFS2 的术语中叫做 segment construction。NILFS2 用一个专门的内核线程来处理 segment construction 的工作。该线程定时被唤醒,假如需要,便创建一个 segment,生成一个 checkpoint。这就是 NILFS2 不间断 snapshot 的具体实现。此外,每次事务提交之后,NILFS2 也将唤醒后台线程,创建一个 checkpoint。

下面以创建文件为例理解事务和 segment construction。

用户程序调用 open() 系统调用,并制定 O_CREAT 选项。VFS 将调用 nilfs 文件系统注册的 nilfs_create() 函数。代码如下:

 static int nilfs_create(struct inode *dir, struct dentry *dentry, int mode, 
			 struct nameidata *nd) 
 { 
	 err = nilfs_transaction_begin(dir->i_sb, &ti, 1); 
	 inode = nilfs_new_inode(dir, mode); 
	 err = PTR_ERR(inode); 
	 if (!IS_ERR(inode)) { 
		 inode->i_op = &nilfs_file_inode_operations; 
		 inode->i_fop = &nilfs_file_operations; 
		 inode->i_mapping->a_ops = &nilfs_aops; 
		 mark_inode_dirty(inode); 
		 err = nilfs_add_nondir(dentry, inode); 
	 } 
	 if (!err) 
		 err = nilfs_transaction_commit(dir->i_sb); 
	 else 
		 nilfs_transaction_abort(dir->i_sb); 
 }

这里主要有两个步骤:创建新的 inode;修改 dir 文件。Nilfs 用 transaction 保护这两个动作。

在 nilfs_transaction_begin 中,nilfs2 将获得一个内核信号量,这样就保证了 transaction 的互斥性。此后调用 nilfs_new_inode 和 nilfs_add_nondir 来修改相应的元数据文件。当这些操作都成功完成后,调用 nilfs_transaction_commit 提交这次事务。

nilfs_transaction_commit 函数首先释放内核信号量,然后唤醒负责处理事务的 nilfs 内核线程。该线程被唤醒后将执行 nilfs_construct_segment 函数,构建一个完整的 Log,即添加 checkpoint,修改 cpfile,插入 super block root,最后更改 superblock 使其指向新的 super block root。

再看垃圾收集

NILFS2 的垃圾收集由用户态进程 cleanerd 执行。垃圾收集算法的具体执行流程如下。

1)首先,cleanerd 通过每个 segment 的 sufile 得到 segment 的空间使用情况。找到哪些 segment 是 reclaimable 的。具体的信息由 nilfs_suinfo_struct 保存,从中可以得到以下这些信息:

  • Sui_flag: 表示 segment 是何种状态,包括 dirty,active,以及 erroneous。某些状态下的 segment 不适于参与垃圾回收。
  • Sui_lastmod:表示 segment 上次修改的时间。垃圾收集一般会选择收集比较老的 segment,和 Java 中的分代收集是同样的理由,往往较年轻的数据更可能短期内会被再次读写,因此成为垃圾的可能性较小。

NILFS2 对所有的 segment 的 sufile 信息进行分析,那些 dirty,non-active 并且 lastmod 最老的 segment 成为候选者。

2)进一步筛选步骤一中选出的候选 segment。

用户通过 /etc/nilfs_cleanerd.conf 文件配置垃圾收集参数,此时 cleanerd 读取配置文件中的 nsegments_per_clean 参数,该参数配置用户希望每次垃圾收集中应该处理多少个 segment。按时间排序,定下最终参与垃圾收集的 segment。

3)您已经了解到,每个 segment 都有一个 summary 头,其中保存了该 segment 中所有 log 的信息。Cleanerd 对 segment 中的每个 block 进一步分析其状态。判断该 block 是 dead 还是 live 状态。Live 状态的 block 正在被使用,因此将被拷贝到新的 log 中。

判断 block 是否为 live 有几个条件:

  • 该 block 被某个 snapshot 索引,因此是一个 live 的 block。
  • 该 block 被某个 inode 直接索引,这说明该 block 正在被某个文件使用,因此是一个 live 的 block。

4)在 segment 中还保存了很多的 checkpoint。Checkpoint 和 snapshot 不同,checkpoint 如果已经比较年老,就将被 cleanerd 收集掉。因此在这个步骤中,cleanerd 查询 segment 中的所有 checkpoint,如果其年龄大于用户在 /etc/nilfs_cleanerd.conf 中的配置,便可以回收这些 checkpoint。否则就将该 checkpoint 所指向的 block 拷贝出来。

5)至此,segment 中所有 live 的 block 都被找到,cleanerd 将这些 block 写入新的 log,然后将被清理的 segment 标志为空闲状态,此后 NILFS2 便可以使用这个 segment 追加新的 log 了。


NILFS2 和 journal 日志文件系统的区别

Ext3 是一种 journaling file system;NILFS2 是一种 Log-Structured File system。如果您第一次看到这些术语往往以为是同一种技术。Journal 和 Log 在词典上似乎没有区别,都可以翻译为日志。Log 的来源是早期航海者用木头记录航海日志,Journal 则是教堂中每日的记事。到了现代英语中,似乎可以通用。

但实际上 Log Structured File System 和 Journaling File System 是两种不同的技术。之间的区别也非常简单:

Journal 文件系统保存在日志中的只有 metadata,而 Log-structure 文件系统则采用日志记录一切改动,包括 metadata 和数据。

Journal 文件系统的写操作是随机写,而 Log-Structure 文件系统则只追加写。


结束语

当试图描述一个文件系统时,我发现很难理清楚头绪。因为文件系统有多条线索,也许一个好的方法是跟踪文件的读,写,以及其他操作的完整流程,但这势必造成代码的罗列,似乎不利于从整体上理解。本文从几个关键技术入手,希望能够描述 NILFS2 的基本原理,为您进一步阅读源代码提供一些背景知识。

或者您和我一样,只是想了解 Log Structure 这种文件系统的设计思想,并不打算深入代码细节。毕竟多数人不会有机会和能力去参与实现一个真正的文件系统,就像一个好东西无法拥有,只可默默关注吧。

参考资料

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=448356
ArticleTitle=从 NILFS2 看 Log-Structure 文件系统
publish-date=11192009