Linux のページキャッシュが Network File System (NFS) でどうはたらくか確認する

linux

Linux カーネルには各ファイルシステムによってディスクやネットワークから読み込まれたファイルを自動でメモリにキャッシュするページキャッシュの機構がある。

/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]