我正在将 4 * 4KB 块写入文件。如果我使用fallocate()
9 个块来预分配文件,而不是只预分配 4 个块,则速度始终会慢 50% 左右。为什么?
预分配 8 块和 9 块之间似乎有一个分界点。我还想知道为什么第一块和第二块写入始终较慢。
这个测试是从我正在使用的一些文件复制代码中总结出来的。灵感来自这个问题关于dd
,我正在使用O_DSYNC
写入,以便可以测量磁盘写入的实际进度。 (完整的想法是开始复制一个小块来测量最小延迟,然后自适应地增加块大小以提高吞吐量)。
我正在一台带有旋转硬盘驱动器的笔记本电脑上测试 Fedora 28。它是从早期的 Fedora 升级而来的,因此文件系统并不是全新的。我不认为我一直在摆弄文件系统默认值。
- 内核:4.17.19-200.fc28.x86_64
- 文件系统:ext4,LVM 上。
- 挂载选项:rw、relatime、seclabel
- 字段来自
tune2fs -l
- 默认挂载选项:user_xattr acl
- 文件系统功能: has_journal ext_attr resize_inode dir_index 文件类型 need_recovery 范围 64 位 flex_bg稀疏_超大文件巨大_文件 dir_nlink extra_isize
- 文件系统标志:signed_directory_hash
- 块大小:4096
- 空闲块:7866091
时间来自strace -s3 -T test-program.py
:
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000048>
write(3, "\0\0\0"..., 4096) = 4096 <0.036378>
write(3, "\0\0\0"..., 4096) = 4096 <0.033380>
write(3, "\0\0\0"..., 4096) = 4096 <0.033359>
write(3, "\0\0\0"..., 4096) = 4096 <0.033399>
close(3) = 0 <0.000033>
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000110>
fallocate(3, 0, 0, 16384) = 0 <0.016467>
fsync(3) = 0 <0.000201>
write(3, "\0\0\0"..., 4096) = 4096 <0.033062>
write(3, "\0\0\0"..., 4096) = 4096 <0.013806>
write(3, "\0\0\0"..., 4096) = 4096 <0.008324>
write(3, "\0\0\0"..., 4096) = 4096 <0.008346>
close(3) = 0 <0.000025>
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000070>
fallocate(3, 0, 0, 32768) = 0 <0.019096>
fsync(3) = 0 <0.000311>
write(3, "\0\0\0"..., 4096) = 4096 <0.032882>
write(3, "\0\0\0"..., 4096) = 4096 <0.010824>
write(3, "\0\0\0"..., 4096) = 4096 <0.008188>
write(3, "\0\0\0"..., 4096) = 4096 <0.008266>
close(3) = 0 <0.000012>
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000050>
fallocate(3, 0, 0, 36864) = 0 <0.022417>
fsync(3) = 0 <0.000260>
write(3, "\0\0\0"..., 4096) = 4096 <0.032953>
write(3, "\0\0\0"..., 4096) = 4096 <0.033265>
write(3, "\0\0\0"..., 4096) = 4096 <0.033317>
write(3, "\0\0\0"..., 4096) = 4096 <0.033237>
close(3) = 0 <0.000019>
测试程序.py:
#! /usr/bin/python3
import os
# Required third party module,
# install with "pip3 install --user fallocate".
from fallocate import fallocate
block = b'\0' * 4096
for alloc in [0, 4, 8, 9]:
# Open file for writing, with implicit fdatasync().
fd = os.open("out.tmp", os.O_WRONLY | os.O_DSYNC |
os.O_CREAT | os.O_TRUNC)
# Try to pre-allocate space
if alloc:
fallocate(fd, 0, alloc * 4096)
os.write(fd, block)
os.write(fd, block)
os.write(fd, block)
os.write(fd, block)
os.close(fd)
答案1
8 个和 9 个 4KB 块之间存在差异的原因是,ext4 在将由 创建的未分配盘区转换fallocate()
为已分配盘区时具有启发式。对于 32KB 或更少的未分配盘区,它只是用零填充整个盘区并重写整个内容,而较大的盘区被分成两个或三个较小的盘区并写出。
在 8 块的情况下,整个 32KB 范围将转换为正常范围,前 16KB 写入您的数据,其余部分用零填充并写出。在 9 块的情况下,36KB 范围被分割(因为它超过 32KB),您留下 16KB 范围用于数据和 20KB 未写入范围。
严格来说,20KB 未写入的范围也应该只用零填充并写出,但我怀疑它并没有这样做。但是,这只会稍微改变收支平衡点(在您的情况下为 16KB+32KB = 12 个块),但不会改变底层行为。
您可以filefrag -v out.tmp
在第一次写入后使用来查看磁盘上的块分配布局。
也就是说,您可以完全避免fallocate和O_DSYNC,并让文件系统尽快写出数据,而不是使文件布局变得比需要的更糟糕......
答案2
这种差异可能看起来很有趣,但最重要的是要了解您正在滥用fallocate()
. fallocate()
仅保证在磁盘上保留空间。不能保证提高同步写入的性能,即避免写入需要磁盘寻道的文件系统元数据。
您可以通过修改test-program.py
为预写一些数据块而不是使用 来说明这一点fallocate()
。在我的ext4
文件系统上,这为任一预分配大小提供了较低的“最小延迟”测量。我应该指出,其他文件系统将具有不同的性能配置文件。具体来说,如果它们是使用像写时复制那样实现的btrfs
,那么这将不起作用。
代码更改:
# Try to pre-allocate space
if alloc:
- fallocate(fd, 0, alloc * 4096)
+ os.pwrite(fd, block * alloc, 0)
+ os.fsync(fd)
结果:
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000088>
pwrite64(3, "\0\0\0"..., 36864, 0) = 36864 <0.035337>
fsync(3) = 0 <0.000366>
write(3, "\0\0\0"..., 4096) = 4096 <0.015217>
write(3, "\0\0\0"..., 4096) = 4096 <0.008194>
write(3, "\0\0\0"..., 4096) = 4096 <0.008371>
write(3, "\0\0\0"..., 4096) = 4096 <0.008299>
close(3) = 0 <0.000034>