使用 ReiserFS 进行日志记录
近来,Linux 上出现了一些新的文件系统,为服务器和桌面带来了急需的功能。我将简要介绍 ReiserFS 的一些关键特性,并讨论日志层的某些细节。
ReiserFS 将所有文件系统对象存储在单个 B*树中。该树支持
动态 inode 分配
紧凑的索引目录
可调整大小的项目
60 位偏移量
树中有四种基本类型的项目。stat 数据、目录项、间接项和直接项。通过搜索键来查找项目,其中键包含 ID、您要查找的对象中的偏移量以及项目类型。
ReiserFS 目录随着其内容的变化而增长和缩小。文件名的哈希值用于保持目录中条目的偏移量不变。此哈希的树索引允许非常大的目录而不会有太多的性能损失,并且仍然为 NFS 和标准目录操作提供干净的支持。
对于文件,间接项指向数据块,而直接项包含打包的文件数据。此打包的文件数据直接存储在树中,并且可以与来自其他对象的项目共享树节点中的空间。因此,对于大文件,ReiserFS 存储类似于 ext2 使用的块指针,但对于小文件,我们将数据打包在一起以防止空间浪费。
所有这些项目都可以通过重新平衡树来调整大小。我们可以附加到打包的文件数据,或者如果我们需要 stat 数据中的另一个字段,它可以增长以容纳新信息。磁盘格式值得比我在这里给出的更多细节,您可以从 ReiserFS 主页上的论文中了解更多信息(请参阅“资源”)。
ReiserFS 实际上有两种主要的磁盘格式。我们在 2.4 代码中引入的新格式允许 60 位文件偏移量,而我们在 2.2 代码中使用的格式使用 32 位偏移量。当您在新内核下挂载较旧的文件系统时,将保留旧格式,并且不允许大文件。
有一个用于转换为新格式的挂载选项,但是用于在 2.2 内核下挂载新格式的代码仍处于 beta 阶段。与其在本文中发布过时的信息,我建议访问 ReiserFS 网站以获取有关启用大文件支持的详细信息。
我在这里的目标不是描述 API 或日志数据结构;我希望列出文件系统日志记录中涉及的主要问题以及 ReiserFS 如何处理这些问题。
在讨论日志记录如何工作之前,让我们先讨论我们试图解决的问题。为了在崩溃后拥有一致的文件系统,更新需要是原子的。它们需要完全发生或完全不发生。例如,要将块附加到文件,您需要更新文件的块指针,从空闲列表中分配块并更新超级块。如果系统在这些更改过程中崩溃,则文件可能具有指向仍在空闲列表中的块的指针,或者超级块可能没有更新其中的统计信息,或者您分配的块可能会丢失(不在文件中也不在空闲列表中)。
ReiserFS 日志使用简单的仅元数据、预写日志记录方案。其思想是在将任何更改写入磁盘之前,首先将其提交到日志。崩溃后,将重放已提交的事务,这无非是将块从日志复制到主磁盘区域。
将更改写入日志并不是使日志记录复杂的原因。困难的部分是防止日志将您的文件系统速度降至爬行。最明显的优化是以大的顺序块写入日志,并减少写入的提交块的数量。大多数操作更新少量块,因此日志将多个操作组合成一个大的原子单元。
修改后的缓冲区在复制到日志之前无法刷新,并且在刷新之前无法释放。较大的事务会占用更多的内核内存,但也使许多其他优化成为可能。由于 ReiserFS 将所有内容存储在平衡树中,因此树经常需要平衡。树块被分配、修改,然后在稍后的另一个平衡中释放。通过较大的事务,我们增加了块在写入日志或主磁盘之前被释放的机会。
块被一遍又一遍地记录是很常见的。如果超级块包含在事务一、二和三中,则每个事务都需要将其写入日志一次。但是,在事务三完成后,才需要将其写入主磁盘区域。所需的写入总数较低,并且大多数写入都是到顺序日志的。在某些情况下,这实际上使日志记录比原始文件系统更快。
在可能的情况下,日志 I/O 由工作线程 kreiserfsd 完成。这允许日志提交在后台发生,而不会减慢用户进程的速度。但是,日志的大小是固定的,因此用户进程可能必须等待日志空间可用,然后才能启动新事务。必须非常小心,以确保等待日志的进程不会占用已在事务中的进程所需的资源。
大多数文件系统不需要意识到有一个日志层在保持安全,但是有一些新的规则需要遵循。首先,修改脏缓冲区是不安全的。在 SMP 系统上,当您更改缓冲区时,另一个 CPU 可能正在写入缓冲区,这意味着修改将在事务提交之前到达磁盘。
大多数操作将更改有限数量的缓冲区,但是文件写入和截断实际上是无界的。我没有在日志层中添加无界事务大小的复杂性,而是选择在这些操作中编写一致性点。如果当前事务需要结束,它们会记录足够的信息以使文件系统保持一致,然后启动新事务。当使用数据日志记录时,fsync 需要执行相同的检查。
日志层要求的另一个新规则与在使用仅元数据日志记录时重用块有关。想象一下这两个事务
1. allocate block 200, insert into the tree<\n> change and log block 200 free block 200 close and commit transaction 1 2. allocate block 200 as a data block change block 200, fsync to disk close and commit transaction 2 [system crash]
崩溃后,事务按顺序重放。在重放事务一时,块 200 的日志版本被复制到主磁盘中,并且在重放事务二后,块 200 是文件中的数据块。但是,fsync 写入块 200 的内容不再存在。ReiserFS 通过从不分配数据块来避免这种情况,直到没有日志重放会用旧信息覆盖内容的可能性为止。当文件系统已满时,这意味着我们必须将事务刷新到磁盘并找到可重用的块。如果数据块被记录然后稍后直接写入,则需要进行类似的检查。
既然每次崩溃后都不需要 fsck,我们需要更加小心丢失的文件。取消链接的文件实际上直到使用它的最后一个打开进程完成才会被删除。如果系统在删除操作完成之前崩溃,则日志将提供一致的文件系统,但仍会为该文件分配一些空间。由于该文件不在目录树中,因此无法回收这些块。
在 ReiserFS 中解决此问题的最简单方法是将文件链接到特殊目录中。ReiserFS 目录非常快,如果您不担心文件名冲突,则几乎不涉及锁定。崩溃后,读取目录,并完成任何剩余对象的文件删除。特殊目录实际上根本不需要文件名,只需要查找文件的关键信息。此修复程序尚未集成到 ReiserFS 官方版本中,但应该很快就会集成。
有时,人们会要求提供导出到用户空间的事务 API 版本。ReiserFS 日志层旨在支持通常很快完成的有限操作,并且它不适合通用事务子系统。但是,向用户空间提供原子写入并让他们更好地控制操作的分组可能是一个好主意。这样,应用程序可以请求在特定目录中创建 64K 文件,并将其视为原子操作。到目前为止,在这个领域几乎没有进行任何规划。
当系统内存不足时,内核需要开始将脏数据刷新到磁盘,以便可以释放页面。但是,来自未提交事务的固定缓冲区在事务提交之前无法释放,这使得 VM 在没有文件系统的帮助下无法执行任何操作。我们还希望确保日志不会使用过高比例的系统内存来存储固定缓冲区。
我们将与 VM 开发人员合作,以便正确地向文件系统施加内存压力。API 尚未最终确定,但人们似乎倾向于与页面关联的刷新回调以及通用的内存压力注册系统。目前尚不清楚其中有多少将在 2.4 内核中发生,以及将为 2.5 留下什么。
LVM 为 Linux 增加了很多很酷的新功能,其中之一是能够创建设备的只读快照。快照创建速度非常快,并且使用写时复制来保持快照在原始设备被修改时保持不变。这允许对几乎任何文件系统上的大多数软件进行在线、一致的备份。
但是,带日志的文件系统使这变得有点困难。当在 ReiserFS 上调用 sync 时,我们只是将元数据更改提交到日志,知道重放将在崩溃后使事情保持一致。对于只读 LVM 快照,日志重放不是一种选择。相反,我们可以提供一些新的通用调用,将所有内容刷新到主磁盘并暂停新的文件系统修改。当事情暂停时,LVM 初始化快照,因此它将在没有日志重放的情况下保持一致。一旦 LVM 完成,文件系统将被解锁,写入将正常进行。
由于所有文件系统操作都需要能够等待日志,因此这在 ReiserFS 中很容易编码。LVM 0.9 和 ReiserFS 3.6.18 具有此功能,但我们不确定通用调用何时会添加到内核中。无论如何,提供缺失部分的补丁程序将在 ReiserFS 和 LVM 网站上提供。
LVM 的另一个功能是能够将区段从一个设备重新定位到另一个设备。如果您发现磁盘的某个区域的流量高于平均水平,则可以将这些块重新定位到更快的设备。实际上,您可以将整个日志区域重新定位到更快的设备,从而减少磁头争用和驱动器寻道。将日志重新定位到固态磁盘可以真正提高日志密集型应用程序的性能。
在 2.2 内核中,软件 RAID 代码可以将固定缓冲区写入磁盘,这破坏了用于保持事物一致的写入排序约束。只有驱动器条带化和连接是完全安全的,并且只要您不使用在线重建代码,镜像也是安全的。在 2.4 内核中,所有软件 RAID 级别都应与带日志的文件系统正常工作。
ReiserFS 在支持 NFS 方面存在问题,因为需要在树中找到对象需要 64 位信息,而 NFS 希望仅使用 inode 号码(32 位长)找到 inode。好消息是 NFS 文件句柄有足够的空间来存储 ReiserFS 以后再次找到文件所需的额外信息,并且其他内核开发人员编写了 API,使文件系统可以控制某些文件句柄。到本文发布时,应该有公开的补丁来为 ReiserFS 添加适当的 NFS 支持。
对于性能基准测试,一些新驱动器默认具有写回缓存。这意味着驱动器报告写入已完成,但实际上尚未在介质上。该块仍位于驱动器的缓存中,可以在其中重新排序写入。如果发生这种情况,元数据更改可能会在日志提交块之前写入,如果机器断电,则会导致损坏。禁用 IDE 和 SCSI 驱动器上的写回缓存非常重要。
一些硬件 RAID 控制器提供电池支持的写回缓存,如果系统断电,它可以保存缓存内容。这些应该是可以安全使用的,但应经常检查缓存电池。使用这些写入缓存可以显着提高性能,特别是对于日志密集型应用程序(如邮件服务器)。
邮件服务器往往是带日志文件系统的最坏情况,因为它们需要确保每个文件操作都完成。它们使用 fsync 或其他一些技巧来防止在崩溃后丢失消息,这迫使文件系统在事务仍然很小时关闭事务。
邮件服务器需要一种将新文件快速提交到磁盘的方法,而数据日志记录可以对此有所帮助。在 fsync 调用期间,您记录将文件添加到树中所需的数据块和元数据,从而产生一个大的顺序写入。如果写入的文件是瞬态假脱机文件,则可能永远不会写回主驱动器。与快速专用日志记录设备结合使用,数据日志记录可以带来很大的性能提升。
到目前为止,ReiserFS 2.4 代码不支持数据日志记录。我们的 2.2 内核版本中有数据日志记录代码,但需要将其适应于 2.4 页面缓存。
新的 Linux 文件系统的最终结果应该非常有趣。管理员将能够为其应用程序选择最佳产品,程序员将能够将其决策与替代方案进行比较。随着社区从每个文件系统中挑选和选择最佳功能,整个 Linux 将受益。
Chris Mason (mason@suse.com) 在开始为 ReiserFS 项目做出贡献之前是一名系统管理员。他现在在纽约州罗切斯特的家中为 SuSE 全职工作。