背景
这几年随着SSD等高性能介质的普及,及其在大规模分布式存储系统上的应用。基于Append only的日志写入技术也应用得越来越多,这几天刚好有空,重读了Ext4文件系统的日志部分的内容,也正好看到一篇对Ext4日志技术进行优化的论文,该论文中的优化方案,作为Ext4经典日志技术的补充,随着Linux 5.10内核一起发布,Geek玩家,可以尝尝鲜。
Ext4日志及不足
基础知识
- Ext4日志,第一要义是保证文件系统的一致性,第二才是数据的完整性。(言外之意:开启日志,数据也可能会丢)。
- Ext4日志,在文件系统中是一个文件,默认Inode号为8,大小为一个块组(128MiB),日志文件通常存储在文件系统分区的中部;当然在格式化文件系统时,可以通过-J参数设置日志的大小。
- Ext4支持三种日志模式
- ordered,日志中只记录元数据,数据写入磁盘后才会提交日志。这是默认模式。
- journal,日志中同时记录元数据及数据内容,最安全也是最慢的模式,同时会导致延迟分配及Direct-I/O失效。
- writeback,日志中只记录元数据,但是不保证元数据和数据的写入顺序。(言外之意:元数据提交了,数据可能还没有落盘)。
- Ext4日志,可以关闭,即采用无日志模式运行
- Ext4日志文件,默认放在文件系统的中部区域,称为内部日志;为提升日志的可靠性,也可以将日志配置在独立的设备上,称为外部日志,你可以在配置及挂载ext4文件系统时,通过-journal-dev参数设置外部日志设备
日志结构
Ext4采用JBD2进行日志管理,日志的详细磁盘结构, 总结如下:
- 内部日志
日志文件由一个超级块和若干日志事务组成,每个日志事务包含一个描述块,若干撤销块和(或)若干数据块,以及一个提交块
- Block Header: 日志中的每个块都包含一个12字节的通用块头,包括:4字节的魔数,4字节的类型,4字节的事务ID。
- Superblock:日志超级块(内嵌Block Header),存放在日志文件的头部,占用1024字节,用来记录日志文件的结构信息。
- Descriptor Block: 描述块(内嵌Block Header),至少36字节,占用一个block,包含一组块标签用来记录事务中数据块的位置,块标签内容会因为JBD2的标志设置的不同而不同,通常包含一个标识,一个块地址以及校验码。
- Revocation Block:撤销块(内嵌Block Header),至少16字节,占用一个block,包含一组需要撤销的块ID,撤销块用来表明之前的某个日志事务中的块不再需要重放
- Commit Block:提交块(内嵌Block Header),32字节,占用一个block,通常包含校验码和时间。提交块,作为一个哨兵块,表明日志事务已经落盘
- 外部日志
外部日志与内部日志很类似,只是因为放在了外部设备上,多了一个标准的Ext4超级快(及前置填充)
日志原理
JBD2的操作单位是块(block),提交一个日志的时候,会包含日志事务中的所有块,因此一个逻辑操作可能影响多个块。(言外之意:一个日志事务中可能包含多个变更,也可能包含与本次逻辑操作不相干的变更内容)
背景知识
- 在Linux系统中,一个典型的文件系统I/O要经过App -> VFS(Page Cache)-> (文件系统)JBD2 -> 块I/O子系统 -> 磁盘。Page Cache中的文件数据由系统在必要时通过回写线程(BDI设备的kworker线程)或者用户fsync回写,JBD2日志缓存中的数据由JBD2线程定时或者用户fsync回写。
- 在调度层,回写线程(flush)和fsync的I/O优先级不同,fsync调用携带了SYNC标志,优先级更高,所以fsync的I/O会加入到CFQ的同步队列,回写flush的I/O会加入CFQ的异步队列。
下图展示了日志提交,及回写(flush),用户fsync的相关过程
- 图(a):用户进程Process1,Process2,Process3分别向文件fileA,fileB,fileC写入内容(缓存在Page Cache),回写线程为脏数据分配数据块(Ext4开启了延迟分配的话,在回写的时候才分配数据块),将相关的inode注册到running状态的日志事务中(如果当前没有running状态的事务,创建一个新的日志事务,然后设置为runing状态),然后flush数据块(1231)到异步队列。
- 图(b):图(a)回写继续进行,用户进程Process2更新文件fileB,然后发起了fsync,用户进程被block,fsync做了两个动作:首先更新相应的元数据块并添加到running的日志事务中,然后回写fileB的数据块(2)到同步队列。图(a)中的回写还在继续,fileA的数据块(12)已经持久化到磁盘,fileC的数据块(12)添加到了异步队列。
- 图(c):图(b)中的fsync继续进行(还未返回),系统发起了JBD2日志提交,事务状态修改为commiting(如果之前已经有事务处于commiting状态,那么当前的提交会被block), 并等待返回。由于日志事务中包含fileA,fileB,fileC的信息,所以三个文件的脏数据都会被回写。
- 图(d):图(c)中的数据回写完成后,日志事务写入到磁盘,状态修改为checkpoint。fsync返回,用户进程Process2返回。
总结
根据上文的分析,我们归纳出Ext4的日志机制有如下的不足,并导致I/O延迟增加
- JBD2串行事务依赖,为了避免并发提交重叠事务带来的一致性风险,每个时刻只能有一个事务处于commiting状态。
- JBD2组合事务提交,JBD2事务中可能包含多个不相干的inode,当发起事务提交时,会将所有文件的数据块都提交。
- JBD2组合事务可能使延迟分配失效,因为组合事务提交的原因,不相干的文件也被提交,导致延迟分配策略失效。
iJournal日志技术
与JBD2块级的日志机制不同,iJournal是一种文件级的日志技术。iJournal已经应用到Linux 5.10,作为JBD2的一个补充,JBD2日志称为标准提交日志,没有变化,新增的iJournal日志称为快速提交日志,核心目的是提升fsync的性能。下面我们一起来看下iJournal的工作原理
核心思想
iJournal用来提升fsync的性能,因此只在调用fsync的时候才会触发。iJournal日志事务保存在iJournal日志区,iJournal将生成的日志事务保存在iJournal日志区,但并不会提交/影响JBD2中的running日志事务,同时iJournal中提交的元数据块,也会继续提交到JBD2日志事务中。iJournal日志事务只包含最基本的用于恢复的文件级元数据,如:Inode项,外部extent结构(非内联在inode的extent)以及相关的目录项,因此比JBD2日志要小得多。
下图展示了通过iJournal恢复块位图的一个示例:通过日志事务中的inode项或者外部extent中记录的extent信息恢复块位图,如图,extent中包含30和31两个数据块,因此可以用于恢复块位图中的第30和31号位。
日志事务
iJournal用独立于JBD2的区域来保存日志,为了支持多核,采用per-core的设计,即:每个核一个iJournal日志区。下图是iJournal日志事务的格式,包括两种日志事务:
- 文件日志事务, 包含的是fsync的文件的元数据,
- 目录日志事务, 包含的是与fsync相关的父目录的元数据。
iJournal header:
iJournal日志事务头,占4K,包含一个JBD2日志头(Block Header),4字节的Inode号,256字节的文件Inode结构以及若干块标签。
- iJournal日志事务与包含同一个文件的JBD2日志事务有相同的事务号,iJournal日志分布在多个日志区,恢复模块根据事务号来鉴别事务顺序。另外,在一个JBD2的running日志事务过程中,可能发生多个文件fsync,因此可能存在多个有相同事务号的iJournal事务,为了解决这些事务的顺序问题,iJournal引入了全局子事务号,该子事务号随着fsync的调用而线性递增。
- 文件inode号,用来恢复inode表,inode位图及块组描述表。
- 块标签,每个块标签表示的是日志文件中一个external extent到文件块的映射。进行一致性恢复时,会使用inode结构中的internal extent,块标签,以及external extent来恢复块位图。
文件日志事务
文件日志事务包含的是fsync的文件的元数据,由一个iJournal日志头,若干external extent(如果有)以及一个提交块组成。
文件日志事务中,只包含脏的external extent,为了减少搜索extent树的开销,iJournal的引入,需要对文件系统进行如下的修改:为每个文件维护一个未提交的脏extent列表,在extent创建/删除时对列表进行更新。【每个列表元素占用20字节,看起来开销可接受】。
目录日志事务
目录日志事务,记录的是fsync文件的父目录的更新,有一个iJournal日志头,若干目录项(DE)以及一个提交块组成。举个例子:
创建目录/A,子目录/A/B, fsync文件/A/B/c,这个时候iJournal需要记录目录/A, /A/B,
以及文件/A/B/c的变化。提交日志的时候,也需要将目录也一并提交。
为了跟踪未提交的目录,需要在文件的Inode中添加uncommitted_DE
标志。创建文件时,设置该标志,表示文件目录项还未记录在父目录块中。父目录块被JBD2提交后,清除该标志。iJournal在创建fsync日志记录时,会先检查文件inode中的uncommitted_DE
标志,如果标志被设置,就会递归的查找未提交的父目录,然后创建目录日志事务,并按照最顶级目录,下一级目录…的顺序将目录记录在目录日志事务中,最后才创建文件日志事务。【注:为了减少解码目录块的时间消耗,目录日志事务中每个DE项会记录完整的目录块】
如果一个fsync的文件,没有外部extent以及父目录DE块被修改,那么所有的信息可以保存在iJournal日志头中,只占用一个块大小。
故障恢复
与JBD2日志不同,iJournal日志只用于恢复与fsync相关的目录和文件。为了简化iJournal的实现,某些场景也是用JBD2日志来实现
- 对于目录的fsync,用JBD2日志来实现;
- 对于包含文件硬链接的目录,用JBD2日志来实现;为了处理这种情况,在文件的inode中添加
uncommitted_HL
标志,当inode中的i_link_count
计数因为硬链接增加时,设置该标志。JBD2提交了日志后,清除该标志。fsync调用会检查这个标志,如果该标志被设置,就调用JBD2日志。
进行故障恢复时,恢复模块先扫描JBD2标准日志区,提交那些还未checkpoint
的日志事务,同时找到最后提交的日志事务号Max_TxID
。然后,扫描iJournal快速日志区,只恢复那些合法的iJournal日志事务:合法的iJournal日志事务是指那些事务号大于Max_TxID
的事务,如果一个inode有多个iJournal事务,那么拥有最大子事务号的事务才是合法的。怎么理解,请看下文:
下图(a)显示了一个日志提交场景
下图(b)显示了一个文件恢复场景
- 图(a),在时刻30,JBD2事务号TxID=n的日志事务被提交,事务号+1,变成TxID=n+1;在事务号TxID=n+1的事务周期内,文件fileB,fileC,fileD发生变化,两个进程分别发起了针对文件fileC,fileD的fsync。
- 图(b),iJournal事务号(TxID, sub-TxID)= (n+1, 0)和(n+1, 1)的日志事务分别包含fileC和fileD的已提交日志事务。
- 图(a),在事务号TxID=n+1的事务提交前,系统Crash。图(b),因为JBD2事务号TxID=n的事务已提交,所以iJournal事务号TxID=n的事务不合法,所以恢复时,iJournal恢复从事务号TxID=n+1的事务开始。图(a),在TxID=n+1事务期间的文件fileB的变更丢失。
iJournal恢复
因为每个fsync调用会生成一个文件日志事务和若干目录日志事务,如果在处理fsync过程中,发生系统Crash,fsync生成的多个日志事务无法实现原子提交。此外,目录事务中的DE块也包含不相干文件的信息,因此,在进行故障恢复时,并没有直接将DE块复制到文件系统上,而是先识别出发生变化的目录项,如果上述目录项指向的inode可以访问,就用DE块更新文件系统。
图(a)显示了iJournal恢复的初始状态,inode号等于3的文件,包含3个external extent,包含24个数据块;图(b)进行了一些文件操作,首先10个数据块(50-59)及在12号块的external extent结构被删除;接着,追加6个数据块(74-79),在13号块的external extent结构被更新;最后发起fsync。
图(c),假定在JBD2提交前,发生了系统Crash。恢复模块,根据iJournal日志,生成inode结构以及external extent树。将生成的inode与文件系统中的inode进行比较,恢复模块通过记录的fsync日志能够识别出文件系统的变化,并应用这些变更恢复系统。12号块的external extent被删除时,JBD2日志记录一个撤销块,用于跳过该日志事务的恢复。同时iJournal也会跳过撤销块的写入。
效果
根据论文中的测试数据,iJournal在桌面应用,移动应用两个场景下,fsync延迟以及多核带宽性能都有非常显著的改善。