Pgfs:PostGres 文件系统

作者:Brian Bartholomew

PostGres 文件系统是一个 Linux NFS 服务器,它将软件版本呈现为 NFS 文件系统中的不同文件树。每个版本都与其他所有版本完全不同,并且可以独立修改,而无需考虑之前的或之后的版本。每个版本都保留了它在普通文件系统上所具有的所有属性,例如文件所有权、权限、二进制文件内容、跨目录硬链接和非文件(如设备和符号链接)。效果就像每个软件版本都有自己的独立目录一样,只是使用的磁盘空间要少得多。

Pgfs 的设计动机

举个例子,假设一年前,您选择了您最喜欢的 Linux 发行版,并将其安装在您的新计算机上。该发行版大约有 15,000 个文件,占用磁盘空间约 200MB。在这一年中,您对您的软件进行了大量的修改,现在它与原始发行版已经大相径庭。这些修改是逐步完成的,其中一些用全新的二进制文件替换了原始的二进制文件,例如升级 sendmail(8) 或 ftpd(8)。现在您希望将您的机器与原始发行版进行比较,检查您所做的更改,并将它们应用到一个新的 Linux 发行版中。

您如何记录您所做的更改?您可以每次修改后都保存一份完整的发行版副本——这样做将消耗 200MB * 100 次修改 = 20 GB 的磁盘空间。即使使用一对 9 GB 的硬盘驱动器,这也非常昂贵。但是,您注意到大多数修改只更改了少量文件——每次修改可能总共只有半 MB。仅存储更改的文件将使用 200MB + (100 次修改 * 0.5MB/次修改) = 250MB 的磁盘空间——这要好得多。什么应用程序会只为您存储差异呢?您可以使用 CVS,但 CVS 实际上并不适合。

现在假设您是一位系统管理员,并且多年来每天都面临着这个版本控制问题,但一直没有找到令人满意的解决方案。因此,由于您也是一位开发人员,您决定构建一个应用程序来存储类似的文件树,以利用您发现的压缩机会。从根本上说,这个应用程序需要接收文件树并再次吐出它们,并且它必须比保留每个树的完整副本使用更少的磁盘空间。它应该接受单个文件或批量文件。您不应该为了做一个更改而不得不提取并重新提交整个文件树。

您将如何实现这个应用程序?首先决定需要什么数据结构,以及需要什么例程来操作这些结构。让我们从文件开始。文件由两个部分组成——一个 stat(2) 结构和一个大的二进制数据块。假设您将二进制数据存储在一个文件中,并用一个数字命名这个文件。然后,用一个数字命名 stat 结构,并将固定长度的结构存储在磁盘上的数组中。可以使用字段拆分例程将结构分解开,并使用记录创建例程组装起来。接下来,需要一个结构来表示您的软件的不同版本。您的操作系统的每个版本都由一个文件树组成。您将代表一个特定软件版本的文件树称为“版本集”或“verset”并对其进行编号。

接下来需要的是一些例程,用于在磁盘上搜索 stat 数组以查找特定结构、添加结构和删除结构。由于您将对结构进行随机访问,请将它们存储在 dbm(数据库管理)文件中,并使用 dbm 访问例程,而不是编写自己的例程。Dbm 还为您提供了处理 stat 结构编号索引的例程,以便加快您的访问速度。您将需要编写维护例程来复制 dbm 文件,以及将字段从一个 dbm 文件复制到另一个具有不同结构布局的 dbm 文件。

向您的数组中添加一个新的 stat 结构可能需要修改您正在添加的结构以外的其他结构中的字段;例如,当您向现有文件树添加一个文件时。如果可以收集一堆这样的修改并一次性完成它们,或者在发现问题时不执行任何操作,那么您的编程任务将简单得多。一次性完成所有或不执行任何复杂修改的想法被称为“事务”。

要使用您的应用程序,您需要让它接受的命令。一些命令可能是“添加整个文件树”、“添加单个文件”和“用这些字节替换文件的某些字节”。NFS 团队已经弄清楚了您需要的最少文件操作集。(请参阅侧边栏 1。)现在决定如何为每个文件操作修改 stat(2) 结构,并编写伪代码来修改 stat(2) 数组。在设计 NFS 命令的语义时,开始考虑向您的应用程序发送 NFS 命令,使其成为 NFS 服务器。

接下来编写您的应用程序。考虑使用 SQL 数据库怎么样?数据库将应用程序数据结构与其在磁盘上的表示形式分离,从而为您带来以下优势

  1. 结构可以使用任意字段定义并存储在表中。

  2. 可以使用全套例程来添加、删除和修改结构,以及用于快速查找结构的索引。

  3. 可以使用一个不错的命令语言在应用程序演变时在结构格式之间进行转换。

  4. 数据库中的例程旨在操作无法一次全部装入内存的数据块,因此您的应用程序可以无问题地增长。

