Debian 包管理器可以dpkg
通过使用 AIO fsync() 操作之一而不是sync_file_range() + fsync() 来获得显着的性能改进吗?
[提议的] fsync2() API 本质上与现有的 AIO_FSYNC/AIO_FDSYNC API 相同,只是它是同步的,而这是应用程序想要避免的。
我反对[使用] AIO_FSYNC 的唯一论点是“该实现只是一个工作队列”,这在很大程度上是无意义的,因为它独立于文件系统实现,但允许自动内核端并行化所有发出的 fsync 操作。这允许文件系统在完成并发 fsync 操作时自动优化掉不必要的日志写入 - 当用户应用程序从大量进程/线程同时运行 fsync() 时,XFS、ext4 等已经这样做了......
这个简单的实现允许在 XFS 上执行简单的“使用 aio fsync 解压”工作负载(即“批量写入许多 4kB 文件和 aio_fsync(),在分派新批次之前退出已完成的 fsync()”)工作负载大约 2000 个文件/秒(同步写入 IO 延迟范围)到超过 40,000 个文件/秒(后端存储上的写入 IOPS)。
--戴夫·钦纳
示例工作负载与apt-get install
或相似dpkg -i
(部分取决于已安装软件包中文件的大小:-)。 dpkg
在将它们重命名到位之前,必须有效地 fsync() 所有解压的文件。
dpkg
已根据 Ted T'so 的建议进行了优化。优化是在某些点添加对sync_file_range() 的调用。这个系统调用做了不是提供与 fsync() 相同的保证。请阅读文档同步文件范围()并注意突出的警告:-)。
这些操作都不会写出文件的元数据。因此,除非应用程序严格执行已实例化磁盘块的覆盖,否则无法保证数据在崩溃后可用。
dpkg
在写入每个文件后立即触发数据写回,使用SYNC_FILE_RANGE_WRITE
.它首先写入包的所有文件。然后第二次遍历文件,等待使用SYNC_FILE_RANGE_WAIT_BEFORE
、调用 的数据写回fsync()
,最后将文件重命名到位。
查看提交:
- 默认情况下禁用同步sync(2)
- 添加新的 --force-unsafe-io 以禁用解包时的安全 I/O 操作
- 在 Linux 上尽快启动解压文件的写回
- 在 Linux 上,在 fsync 之前完成写回
我的假设是,并行化 fsync() 操作可以通过允许更有效的批处理来提高性能元数据写入,特别是批处理相关的屏障/磁盘缓存刷新,这是确保磁盘上的元数据始终一致所必需的。
编辑:看来我的假设太简单了,至少在使用 ext4 文件系统时:
第二个系列的sync_file_range()调用以及操作
SYNC_FILE_RANGE_WAIT_BEFORE
将阻塞,直到先前启动的写回完成。这样基本就保证了延迟分配已经得到解决;也就是说,数据块已被分配和写入,并且索引节点已更新(在内存中),但不一定被推送到磁盘。[fsync()] 调用实际上会强制将 inode 写入磁盘。 对于 ext4 文件系统,第一个 [fsync()] 实际上会将所有 inode 推送到磁盘,并且所有后续的 [fsync()] 调用实际上都是空操作(假设文件“a”、“b”和“c”都位于同一文件系统上)。但这意味着它将(重量级)jbd2 提交的数量降至最低。
它使用 Linux 特定的系统调用 ---sync_file_range() --- 但结果应该是所有文件系统的全面更快的性能。因此,我不认为这是针对 ext4 的 hack,尽管它可能确实使 ext4 的速度比任何其他文件系统更快。
--特德·索
其他一些文件系统可能会受益于使用 AIO fsync() 操作。
bcachefs
(正在开发中)声称比 ext4 更好地隔离不同文件之间的 IO。所以测试可能特别有趣。
听起来好像 ext4 对于纯 AIO fsync() 模式可能没有那么优化(我猜其他文件系统也可能有相同的约束)。如果是这样,我想可以首先执行所有相同的sync_file_range()调用,然后开始所有AIO fsync()操作作为第二轮,并通过将所有文件重命名为fsync()来完成操作完成。
老的:
这种调查的第一步应该是测量:-)。
可以使用 禁用 fsync() 部分echo "force-unsafe-io" > /etc/dpkg/dpkg.cfg.d/force-unsafe-io
。
到目前为止,我尝试在 Debian 9 容器中apt-get install
运行。strace -f -wc
例如,使用“unsafe io”安装aptitude
包,只有 495 个同步 fsync() 调用。正常安装时aptitude
,有 1011 个 fsync() 调用。 “unsafe io”还禁用了该SYNC_FILE_RANGE_WAIT_BEFORE
调用,将sync_file_range()调用的数量从1036减少到518。
然而,尚不清楚这是否会减少平均时间。如果确实如此,那么它似乎也只不过是运行之间的随机变化而已。到目前为止,我在机械 HDD 上的 ext4 和 XFS 上进行了测试。
apt-get
表示 518 个解压文件的总大小为 21.7 MB(请参见下面的输出)。
关于 495 fsync() 调用,即使在请求“不安全 io”时,该调用仍然存在:
在 ext4 上,strace 输出显示剩余的 fsync() 调用所花费的时间约为 11 秒。在XFS上,相应的数字约为7秒。在所有情况下,这是安装所花费的大部分时间aptitude
。
因此,即使“不安全的 io”对安装带来了小小的改进aptitude
,似乎您也需要/var
安装在比系统其他部分更快(延迟更低)的设备上,然后差异才会真正明显。但我对优化这个利基案例不感兴趣。
运行strace -f -y -e trace=fsync,rename
显示,对于剩余的 fsync() 调用,其中 2 个是 on /etc/ld.so.cache~
,其中 493 个是对/var/lib/dpkg/
包数据库内的文件。
318 的 fsync() 调用位于/var/lib/dpkg/updates/
.这些是 dpkg 数据库的增量/var/lib/dpkg/status
。在 dpkg 运行结束时,增量将汇总到主数据库中(“检查点”)。
The following NEW packages will be installed:
aptitude aptitude-common libboost-filesystem1.62.0 libboost-iostreams1.62.0 libboost-system1.62.0 libcgi-fast-perl libcgi-pm-perl
libclass-accessor-perl libcwidget3v5 libencode-locale-perl libfcgi-perl libhtml-parser-perl libhtml-tagset-perl libhttp-date-perl
libhttp-message-perl libio-html-perl libio-string-perl liblwp-mediatypes-perl libparse-debianchangelog-perl libsigc++-2.0-0v5 libsqlite3-0
libsub-name-perl libtimedate-perl liburi-perl libxapian30
0 upgraded, 25 newly installed, 0 to remove and 0 not upgraded.
Need to get 0 B/6000 kB of archives.
After this operation, 21.7 MB of additional disk space will be used.
答案1
这个问题表明这对 ext4 或 XFS 没有帮助。
我还测试了安装一个更大的包 ( linux-image-4.9.0-9-amd64
)。不管怎样,它似乎仍然需要同样的时间--force-unsafe-io
。
外部2
在 ext2 上,--force-unsafe-io
安装时间linux-image
从 50 秒减少到 13 秒。
我运行测试的内核是5.0.17-200.fc29.x86_64
,它使用CONFIG_EXT4_USE_FOR_EXT2
.
我使用用户空间 aio_fsync() 实现测试了 ext2。然而,最好的改进并不依赖于使用 AIO fsync()。
我的进步实际上是由于副作用。我已将 dpkg 更改为首先执行所有 fsync() 操作,然后执行所有 rename() 操作。而未打补丁的 dpkg 在每次 fsync() 之后调用 rename()。我使用的 AIO 队列深度高达 256。队列深度为 1 的 AIO fsync() 明显慢于同步 fsync() - 似乎存在一些开销。最好的改进还需要SYNC_FILE_RANGE_WRITE
首先完成所有原始操作。改进版本安装时间linux-image
约为 18 秒。
这个操作顺序实际上是 Ted T'so 最初建议的:-D。发生的情况是CONFIG_EXT4_USE_FOR_EXT2
, fsync() 也有助于同步父目录。您希望首先执行所有文件名操作,这样就可以避免每个目录进行多次磁盘更新。 CONFIG_EXT2
我认为这对于旧的实现或普通的文件系统不会发生ext4
。
[...] 显然这也包括 ext2 默认模式。 [...]
https://elixir.bootlin.com/linux/v5.0.17/source/fs/ext4/fsync.c#L38
* If we're not journaling and this is a just-created file, we have to
* sync our parent directory (if it was freshly created) since
* otherwise it will only be written by writeback, leaving a huge
* window during which a crash may lose the file. This may apply for
* the parent directory's parent as well, and so on recursively, if
* they are also freshly created.
和以前一样,用sync() 替换fsync() 阶段似乎提供了令人不安的良好性能,匹配--force-unsafe-io
:-)。如果您可以使用它们,sync() 或syncfs() 似乎非常好。
BTFS
当我开始在 btrfs 上测试 aio_fsync() 时,我发现由于最近的数据完整性修复,fsync() 操作可能会导致文件的 rename() 阻塞。我决定我对 btrfs 不感兴趣。