*より大きい*事前に割り当てられたファイルに同じデータを書き込むのが遅いのはなぜですか?

*より大きい*事前に割り当てられたファイルに同じデータを書き込むのが遅いのはなぜですか?

4 * 4KBチャンクをファイルに書き込みます。 4ブロックではなく9ブロックにファイルを事前に割り当てると、fallocate()常に約50%遅くなります。なぜ?

事前に割り当てられたブロック8と9の間にカットオフがあるようです。また、最初と2番目のブロック書き込みが常に遅い理由も疑問に思います。

このテストは私が使用しているいくつかのファイルコピーコードから得られました。からインスピレーションを受けるこの質問はddO_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 sparse_extra大容量ファイルhuge_file 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のブロックが異なる理由は、作成された未割り当ての拡張領域を割り当てられfallocate()た拡張領域に変換するときにext4にヒューリスティックがあるためです。 32KB以下の未割り当てエクステントの場合、単にフルエクステントをゼロで埋め、内容全体を書き換えますが、より大きなエクステントは2〜3個の小さなエクステントに分割されて記録されます。

8ブロックの場合、32KBの範囲全体が通常の範囲に変換され、最初の16KBはユーザーデータとして書き込まれ、残りは0で埋められます。 9ブロックの場合、36KBの範囲が分割され(32KBを超えるため)、データ用の範囲は16KB、書き込まれない範囲は20KBが残ります。

厳密に言えば、20KBの記録されていない範囲も単にゼロで埋めて記録する必要がありますが、そうでないようです。ただし、これは損益分岐点をわずかに変更します(あなたの場合は16 KB + 32 KB = 12ブロック)、デフォルトの動作は変わりません。

filefrag -v out.tmp最初の書き込み後、ディスクのブロック割り当てレイアウトを表示するために使用できます。

つまり、fallocateとO_DSYNCを完全に避け、ファイルレイアウトを必要以上に悪くするのではなく、ファイルシステムにできるだけ早くデータを書き込むことができます。

答え2

この違いは興味深いようですが、理解する必要がある最も重要なことはそれを乱用していることですfallocate()fallocate()スペースはディスクにのみ予約されています。同期書き込み性能が向上するという保証はありません。つまり、ディスク検索が必要なファイルシステムメタデータの書き込みを防ぐことです。

test-program.pyを使用する代わりに、いくつかのデータブロックを事前に作成するように変更することでこれを説明できますfallocate()。私のext4ファイルシステムは、事前に割り当てられたサイズに対してより低い「最小待ち時間」メジャーを提供します。他のファイルシステムには異なるパフォーマンスプロファイルがあることを指摘したいと思います。特に、copy-on-write のようなものを使って実装された場合には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>

関連情報