为什么将相同的数据写入*较大的*预分配文件会更慢?

为什么将相同的数据写入*较大的*预分配文件会更慢?

我正在将 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>

相关内容