概述
这个问题的结构如下:
我首先给出一些背景知识,说明为什么我对这个主题感兴趣,以及它将如何解决我正在处理的问题。然后,我询问有关文件系统缓存的实际独立问题,因此,如果您对动机(某些 C++ 项目构建设置)不感兴趣,请跳过第一部分。
最初的问题:链接共享库
我正在寻找一种方法来加快项目的构建时间。设置如下:目录(我们称之为workarea
)位于 NFS 共享中。它最初只包含源代码和 makefile。然后,构建过程首先在 中创建静态库workarea/lib
,然后workarea/dll
使用 中的静态库在 中创建共享库workarea/lib
。在创建共享库的过程中,这些库不仅会被写入,还会再次读取,例如nm
在链接时验证没有丢失任何符号。并行使用许多作业(例如 make -j 20 或 make -j 40),构建时间很快就会被链接时间所支配。在这种情况下,链接性能受到文件系统性能的限制。例如,在 NFS 共享中并行链接 20 个作业大约需要 35 秒,但在 RAM 驱动器中只需要 5 秒。请注意,使用 rsync 复制dll
回 NFS 共享还需要 6 秒,因此在 RAM 驱动器中工作并随后同步到 NFS 比直接在 NFS 共享中工作要快得多。我正在寻找一种无需在 NFS 共享和 RAM 驱动器之间显式复制/链接文件即可实现快速性能的方法。
请注意,我们的 NFS 共享已经使用了缓存,但该缓存只能缓存读访问。
AFAIK,NFS 要求任何 NFS 客户端在 NFS 服务器确认写入完成之前都不能确认写入,因此客户端无法使用本地写入缓冲区,并且写入吞吐量(即使是峰值)也受到网络速度的限制。在我们的设置中,这有效地将组合写入吞吐量限制为大约 80MB/s。
然而,由于使用了读取缓存,读取性能要好得多。如果我在 NFS 中链接(创建 的内容dll
)并作为 RAM 驱动器的符号链接,性能仍然不错 - 大约 5 秒。请注意,构建过程需要驻留在 NFS 共享中来完成:需要位于共享(或任何持久挂载)中以允许快速增量构建,并且需要位于 NFS 中以便由启动作业的计算机进行访问使用这些dll。 因此,我想对下面的问题应用一个解决方案,也许也可以(后者是为了缩短编译时间)。下面快速设置时间的要求是由于需要执行快速增量构建,仅在需要时复制数据。workarea/lib
workarea/dll
workarea/*
lib
dll
workarea/dll
workarea/lib
更新
我应该对构建设置更具体一些。以下是更多详细信息: 编译单元被编译成临时目录(/tmp 中)中的 .o 文件。然后将它们合并到lib
using中的静态库中ar
。完整的构建过程是增量的:
- 仅当编译单元本身(.C 文件)或包含的标头发生更改(使用包含在 中的编译器生成的依赖文件
make
)时,才会重新编译编译单元。 - 仅当重新编译静态库的编译单元之一时,静态库才会更新。
- 仅当共享库的静态库之一发生更改时,才会重新链接共享库。仅当共享库所依赖的共享库提供的符号发生更改或者共享库本身已更新时,才会重新检查共享库的符号。
尽管如此,完整或接近完整的重建经常是必要的,因为可以使用多个编译器( gcc
、clang
)、编译器版本、编译模式(release
、debug
)、C++标准(C++97
、C++11
)和附加修改(例如)。libubsan
所有组合都有效地使用不同的lib
和dll
目录,因此可以在设置之间切换并基于该设置的最后一次构建增量构建。此外,对于增量构建,通常只需重新编译几个文件,花费很少的时间,但触发(可能很大)共享库的重新链接,花费更长的时间。
更新2
与此同时,我了解了nocto
NFS 挂载选项,这显然可以解决我在除 Linux 之外的所有 NFS 实现上的问题,因为 Linux 始终会刷新写入缓冲区close()
,即使使用nocto
.我们已经尝试了其他一些事情:例如,我们可以使用另一个async
启用的本地 NFS 服务器作为写入缓冲区并导出主 NFS 挂载,但不幸的是,在这种情况下 NFS 服务器本身没有写入缓冲。似乎这async
只是意味着服务器不会强制其底层文件系统刷新到稳定存储,并且在底层文件系统使用写入缓冲区的情况下隐式使用写入缓冲区(因为文件系统显然就是这种情况)在物理驱动器上)。
我们甚至考虑过在使用 挂载主 NFS 共享的同一机器上使用非 Linux 虚拟机的选项nocto
,提供写入缓冲区,并通过另一个 NFS 服务器提供此缓冲挂载,但尚未对其进行测试,并且希望避免这样的解决方案。
我们还发现了几个FUSE
基于 的文件系统包装器用作缓存,但这些都没有实现写缓冲。
缓存和缓冲目录
考虑某个目录(我们称之为 )orig
,它驻留在慢速文件系统中,例如 NFS 共享。对于很短的时间跨度(例如几秒或几分钟,但这应该不重要),我想创建一个orig
使用目录的完全缓存和缓冲视图cache
,该目录驻留在快速文件系统中,例如本地硬盘驱动器甚至是内存驱动器。缓存应该可以通过挂载等方式进行访问,cached_view
并且不需要 root 权限。我假设在缓存的生命周期内,没有直接的读取或写入访问orig
(当然除了缓存本身之外)。通过完全缓存和缓冲,我的意思是:
- 通过将查询转发到 的文件系统
orig
、缓存该结果并从那时起使用它来回答读取查询,并且 - 写查询直接写入
cache
并完成后确认,即高速缓存也是写缓冲区。当close()
对写入文件调用时,甚至应该发生这种情况。然后,在后台,写入被转发(可能使用队列)到orig
.cache
当然, 对写入数据的读取查询是使用 中的数据来回答的。
此外,我需要:
- 缓存提供了关闭缓存的功能,该功能会刷新所有对
orig
.刷新的运行时间应该只取决于写入文件的大小,而不是所有文件。之后,人们就可以安全地orig
再次访问。 - 设置时间很快,例如,缓存的初始化可能仅取决于 中的文件数量
orig
,而不取决于 中的文件大小,因此不能选择orig
复制orig
到一次。cache
最后,我也希望有一个解决方案,不使用另一个文件系统作为缓存,而只是在主内存中缓存(服务器有足够的 RAM)。请注意,使用 NFS 等内置缓存不是一种选择,因为据我所知 NFS 不允许写入缓冲区(参见第一部分)。
orig
在我的设置中,我可以通过符号链接to的内容来模拟稍微更糟糕的行为cache
,然后使用cache
(因为所有写入操作实际上都用新文件替换文件,在这种情况下,符号链接将被更新版本替换),然后 rsync 修改后的文件文件到orig
之后。这并不完全满足上述要求,例如,读取不只完成一次并且文件被符号链接替换,这当然对某些应用程序产生影响。
我认为这不是解决这个问题的正确方法(即使在我更简单的设置中),也许有人知道一个更干净(更快!)的解决方案。
答案1
哇,很惊讶还没有人回答“overlayfs”。
其实我有两个建议。第一种是使用overlayfs,这基本上正是您所描述的,但有一个警告。 Overlayfs(自 Linux 3.18 左右起成为标准)允许您从两个虚拟合并的目录树中读取数据,同时仅写入其中之一。您要做的就是采用快速存储(如 tmpfs)并将其覆盖到 NFS 卷上,然后在两者的覆盖合并中执行编译。完成后,NFS 上的任何文件的写入次数为零,并且另一个文件系统保存着您的所有更改。如果您想保留更改,只需将它们同步回 NFS 即可。您甚至可以排除您不关心的文件,或者只是从结果中手动挑选一些文件。
你可以在我的一个小项目中看到一个相对简单的overlayfs示例:https://github.com/nrdvana/squash-portage/blob/master/squash-portage.sh 该脚本还展示了如何使用 UnionFS,以防您使用的是没有 overlayfs 的旧内核。
就我而言,Gentoo 使用 rsync 命令更新其软件库需要非常长的时间,因为它有数百万次微小的磁盘写入。我使用overlayfs 将所有更改写入tmpfs,然后使用mksquashfs 构建树的压缩映像。然后我扔掉 tmpfs 并在其位置安装压缩映像。
我的第二个建议是“树外”构建。这个想法是,您将源代码和 makefile 放在一个树中,然后告诉 automake 在镜像第一个树的单独树中生成所有中间文件。
- https://stackoverflow.com/questions/1311231/store-gnu-make- generated-files-elsewhere
- http://voices.canonical.com/jussi.pakkanen/2013/04/16/why-you-should-consider-using-separate-build-directories/
- https://stackoverflow.com/questions/16443854/out-of-tree-build-makefile-without-automake
如果幸运的话,您的构建工具(automake 或诸如此类)已经可以做到这一点。如果你不幸运,你可能不得不花一些时间来修改你的 makefile。