From 75ae33ade8e630b9e798001445130850d94c2cce Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 7 Aug 2026 17:48:42 -0700 Subject: [PATCH] mount: let the kernel cache directory listings (#10634) Every enumeration of a directory walked the whole FUSE machinery, so reopening a folder cost what opening it did. The kernel has a cache for exactly this: with FOPEN_CACHE_DIR the listing lives in the directory's page cache and a repeat enumeration never reaches the mount at all. Local mutations already drop that cache in the kernel. Remote ones arrive through the metadata subscription, so the entry invalidation worker now also tells the kernel which directory changed. The worker is the one place this is safe from: notifying from a thread serving a kernel request can deadlock against the page it holds, which is why the file paths deliberately avoid InodeNotify. Measured in a Linux container, 20k-entry directory, ls repeated: warm listing before 199-355ms after 6-9ms A file written from outside the mount appeared in the next listing within a second, through the subscription notify, and the listing re-cached after. The memory is the kernel's page cache: reclaimed under pressure, owned per-directory, and covering read-through directories the mount-side caches never see. --- weed/mount/weedfs.go | 24 ++++++++++++++ weed/mount/weedfs_dir_read.go | 6 ++++ .../mount/weedfs_dir_read_cache_flags_test.go | 32 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 weed/mount/weedfs_dir_read_cache_flags_test.go diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 032eed7cc..4cb68706e 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -802,9 +802,33 @@ func (wfs *WFS) onEntryInvalidation(invalidation meta_cache.EntryInvalidation) { if listener != nil { listener(invalidation) } + wfs.invalidateKernelDirListing(invalidation.Path) wfs.invalidateOpenFileHandle(invalidation) } +// invalidateKernelDirListing drops the kernel's cached listing of the directory +// holding path. Safe here because invalidations run on their own worker, never +// on a thread serving a kernel request; notifying from a handler can deadlock +// against the page it holds, which is why the file paths avoid InodeNotify. +// A directory the kernel has not looked up has no inode here and nothing +// cached, so it is skipped. +func (wfs *WFS) invalidateKernelDirListing(path util.FullPath) { + server := wfs.fuseServer + if server == nil { + return + } + dir, _ := path.DirAndName() + dirInode, found := wfs.inodeToPath.GetInode(util.FullPath(dir)) + if !found { + return + } + // ENOENT is the kernel not holding the inode, ENOSYS a kernel without the + // notify; neither is worth a line. + if status := server.InodeNotify(dirInode, 0, -1); status != fuse.OK && status != fuse.ENOENT && status != fuse.ENOSYS { + glog.V(4).Infof("invalidate kernel listing of %s: %v", dir, status) + } +} + // MountRoot is the filer path this mount is rooted at. Event paths are absolute // on the filer; a front end that addresses files relative to the mount needs it // to translate them. diff --git a/weed/mount/weedfs_dir_read.go b/weed/mount/weedfs_dir_read.go index faeb01857..428d25ebc 100644 --- a/weed/mount/weedfs_dir_read.go +++ b/weed/mount/weedfs_dir_read.go @@ -109,6 +109,12 @@ func (wfs *WFS) OpenDir(cancel <-chan struct{}, input *fuse.OpenIn, out *fuse.Op } dhid, _ := wfs.AcquireDirectoryHandle() out.Fh = uint64(dhid) + // Let the kernel keep the listing in the directory's page cache, so + // reopening the directory does not reach the mount at all. Local mutations + // drop that cache in the kernel; remote ones arrive through the metadata + // subscription, which notifies the kernel per changed directory. A kernel + // too old for the flag ignores it. + out.OpenFlags |= fuse.FOPEN_CACHE_DIR | fuse.FOPEN_KEEP_CACHE return fuse.OK } diff --git a/weed/mount/weedfs_dir_read_cache_flags_test.go b/weed/mount/weedfs_dir_read_cache_flags_test.go new file mode 100644 index 000000000..0ff8273b7 --- /dev/null +++ b/weed/mount/weedfs_dir_read_cache_flags_test.go @@ -0,0 +1,32 @@ +package mount + +import ( + "testing" + + "github.com/seaweedfs/go-fuse/v2/fuse" + "github.com/seaweedfs/seaweedfs/weed/util" +) + +// TestOpenDirEnablesKernelListingCache pins the reply flags: without them the +// kernel calls back for every enumeration, and losing them would silently +// revert repeat listings to full walks of the mount. +func TestOpenDirEnablesKernelListingCache(t *testing.T) { + dir := util.FullPath("/images") + wfs := newBenchWFS(t, dir, 4) + dirInode, _ := wfs.inodeToPath.GetInode(dir) + + var out fuse.OpenOut + if status := wfs.OpenDir(nil, &fuse.OpenIn{InHeader: fuse.InHeader{NodeId: dirInode}}, &out); status != fuse.OK { + t.Fatalf("OpenDir: %v", status) + } + defer wfs.ReleaseDir(&fuse.ReleaseIn{Fh: out.Fh}) + + for _, want := range []struct { + name string + flag uint32 + }{{"FOPEN_CACHE_DIR", fuse.FOPEN_CACHE_DIR}, {"FOPEN_KEEP_CACHE", fuse.FOPEN_KEEP_CACHE}} { + if out.OpenFlags&want.flag == 0 { + t.Errorf("OpenDir reply lacks %s", want.name) + } + } +}