XFS の speculative preallocation について調べてみた

この記事は「[エムスリー Advent Calendar 2015](http://qiita.com/advent-calendar/2015/m3)」の 15 日目の記事です。

----

きっかけはこれ

```
Filesystem            Size  Used Avail Use% Mounted on
/dev/mapper/vg01-lv_apps
                       18G   18G  184M  99% /apps
```

は！？毎分監視してるのにいきなり90%や95%をすっ飛ばして99%のアラートが！

```
# du -sh .
17G     .
# ls -lh
total 17G
-rw-r--r-- 1 root root 8.4G Sep 29 23:41 20150929-www-access.ltsv
-rw-r--r-- 1 root root  93K Sep 29 23:40 20150929-www-error.log
```

どいうこと？ 8.4GB のファイルがあるだけなのになんで 17GB 使ってることになってんの？

ls や find で見つからないのにディスクが溢れそうっていうのは通常削除済みのファイルを開いているプロセスが存在してるパターンなのだが今回はそれではない。

なにかのバグでファイルがおかしくなっているのだろうか？

```
# ls -lh
total 17G
-rw-r--r-- 1 root root 8.5G Sep 29 23:59 20150929-www-access.ltsv
-rw-r--r-- 1 root root  95K Sep 29 23:56 20150929-www-error.log
-rw-r--r-- 1 root root  21M Sep 30 00:02 20150930-www-access.ltsv
# du -sh *
17G     20150929-www-access.ltsv
96K     20150929-www-error.log
31M     20150930-www-access.ltsv
```

しばらくすると戻った。なんじゃこりゃ？（日付が変わってファイルが close されたのも関係ある？）

```
# ls -lh
total 8.6G
-rw-r--r-- 1 root root 8.5G Sep 29 23:59 20150929-www-access.ltsv
-rw-r--r-- 1 root root  95K Sep 29 23:56 20150929-www-error.log
-rw-r--r-- 1 root root  55M Sep 30 00:07 20150930-www-access.ltsv
-rw-r--r-- 1 root root  158 Sep 30 00:04 20150930-www-error.log
# du -sh *
8.5G    20150929-www-access.ltsv
96K     20150929-www-error.log
63M     20150930-www-access.ltsv
4.0K    20150930-www-error.log
```

RHEL も 7 から xfs がデフォルトになったし [ RDBMS in the Cloud: PostgreSQL on AWS (PDF)](http://media.amazonwebservices.com/AWS_RDBMS_PostgreSQL.pdf) にもデータ領域には xfs 使えって書いてあるし時代は xfs だろうということで選択していた xfs が怪しいんだろうなと思ったもののよくわからない、この後も再発したため、とりあえず　ext4 で作りなおすことで回避していたものの数カ月後、別のサーバーでも発生したのでこれは xfs に仕掛けがあるんだろうと調べてみた。

ググってみると「なんじゃこりゃ？」と思ったのは私だけではなかったっぽい。 xfs には speculative preallocation というファイル拡張時に大きめの連続した領域を確保することでフラグメントを抑えてパフォーマンスの低下を防ごうという機能が存在するようです。
http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=055388a3188f56676c21e92962fc366ac8b5cb72
この commit 後にも修正されているのでこれは最新のコードではありません。 [kernel-2.6.32-573.8.1.el6.src.rpm](http://vault.centos.org/6.7/updates/Source/SPackages/kernel-2.6.32-573.8.1.el6.src.rpm) を見てみました。

では、この preallocation で同様の状況が再現可能か試してみます。

次の簡単なスクリプトを用意します。

```pl:xfs-preallocation-test.pl
#!/usr/bin/perl
use strict;
use warnings;

sub bytes {
    my ($size) = @_;

    if ($size =~ s/g\z//i) {
        $size *= 1024 * 1024 * 1024;
    }
    elsif ($size =~ s/m\z//i) {
        $size *= 1024 * 1024;
    }
    elsif ($size =~ s/k\z//i) {
        $size *= 1024;
    }
    return $size;
}

if (scalar(@ARGV) != 4) {
    print "Usage: $0 /path/to/src_file /path/to/out_file bs size\n";
    exit(1);
}

my ($src_file, $out_file, $bs, $size) = @ARGV;
my $buf;
my $wrote = 0;

$bs = bytes($bs);
$size = bytes($size);

my $w;
open($w, ">>", $out_file)
  or die "cannot open '$out_file': $!";

my $r;
open($r, "<", $src_file)
  or die "cannot open $src_file: $!";

sleep 3;

while ($wrote < $size) {
    sysread($r, $buf, $bs);
    syswrite($w, $buf, length($buf));
    $wrote += length($buf);
}

close($r);

close($w);
```

[DigitalOcean](https://www.digitalocean.com/) の CentOS 6.7 x86_64 サーバーで 30GB の xfs パーティションを作ってテストしてみます。

```
truncate -s 30G /xfs-disk
mkfs -t xfs /xfs-disk
mkdir /xfs
mount -o loop -t xfs /xfs-disk /xfs
perl xfs-preallocation-test.pl /dev/zero /xfs/test 8k 8.5g &
watch -n 1 'ls -l /xfs/test; ls -lh /xfs/test; du -sh /xfs/test; xfs_bmap -v /xfs/test'
```

この処理の出力を asciinema にアップしました。ファイルサイズ拡張時に一気に 2GB や 4GB 増える様子が確認できます。
https://asciinema.org/a/e8mbpx26cxkqvr1zkad30i9ht
([ttyrec](http://qiita.com/yteraoka/items/ea85f094f7c934e78e11) より共有が楽ちんですね、手元にファイルで保存しておくこともできるし便利)

/dev/zero から 8.5G を書き込んだファイルのフラグメント状態を確認すると今回は次のように 5.8GB, 277MB, 1.7GB, 876MB に分断されていました。

```
[root@xfstest ~]# xfs_bmap -v /xfs/test
/xfs/test:
 EXT: FILE-OFFSET           BLOCK-RANGE        AG AG-OFFSET              TOTAL
   0: [0..11936783]:        96..11936879        0 (96..11936879)      11936784
   1: [11936784..12505039]: 15728704..16296959  1 (64..568319)          568256
   2: [12505040..16031583]: 24117296..27643839  1 (8388656..11915199)  3526544
   3: [16031584..17825791]: 19923088..21717295  1 (4194448..5988655)   1794208
```

このような細切れのファイルでは 8.5GB のファイルなのに 17GB も割り当てられるような状況にならないので状況再現のために `xfs_fsr` でデフラグします。

```
[root@xfstest ~]# xfs_fsr /xfs/test
[root@xfstest ~]# xfs_bmap -v /xfs/test
/xfs/test:
 EXT: FILE-OFFSET          BLOCK-RANGE        AG AG-OFFSET               TOTAL
   0: [0..3791751]:        11936880..15728631  0 (11936880..15728631)  3791752
   1: [3791752..17825791]: 31488064..45522103  2 (30784..14064823)    14034040
[root@xfstest ~]#
```

1.8GB と 6.8GB にまとまりました。

ここで先ほどと同じスクリプトでファイルに 3GB ほど追記してみます。(3GB も要らなかった)

```
perl xfs-preallocation-test.pl /dev/zero /xfs/test 8k 3g &
watch -n 1 'ls -l /xfs/test; ls -lh /xfs/test; du -sh /xfs/test; xfs_bmap -v /xfs/test'
```

https://asciinema.org/a/0tdob8vxfj5p08bap5uxblemx

8.5GB にちょいと追記しただけで今度は 17GB まで膨れ上がりました、そして、書き込み終了後もしばらく 17GB のままです（なんだか見覚えのある感じです）。

```
[root@xfstest ~]# date; ls -lh --full-time /xfs/test; du -h /xfs/test
Sun Dec 13 21:51:09 EST 2015
-rw-r--r-- 1 root root 12G 2015-12-13 21:49:56.831999975 -0500 /xfs/test
17G     /xfs/test
[root@xfstest ~]# date; ls -lh --full-time /xfs/test; du -h /xfs/test
Sun Dec 13 21:54:30 EST 2015
-rw-r--r-- 1 root root 12G 2015-12-13 21:49:56.831999975 -0500 /xfs/test
17G     /xfs/test
[root@xfstest ~]# date; ls -lh --full-time /xfs/test; du -h /xfs/test
Sun Dec 13 21:54:34 EST 2015
-rw-r--r-- 1 root root 12G 2015-12-13 21:49:56.831999975 -0500 /xfs/test
12G     /xfs/test
```

5分近く 17GB をキープしていました。これは fs.xfs.speculative_prealloc_lifetime の値が効いていそうです。

```
[root@xfstest ~]# sysctl fs.xfs.speculative_prealloc_lifetime
fs.xfs.speculative_prealloc_lifetime = 300
[root@xfstest ~]# cat /proc/sys/fs/xfs/speculative_prealloc_lifetime
300
```

おそらくこれれすね、あの 99% になった現象は。毎日発生するわけではなかったのでたまたまうまい具合に連続したエクステントを確保できてしまった場合に一気に大きく割り当てられてしまったのでしょう。一度の拡張は最大で8GB （デフォルトの bsize=4096 の場合）。ボリュームの空き容量やクオータ制限での残り書き込み可能サイズでこの値は小さくなります。

```
freespace       max prealloc size
  >5%             full extent (8GB)
  4-5%             2GB (8GB >> 2)
  3-4%             1GB (8GB >> 3)
  2-3%           512MB (8GB >> 4)
  1-2%           256MB (8GB >> 5)
  <1%            128MB (8GB >> 6)
```

こんなのやだよ、無効にしたいよという場合にはどうするかというと、マウントオプションで allocsize を明示的に指定すれば良いようです。パフォーマンスを気にする必要の無いログサーバーなどであれば無駄に確保されるより良さそうです。

https://www.kernel.org/doc/Documentation/filesystems/xfs.txt

```
  allocsize=size
	Sets the buffered I/O end-of-file preallocation size when
	doing delayed allocation writeout (default size is 64KiB).
	Valid values for this option are page size (typically 4KiB)
	through to 1GiB, inclusive, in power-of-2 increments.

	The default behaviour is for dynamic end-of-file
	preallocation size, which uses a set of heuristics to
	optimise the preallocation size based on the current
	allocation patterns within the file and the access patterns
	to the file. Specifying a fixed allocsize value turns off
	the dynamic behaviour.
```

これで少し安心して xfs が使えそうです。