Linux のページキャッシュが Network File System (NFS) でどうはたらくか確認する
linuxLinux カーネルには各ファイルシステムによってディスクやネットワークから読み込まれたファイルを自動でメモリにキャッシュするページキャッシュの機構がある。
/proc/sys/vm/drop_caches に 3 を書き込んでページキャッシュに加え inode のキャッシュもすべて削除した状態から連続でファイルを読み込むと、キャッシュが効いている2回目は大幅に速くなる。これが NFS においてどのようにはたらくかを確認する。
$ dd if=/dev/urandom of=/data/pagecache_test_local.bin bs=1M count=200
$ sync; echo 3 > /proc/sys/vm/drop_caches
$ time cat /data/pagecache_test_local.bin > /dev/null
real 0m0.615s
user 0m0.000s
sys 0m0.043s
$ time cat /data/pagecache_test_local.bin > /dev/null
real 0m0.027s
user 0m0.000s
sys 0m0.027s
NFS サーバーをローカルに立ち上げディレクトリを公開する。
$ sudo dnf install -y nfs-utils
$ sudo systemctl enable --now nfs-server
$ sudo mkdir -p /srv/nfs_test
$ sudo chmod 777 /srv/nfs_test
$ echo "/srv/nfs_test 127.0.0.1(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
$ sudo exportfs -ra
これを他のディレクトリにマウントする。
$ sudo mkdir -p /mnt/nfstest
$ sudo mount -t nfs4 -o vers=4.2 127.0.0.1:/srv/nfs_test /mnt/nfstest
$ mount | grep nfstest
127.0.0.1:/srv/nfs_test on /mnt/nfstest type nfs4 (rw,relatime,vers=4.2,rsize=262144,wsize=262144,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1)
マウントしたディレクトリのファイルを連続で読むと同じくキャッシュが効いていることが分かる。
$ dd if=/dev/urandom of=/mnt/nfstest/testfile.bin bs=1M count=200
$ sync; echo 3 > /proc/sys/vm/drop_caches
$ time cat /mnt/nfstest/testfile.bin > /dev/null
real 0m0.617s
user 0m0.000s
sys 0m0.085s
$ time cat /mnt/nfstest/testfile.bin > /dev/null
real 0m0.038s
user 0m0.000s
sys 0m0.038s
ファイル本体のページキャッシュとは別に stat() で取れるファイルサイズや更新時刻といったメタデータのキャッシュもあり acregmin~acregmax 秒の間保持される。
$ grep -A 20 nfstest /proc/self/mountstats
device 127.0.0.1:/srv/nfs_test mounted on /mnt/nfstest with fstype nfs4 statvers=1.1
opts: rw,vers=4.2,rsize=262144,wsize=262144,namlen=255,acregmin=3,acregmax=60,acdirmin=30,acdirmax=60,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none
...
これにより繰り返し stat() を呼んでも毎回 NFS への問い合わせは走らない。
$ get_getattr_count() {
grep -A 30 nfstest /proc/self/mountstats | grep "GETATTR:" | awk '{print $2}'
}
$ echo -n "AAA" > /srv/nfs_test/acregmin_test.txt
$ for i in $(seq 1 8); do
before=$(get_getattr_count)
stat /mnt/nfstest/acregmin_test.txt > /dev/null
after=$(get_getattr_count)
echo "delta=$((after-before))"
sleep 0.5
done
delta=1
delta=0
delta=0
delta=0
delta=0
delta=0
delta=1
delta=0
ただ、連続で cat するとアクセスによって atime が更新されたのを 2 回目 stat() で読みに行って以降、サーバーアクセスがなくなる。
$ get_all() {
local block=$(grep -A 30 nfstest /proc/self/mountstats)
local op=$(echo "$block" | grep "^\s*OPEN:" | awk '{print $2}')
local opna=$(echo "$block" | grep "^\s*OPEN_NOATTR:" | awk '{print $2}')
local rd=$(echo "$block" | grep "^\s*READ:" | awk '{print $2}')
local ga=$(echo "$block" | grep "^\s*GETATTR:" | awk '{print $2}')
echo "OPEN=$op OPEN_NOATTR=$opna READ=$rd GETATTR=$ga"
}
$ for i in $(seq 1 8); do
before=$(get_all)
cat /mnt/nfstest/open_test.txt > /dev/null
after=$(get_all)
echo "iter$i before=[$before] after=[$after]"
sleep 0.5
done
iter1 before=[OPEN=0 OPEN_NOATTR=0 READ=0 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=1]
iter2 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter3 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter4 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter5 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter6 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter7 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
iter8 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=2]
これは NFS の delegation の影響を受けているため。NFS サーバーからコールバックで通知がない限りメタデータを問い合わせることなく使いまわすことができる。
$ grep DELEG /proc/locks | grep ":$(stat -c %i /mnt/nfstest/open_test.txt) "
1: DELEG ACTIVE READ 4161 103:01:26149127 0 EOF
delegation を無効にすると毎回 OPEN_NOATTR() で呼ばサーバーへ通信するようになる。
# reset
$ sync && sudo umount /mnt/nfstest && sudo mount -t nfs4 -o vers=4.2 127.0.0.1:/srv/nfs_test /mnt/nfstest
$ echo 0 | sudo tee /proc/sys/fs/leases-enable
$ for i in $(seq 1 8); do
before=$(get_all)
cat /mnt/nfstest/open_test.txt > /dev/null
after=$(get_all)
echo "iter$i before=[$before] after=[$after]"
sleep 0.5
done
iter1 before=[OPEN=0 OPEN_NOATTR=0 READ=0 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=1]
iter2 before=[OPEN=1 OPEN_NOATTR=0 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=1 READ=1 GETATTR=1]
iter3 before=[OPEN=1 OPEN_NOATTR=1 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=2 READ=1 GETATTR=1]
iter4 before=[OPEN=1 OPEN_NOATTR=2 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=3 READ=1 GETATTR=1]
iter5 before=[OPEN=1 OPEN_NOATTR=3 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=4 READ=1 GETATTR=1]
iter6 before=[OPEN=1 OPEN_NOATTR=4 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=5 READ=1 GETATTR=1]
iter7 before=[OPEN=1 OPEN_NOATTR=5 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=6 READ=1 GETATTR=1]
iter8 before=[OPEN=1 OPEN_NOATTR=6 READ=1 GETATTR=1] after=[OPEN=1 OPEN_NOATTR=7 READ=1 GETATTR=1]