要添加一个名为“cokecans”的字段来统计创建每个文件所需的 Coke 罐数,只需添加它即可。您可以使用几行 SQL 将现有数据传输到带有 cokecans 字段的新表中。将其与 C 语言编码进行比较,在 C 语言中,需要编写一堆自定义的二进制格式转换程序。

然后,找到用户级 NFS 服务器的框架并将其移植到 Linux(请参阅侧边栏 2),并将 NFS 命令的来源连接到您的应用程序的命令输入。现在您有了一个 NFS 服务器,它可以呈现文件树,但会压缩树之间的相似之处。由于您的应用程序可以像任何文件系统一样使用,因此您不必构建任何专门的程序来操作版本——您可以使用 grep 搜索文件,并使用 diff 比较文件树。

为了控制您的应用程序,创建一些它可以视为特殊的伪魔法文件名,例如 procfs。写入这些文件的行是发送给您的应用程序的命令。现在,您的应用程序可以使用 shell 中的 echo(1) 命令而不是某些晦涩的套接字协议来控制。

上面的描述并不完全是我编写 Pgfs 的方式,但它确实概述了设计动机。在我尝试在 CVS 下存储 BSDI 发行版的副本并在实践中失败后,我开始编写一个基于数据库实现的 NFS 服务器。我的第一个版本是用 Perl5 使用 PostGres 客户端库编写的,我以空格分隔的文本字符串形式键入 NFS 命令。我用 C 语言重新编码以拾取 NFS RPC。我的第一个数据库设计模式使用一个表来存储“名称”——保存文件名和符号链接,另一个表来存储“inodes”——保存 stat(2) 结构的其余部分和指向文件内容的指针。但是,我不喜欢连接操作(即,将来自两个表中具有相同键的行匹配起来),我也不想在数据库或应用程序代码中实现连接。

Pgfs 如何为用户工作

让我们以您最喜欢的 Linux 发行版为例,说明需要版本控制的文件树。首先,将原始操作系统从 CD-ROM 复制到 Pgfs

cp -va /cdrom /pgfs/1/1

让我们检查一下目标路径名。pgfs 部分是 Pgfs 的挂载点。第一个 1 是“模块”。将独立演进的软件存储在不同的模块中可以节省磁盘空间。第二个 1 是版本集。我们有一个全新的空 Pgfs,所以我们将写入版本集 1。复制完成后,使用 ls 查看 pgfs 目录中的内容

ls -l /pgfs/1/1/bin/su /pgfs/1/1/dev/cua0
ls 的输出如下所示
-rwsr-xr-x  1 root   bin     9853 Aug 14 1995 /pgfs/1/1/bin/su
crw-rw----  1 root   uucp    5, 64 Jul 17 1994 /pgfs/1/1/dev/cua0
请注意 su(1) 上的 suid 位和 cua0 模式上的前导 c。Pgfs 存储属性和非文件,就像任何其他文件系统一样。如果您在挂载 Pgfs 时选择了接受 suid 位的挂载选项,则此 su 副本将使您成为 root 用户。接下来,将版本集 1 复制到新的版本集,以便可以修改新的版本集,而不会更改旧版本集中的文件
echo "cpverset 1" > /pgfs/ctl
在您的新版本集中,您安装了较新版本的 sendmail
cp /tmp/sendmail /pgfs/1/2/usr/sbin/sendmail
chown root.bin /pgfs/1/2/usr/sbin/sendmail
chmod 6555 /pgfs/1/2/usr/sbin/sendmail
现在您有了两个不同的版本集,您可以比较它们的内容。您可以使用 shell 通配符或其他文件名扩展来访问多个版本集。要查找有哪些版本集,请执行 ls /pgfs/1
strings - /pgfs/1/1/usr/sbin/sendmail | \
        grep version.c
