ファイルシステムのブロックサイズの活用事例

ファイルシステムのブロックサイズといえば、stat コマンドで表示されるこれ (IO Block: 4096 )。 どういうときにこの値がユーザ空間で利用されているかを調べた。

$ stat /
  File: ‘/’
  Size: 270             Blocks: 0          IO Block: 4096   directory
Device: ca01h/51713d    Inode: 96          Links: 19
Access: (0555/dr-xr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-04-28 19:55:24.684679391 +0000
Modify: 2022-05-30 00:34:54.501073199 +0000
Change: 2022-05-30 00:34:54.501073199 +0000
 Birth: -

結論

cp や cat は、ファイルシステムのブロックサイズを基にバッファリングする I/O サイズを調整している。

動作確認

この調整はとくにネットワーク経由の I/O で効果的なため、NFS を例に動作確認する。

まずは nfs ファイルシステムを /mnt/nfs にマウントする。

$ sudo mkdir /share
$ sudo chmod 1777 /share
$ sudo echo "/share 127.0.0.1(rw)" | sudo tee /etc/exports
/share 127.0.0.1(rw)
$ sudo systemctl start nfs-server

$ sudo mkdir /mnt/nfs
$ sudo mount -t nfs 127.0.0.1:/share /mnt/nfs/

いちおう nfs がマウントされていることと、適当に作成したファイルを stat してブロックサイズが IO Block: 131072 であることを確認する。

$ mount -t nfs4
127.0.0.1:/share on /mnt/nfs type nfs4 (rw,relatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1)

$ touch /mnt/nfs/dummy
$ stat /mnt/nfs/dummy
  File: ‘/mnt/nfs/dummy’
  Size: 0               Blocks: 0          IO Block: 131072 regular empty file
Device: 2ah/42d Inode: 54531515    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ec2-user)   Gid: ( 1000/ec2-user)
Access: 2022-06-01 00:04:32.547735818 +0000
Modify: 2022-06-01 00:04:32.547735818 +0000
Change: 2022-06-01 00:04:32.547735818 +0000
 Birth: -

ここで、1M のファイルを作成し、nfs ファイルシステム配下に cp する。 すると、先ほどのブロックサイズ(131072) の単位で write されていることがわかる。

$ dd if=/dev/zero of=1m  bs=1M count=1
1+0 records in
1+0 records out
1048576 bytes (1.0 MB) copied, 0.000975111 s, 1.1 GB/s

$ sudo strace -e write cp 1m /mnt/nfs/
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
...

また cat すると、131072 の単位で write されていることがわかる。

$ strace -e write cat /mnt/nfs/1m > /dev/null
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
...

で、なぜこうなっているかというと、coreutils の src/ioblksize.h で、io_blksize が次のように定義されているから。 IO_BUFSIZE(64*1024 = 65536) と stat で取得できる blksize のどちらか大きいほうが利用される。

enum { IO_BUFSIZE = 64*1024 };
static inline size_t
io_blksize (struct stat sb)
{
  return MAX (IO_BUFSIZE, ST_BLKSIZE (sb));
}

このように、とくに NFS や CIFS などのネットワーク経由のファイルシステムでブロックサイズを大きめに取ることで、とくに同期書き込みの場合に頻繁に I/O を発行することなく、処理ができるようになる(もう少し正確にいうと、ネットワーク経由で read/write する単位はそれぞれ rsize/wsize で調整する。ここでいうブロックサイズはユーザ空間でバッファリングするサイズ。ただし同期書き込みの場合は、read(2)/write(2) のたびにネットワーク経由の I/O が発生するので、バッファリングするサイズが重要になる。)。