XFSで使った以上に容量が減るナゾ

Red Hatの森若です。

ddやcatで大きめのファイルを操作して、dfでファイルシステムの統計情報を確認する簡単な実験をします。

(注意: 諸条件によりお手元で同じ操作をしても再現しない場合があります)

## 1GBのファイルhogeを作ります
# dd if=/dev/zero of=/hoge bs=1GiB count=1
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.474922 s, 2.3 GB/s
# df -h /
Filesystem                    Size  Used Avail Use% Mounted on
/dev/mapper/rhel_rhel84-root   17G  7.2G  9.9G  42% /

## catで同じ内容のファイルhugaを作ります。
# cat hoge > huga
# df -h /
Filesystem                    Size  Used Avail Use% Mounted on
/dev/mapper/rhel_rhel84-root   17G  8.2G  8.9G  48% /

## catでファイルhugaの末尾に1GB追記します。
# cat hoge >> huga
# df -h /
Filesystem                    Size  Used Avail Use% Mounted on
/dev/mapper/rhel_rhel84-root   17G   11G  6.9G  60% /
## ↑ 1GBしか消費しないはずなのに2GB消費されています

## catでさらにファイルhugaの末尾に1GB追記します。
# cat hoge >> huga
# df -h /
Filesystem                    Size  Used Avail Use% Mounted on
/dev/mapper/rhel_rhel84-root   17G   11G  6.9G  60% /
## ↑ 今度は使用量が変わりませんでした

予想外の動作だという人も多いかと思います。

ファイルのフラグメンテーション

本題に入る前に前提知識です。

伝統的なファイルシステムの仕事として、ファイルのフラグメンテーションを予防することがあります。 ファイルのフラグメンテーションとは1つのファイルがブロックデバイス上でバラバラの位置に配置されることで、 特にハードディスクではファイルの読み書きのパフォーマンスに大きな影響があります。 (近年はNVMe やSSDの普及によって影響が小さくなってきました)

フラグメンテーションでバラバラになったファイルの雰囲気を伝えたい図

フラグメンテーションの逆に、ファイルをブロックデバイス上で連続して配置できると、良いことがあります。

  • あとで参照・更新するときに連続でアクセスできるので速い
  • ファイル内のどこのデータがブロックデバイス上のどこにあるかの対応関係を管理するデータが少なくて済む

このような背景から、ファイルシステムにはファイルのフラグメンテーションを予防するための工夫が含まれています。

XFSのspeculative preallocation

RHELのデフォルトのファイルシステムであるXFSにはフラグメンテーションを予防するための仕組みとして speculative preallocation(直訳すると "投機的な事前割り当て")と呼ばれる機能があります。 仕組みは簡単で、ファイル末尾への書き込みを契機として本来必要なファイル末尾までを格納するブロックより あとの連続したブロックをあらかじめファイルに割りあてます。 もし同じファイルへ追記すると、割りあてられたブロックが利用されます。 割りあてたブロックはデフォルトで5分間使われなければ回収されます。

さきほどと同じ実験をして、消費が1GB増えるはずが2GB増えたときにファイルの状態をstatで見てみます。

## ファイル huga は 2GB、512バイトのブロックが 4194304 個のはずが 6291584個割りあてられている
# stat huga
  File: huga
  Size: 2147483648  Blocks: 6291584    IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 1256334     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Context: unconfined_u:object_r:etc_runtime_t:s0
Access: 2022-11-23 05:51:54.801681431 +0900
Modify: 2022-11-23 05:52:02.397067737 +0900
Change: 2022-11-23 05:52:02.397067737 +0900
 Birth: -
## しばらく待ってからstatすると Block が減っている
# stat huga
  File: huga
  Size: 2147483648  Blocks: 4194304    IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 1256334     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Context: unconfined_u:object_r:etc_runtime_t:s0
Access: 2022-11-23 05:51:54.801681431 +0900
Modify: 2022-11-23 05:52:02.397067737 +0900
Change: 2022-11-23 05:52:02.397067737 +0900
 Birth: -

speculative preallocationは様々なコマンドを呼び出すシェルスクリプトやログの出力などでみられる、 「ファイルをopen、末尾へ出力、ファイルをclose」を間欠的に複数のプロセスから繰り返すパターンにうまく対応します。 あらかじめ確保するサイズはファイル容量によって調整され、ファイルシステム全体の空き容量も考慮されます。

speculative preallocationの制限

ファイルシステムの統計情報を元に何らかの処理をおこなうプログラムが speculative preallocationにうまく対応できない場合があります。 このような場合 speculative preallocationを禁止することはできませんが、 あらかじめ確保するサイズを小さくして影響を小さくすることができます。

XFSのマウントオプション allocsize= でspeculative preallocationのサイズを指定できます。

## fstabの例
/dev/mapper/rhel_rhel84-root /                       xfs     defaults,allocsize=64k        0 0

まとめ

XFSには独特な工夫があり、統計情報を見ているとびっくりすることもあります。 ほとんどの場合に害はありませんが、問題がある場合には制限をかけることもできます。

関連リンク

linux kernelのxfsドキュメント mjmwired.net

RHELのナレッジ記事 access.redhat.com

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。