@(#)version.c  8.6.12.1 (Berkeley) 3/28/95
strings - /pgfs/1/{1,2}/usr/sbin/sendmail | \
        grep version.c
@(#)version.c  8.6.12.1 (Berkeley) 3/28/95
@(#)version.c  8.8.2.1 (Berkeley) 10/18/96
strings - /pgfs/1/*/usr/sbin/sendmail | \
        grep version.c
@(#)version.c  8.6.12.1 (Berkeley) 3/28/95
@(#)version.c  8.8.2.1 (Berkeley) 10/18/96
可视化软件演进

大多数版本控制软件包都侧重于单个文件的单独修改历史,这正是它们的工具所显示的内容。我认为,被称为“客户发布 1.0”的文件集合的概念比每个文件如何到达该集合的概念更重要。

假设一位新员工遇到了包含 200 个版本集的 Pgfs。她首先想知道的事情之一是每个版本集代表什么以及它们如何相互关联。为什么这里有这个版本集?这个版本集来自哪里?哪些版本集代表一致的软件发布?以文件为基本单位的工具会要求她此时比较文件历史记录。糟糕的是,当她比较 /usr 的两个版本时,无法连贯地显示 40,000 个单独的文件历史树。围绕版本集规模构建的工具效果更好,因为版本集比每个版本集的文件少得多。

我想要一个程序,它可以读取整个 Pgfs 数据库,并根据共享文件和唯一文件的数量,绘制每个版本集彼此之间的关系。针对 Pgfs 运行,该程序显示版本集 1 和 2 有 19,998 个相同的文件和 2 个不同的文件,不同的文件是 /usr/foo 和 /usr/bar。该程序绘制了 200 个不同版本的 /usr 的框,连接线颜色和宽度各不相同,具体取决于共享文件的百分比以及较旧和较新文件的百分比。如果我用语言告诉员工 /usr 的两个副本“几乎相同”、“差别很大”或“来自两个不同的操作系统”,她就会很好地理解我所指的大概数字。在我的程序中,我希望那些分类框从比较版本集的图片中在视觉上显而易见。

访问透明性

对于大多数系统管理目的,我并不关心文件是如何或为什么更改的。如果我对内核应用供应商补丁,我所关心的只是在应用补丁前后取回内核树。我不想为了将其推入 Pgfs 而将补丁脚本反向工程为文件添加、删除、重命名和修改。我不应该需要通知版本控制系统要使用签入/签出命令查看或修改哪些文件。我只想要一个 NFS 文件系统,当我离开时,目录中的任何内容都会存储在版本集中以供下次使用。由于我不打算向 Pgfs 提供关于我正在做什么的提示,因此每个操作都需要是可能的。因此,每个版本集都必须完全独立于所有其他版本集。我不想被迫从分支结构中没有循环的先前文件中演变我的文件,或者由于缺少目录版本控制而保持我的文件名在版本之间不变,这仅仅是 CVS 的两个众所周知的限制。

Pgfs 架构

以下是您可以下载的实际 Pgfs 程序的描述。Pgfs 是一个普通的用户级程序,可以读取和写入普通的 TCP 流和 UDP 数据包。由于它是一个不需要任何特权的普通程序,因此它可以运行在任何 Linux 系统上。它不使用任何突破性的系统调用功能,因此不需要进行内核修改。TCP 流数据包由 PostGres 客户端库生成,因此 Pgfs 可以使用 SQL 与 PostGres 数据库进行交互。UDP 数据包按照 NFS 协议的约定进行格式化。所有这些都意味着像 Linux 内核这样的 NFS 客户端可以选择将 NFS 数据包发送到 Pgfs,并且可以像 Pgfs 是任何其他类型的 NFS 服务器一样挂载文件系统。AMD 自动挂载器是另一个充当 NFS 服务器的用户级程序的示例。AMD 响应触发自动挂载器响应的目录浏览 NFS 操作,而 Pgfs 响应所有 NFS 操作。

本质上,Pgfs 是一个 NFS <-> SQL 转换器。当 NFS 请求传入时,C 代码会提交 SQL 以获取请求中提及的目录和文件的 stat(2) 结构,并在执行过程中进行错误和权限检查。首先,它将请求与它收到的关于文件的数据进行比较,强制执行条件,例如是否可以使用 rmdir 删除文件。

如果请求有效并且权限允许,则 C 代码会找到所有必须更改的 stat(2) 结构,例如当前文件、当前目录、上级目录以及共享文件 inode 的硬链接。然后,这些修改通过 SQL 在数据库中进行。这些修改包括副作用,例如更新您可能通常不会想到的访问时间。

每个 NFS 操作都在数据库事务中处理。如果发生“预期”错误,可能是由 NFS 客户端上的错误用户输入引起的,例如键入 rmdir 来删除文件,则会返回 NFS 错误。如果发生“意外”错误,例如数据库未响应或找不到文件句柄,则事务将以不会用错误数据污染文件系统的方式中止。

Pgfs “手动”完成在“真实”文件系统中发生的所有事情。它使用 PostGres 作为存储设备,它通过 inode 编号、路径名和版本集编号进行访问。例如,nfs_getattr NFS 操作的工作方式类似于 lstat(2) 系统调用。getattr 接受文件标识符,在本例中是 NFS 句柄而不是路径名,并返回 stat(2) 结构的所有字段。当 Pgfs 处理 nfs_getattr 操作时,会发生以下事情

  1. NFS 数据包被分解为操作和参数。

  2. NFS 操作计数器递增。

  3. NFS 句柄被分解为字段。

  4. nfs_getattr 参数执行边界检查。

  5. 获取句柄的 stat(2) 信息,例如,select * from tree where handle = 20934

  6. 检查权限。

  7. 文件访问时间已更新,例如,update tree set atime = 843357663 where inode = 8923

  8. 构建 NFS 答复。

  9. 答复已发送到 NFS 客户端

存储模式

保存所有 stat(2) 结构的单个表具有如表 1所示定义的字段。

Inode 编号在整个数据库中是唯一的,即使对于不同版本集中相同的的文件也是如此。每个版本集中的每个文件都有一行数据库行。每个目录有三行;一行用于其来自上级目录的名称,一行用于 .(点),一行用于 ..(点点),来自下级目录。

从哲学上讲,压缩类似的文件树是程序后端的业务——它不应该对用户可见。在 Pgfs 中,每个文件字节集合都包含在一个 Unix 文件中,在所有继承文件名的版本集之间共享写时复制。每当修改共享文件时,都会为该版本集创建一个私有副本。这符合 Pgfs 的系统管理方向,其中文件将很大且是二进制的,并且将完全替换,并且新旧二进制文件不会足够相似以使差异变小。这与源代码不同,在源代码中,相同的文件会一遍又一遍地增量修改,并且差异很小。使用保留完整文件策略,在多个版本集中对文件执行 grep 不会比停留在单个版本集中慢。当压缩算法将中间版本解压缩到临时区域时,不会有很大的延迟。

观众参与时间

到目前为止,我只用整数来标识版本集,但整数很无聊。由于 Pgfs 构建在数据库之上,因此一切皆有可能。您在其他项目中为版本集提出了哪些命名方案来支持配置管理工作?所有名称/标识符都在一个平面空间中吗?它们是分层的吗,它们是否从它们所在的位置继承属性?请告诉我,我对每个版本集有数万甚至数十万个文件的大型项目的经验特别感兴趣。

到目前为止,我提供的创建新版本集的唯一方法是向 Pgfs 发送一个特殊命令,将一个版本集复制到一个新的版本集。但是,版本集只是数据库行的集合,因此可以通过 SQL 程序制造,也许是一个代表跨 14 个版本集的半自动多向合并的程序。从这里获取基本文件树,从那里获取此补丁,从那边获取另一个补丁,并将所有守护程序的所有者设置为 fred,非常感谢。控制此过程的界面会是什么样子?您会有一个交互式文件浏览器购物车之类的东西,您可以在其中从您找到它们的任何地方拉取零碎的东西吗?此过程将如何解决冲突?

在 Pgfs 发行版的 BUGS 和 TODO 文件中,还有更多有趣的开放性问题,这些问题都涉及界面和实现。我鼓励您选择几个您感兴趣的问题,并在 host-gen 邮件列表中讨论它们。

结束语

我想给您的最重要的信息是,文件系统黑客技术不再仅仅是巫师的专利。NFS 提供了一个可移植的文件系统接口,它消除了通常的内核黑客技术要求。NFS 语义不是很好,但它们对于许多事情来说是足够的。任何人都可以使用 Pgfs 的 NFS 解码部分作为框架,并编写一个具有他们梦想的任何语义的文件系统。平凡的可能性是 ftp 浏览或 web 浏览文件系统。更有趣的领域涉及具有分布式物理冗余的广域、容错文件系统。更高级别协议的任务是将故障转化为不良性能。与其手动挑选 Linux ftp 站点列表,您是否更愿意使用一个文件系统,该文件系统可以自动从当前性能最佳的站点获取块?您是否愿意为您的收藏存档站点的子树创建一个新的本地存储区域,而您的用户只需要知道……访问速度更快了?这些想法引发了许多有趣的身份验证和信任问题,其中许多问题可以通过 web-of-trust 的 PGP 模型来解决。现在,前进并编码吧。

侧边栏 3

Brian Bartholomew 正在编写 Pgfs,作为 Host Factory 自动化主机维护系统的组件。Host Factory 将主机集成到 Borg 集体中。Working Version 进行大型站点工具制作,有关 Host Factory 的更多信息,请访问 http://www.wv.com。可以通过 bb@wv.com 联系到 Brian。

加载 Disqus 评论