diff --git a/weed/command/mount.go b/weed/command/mount.go index d400deeeb..d7263cf91 100644 --- a/weed/command/mount.go +++ b/weed/command/mount.go @@ -74,6 +74,7 @@ type MountOptions struct { cacheSymlink *bool fuseMaxBackground *int fuseCongestionThreshold *int + fusePassthroughMaxMB *int64 // macOS-specific FUSE options novncache *bool @@ -162,6 +163,7 @@ func init() { mountOptions.writebackCache = cmdMount.Flag.Bool("writebackCache", false, "enable FUSE writeback cache for improved write performance (at risk of data loss on crash)") mountOptions.asyncDio = cmdMount.Flag.Bool("asyncDio", false, "enable async direct I/O for better concurrency") mountOptions.cacheSymlink = cmdMount.Flag.Bool("cacheSymlink", false, "enable symlink caching to reduce metadata lookups") + mountOptions.fusePassthroughMaxMB = cmdMount.Flag.Int64("fusePassthroughMaxMB", 256, "(Linux >=6.9) auto-enable FUSE passthrough for read-only files up to this size in MB: the kernel serves reads/mmap directly, bypassing the mount process. Auto-detected; falls back when unsupported (needs a privileged mount). 0 disables. A passthrough handle reads a point-in-time snapshot.") mountOptions.fuseMaxBackground = cmdMount.Flag.Int("fuse.maxBackground", 128, "FUSE max_background: maximum in-flight asynchronous requests the kernel will queue. Heavy upload workloads may benefit from higher values (e.g. 2048). Equivalent to writing /sys/fs/fuse/connections//max_background. If -fuse.congestionThreshold is 0, the kernel derives it as 3/4 of this value.") mountOptions.fuseCongestionThreshold = cmdMount.Flag.Int("fuse.congestionThreshold", 0, "FUSE congestion_threshold: in-flight async request count at which the kernel marks the FUSE bdi as congested and throttles new submissions. 0 means use the default (3/4 of -fuse.maxBackground). Equivalent to writing /sys/fs/fuse/connections//congestion_threshold. The kernel silently clamps this to -fuse.maxBackground when set higher.") diff --git a/weed/command/mount_std.go b/weed/command/mount_std.go index c133d82e7..ad1344ff7 100644 --- a/weed/command/mount_std.go +++ b/weed/command/mount_std.go @@ -315,6 +315,11 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { if option.cacheSymlink != nil && *option.cacheSymlink { fuseMountOptions.EnableSymlinkCaching = true } + if option.fusePassthroughMaxMB != nil && *option.fusePassthroughMaxMB > 0 { + // FUSE_PASSTHROUGH requires a non-zero stacking depth advertised in + // FUSE_INIT; without it the kernel rejects backing-file registration. + fuseMountOptions.MaxStackDepth = 1 + } // find mount point mountRoot := filerMountRootPath @@ -328,6 +333,11 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { cacheDirForWrite = cacheDirForRead } + passthroughMaxMB := int64(0) + if option.fusePassthroughMaxMB != nil { + passthroughMaxMB = *option.fusePassthroughMaxMB + } + seaweedFileSystem := mount.NewSeaweedFileSystem(&mount.Option{ MountDirectory: dir, FilerAddresses: filerAddresses, @@ -373,6 +383,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { EnableDistributedLock: option.distributedLock != nil && *option.distributedLock, WritebackCache: option.writebackCache != nil && *option.writebackCache, PosixDirNlink: option.posixDirNlink != nil && *option.posixDirNlink, + FusePassthroughMaxMB: passthroughMaxMB, // Peer chunk sharing PeerEnabled: option.peerEnabled != nil && *option.peerEnabled, PeerListen: peerStringOrEmpty(option.peerListen), diff --git a/weed/mount/filehandle.go b/weed/mount/filehandle.go index ba4a2cdb0..3cd3d85f0 100644 --- a/weed/mount/filehandle.go +++ b/weed/mount/filehandle.go @@ -51,6 +51,16 @@ type FileHandle struct { // for debugging mirrorFile *os.File + + // FUSE_PASSTHROUGH (Linux >= 6.9) state. On a read-only open the whole + // file is materialized into passthroughFile and registered with the + // kernel so reads/mmap bypass this daemon. There is one handle per inode, + // so this state is shared by all concurrent opens and torn down in + // ReleaseHandle when the last open closes. Guarded by passthroughMu. + passthroughMu sync.Mutex + passthroughTried bool + passthroughBackingID int32 + passthroughFile *os.File } func newFileHandle(wfs *WFS, handleId FileHandleId, inode uint64, entry *filer_pb.Entry) *FileHandle { @@ -155,6 +165,7 @@ func (fh *FileHandle) ReleaseHandle() { defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock) fh.dirtyPages.Destroy() + fh.teardownPassthrough() if IsDebugFileReadWrite { fh.mirrorFile.Close() } diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 9b7665183..d683e4c76 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -114,6 +114,16 @@ type Option struct { // When false (default), directories report nlink=2. PosixDirNlink bool + // FusePassthroughMaxMB auto-enables Linux FUSE_PASSTHROUGH for read-only + // opens of files up to this size (MB). When > 0 (the default) and the + // kernel supports it (>= 6.9, privileged mount), the whole file is + // materialized into a local backing file and the kernel serves reads/mmap + // directly from it, bypassing this mount process. Support is auto-detected + // and silently falls back when unavailable; set to 0 to disable. NOTE: a + // passthrough handle reads a point-in-time snapshot — concurrent writes are + // not reflected while the file stays open. + FusePassthroughMaxMB int64 + uniqueCacheDirForRead string uniqueCacheDirForWrite string } @@ -138,6 +148,10 @@ type WFS struct { dhMap *DirectoryHandleToInode fuseServer *fuse.Server IsOverQuota bool + // passthroughDisabled latches true after the first RegisterBackingFd + // failure (unprivileged mount, or a kernel without passthrough), so we + // stop attempting the ioctl on every subsequent open. + passthroughDisabled atomic.Bool fhLockTable *util.LockTable[FileHandleId] hardLinkLockTable *util.LockTable[string] posixLocks *PosixLockTable diff --git a/weed/mount/weedfs_file_io.go b/weed/mount/weedfs_file_io.go index 959d05849..30e164288 100644 --- a/weed/mount/weedfs_file_io.go +++ b/weed/mount/weedfs_file_io.go @@ -68,10 +68,15 @@ func (wfs *WFS) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) out.Fh = uint64(fileHandle.fh) out.OpenFlags = 0 - // For read-only opens, set FOPEN_KEEP_CACHE when the file's mtime - // has not changed since the last open. This tells the kernel to - // preserve its existing page cache, avoiding redundant reads. + // For read-only opens, first try FUSE passthrough (auto-enabled where + // the kernel supports it): the kernel serves reads/mmap directly from a + // local backing file, bypassing this daemon. If passthrough is not + // enabled, fall back to FOPEN_KEEP_CACHE when the file's mtime has not + // changed since the last open, preserving the kernel page cache. if in.Flags&fuse.O_ANYWRITE == 0 { + if wfs.tryEnablePassthrough(fileHandle, out) { + return status + } if entry := fileHandle.GetEntry(); entry != nil && entry.Attributes != nil { wfs.applyKeepCacheFlag(in.NodeId, entry, out) } diff --git a/weed/mount/weedfs_passthrough_linux.go b/weed/mount/weedfs_passthrough_linux.go new file mode 100644 index 000000000..f6f898af9 --- /dev/null +++ b/weed/mount/weedfs_passthrough_linux.go @@ -0,0 +1,149 @@ +//go:build linux + +package mount + +import ( + "fmt" + "os" + + "github.com/seaweedfs/go-fuse/v2/fuse" + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +// tryEnablePassthrough materializes a read-only file into a local backing file +// and registers it with the kernel via FUSE_PASSTHROUGH, so reads and mmap on +// this open are served by the kernel directly from the backing file without +// round-tripping through this daemon. +// +// It is auto-enabled — there is no opt-in flag. Kernel support (Linux >= 6.9 and +// a privileged mount) is probed by the first RegisterBackingFd call and latched +// off on failure, so unsupported mounts transparently use the normal read path. +// It is gated to read-only opens of files no larger than FusePassthroughMaxMB +// (set that to 0 to disable entirely). +// +// Tradeoff: the backing file is a point-in-time snapshot taken at open. While an +// inode stays open in passthrough, concurrent writes (this mount or another) are +// not reflected — best for immutable / read-mostly data. +func (wfs *WFS) tryEnablePassthrough(fh *FileHandle, out *fuse.OpenOut) bool { + capBytes := wfs.option.FusePassthroughMaxMB * 1024 * 1024 + if capBytes <= 0 || wfs.fuseServer == nil { + return false + } + if wfs.passthroughDisabled.Load() { + return false + } + + entry := fh.GetEntry() + if entry == nil || entry.Attributes == nil { + return false + } + fileSize := int64(entry.Attributes.FileSize) + if fileSize <= 0 || fileSize > capBytes { + return false + } + + fh.passthroughMu.Lock() + defer fh.passthroughMu.Unlock() + + // Establish the backing file once per handle (one handle per inode), then + // reuse it for every concurrent open of the same inode. + if !fh.passthroughTried { + fh.passthroughTried = true + backingID, file, err := wfs.setupPassthroughBacking(fh, fileSize) + if err != nil { + glog.V(1).Infof("passthrough setup %s: %v", fh.FullPath(), err) + return false + } + fh.passthroughBackingID = backingID + fh.passthroughFile = file + glog.V(2).Infof("passthrough enabled for %s (%d bytes, backingID %d)", fh.FullPath(), fileSize, backingID) + } + + if fh.passthroughBackingID == 0 { + return false + } + + out.BackingID = fh.passthroughBackingID + out.OpenFlags |= fuse.FOPEN_PASSTHROUGH + // Passthrough and the page-cache keep-cache hint are mutually exclusive; + // the backing file owns caching once passthrough is on. + out.OpenFlags &^= fuse.FOPEN_KEEP_CACHE + return true +} + +// setupPassthroughBacking downloads the whole file into a temp file and +// registers its fd with the kernel, returning the backing ID. +func (wfs *WFS) setupPassthroughBacking(fh *FileHandle, fileSize int64) (int32, *os.File, error) { + dir := wfs.option.CacheDirForRead + if dir == "" { + dir = os.TempDir() + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return 0, nil, err + } + file, err := os.CreateTemp(dir, "swpassthrough-") + if err != nil { + return 0, nil, err + } + // Unlink immediately: the kernel keeps the inode alive via the registered + // fd, so no temp file is left behind on close or crash. + os.Remove(file.Name()) + + if err := materializeFile(fh, file, fileSize); err != nil { + file.Close() + return 0, nil, err + } + + backingID, errno := wfs.fuseServer.RegisterBackingFd(&fuse.BackingMap{ + Fd: int32(file.Fd()), + }) + if errno != 0 { + // Typically EPERM (unprivileged mount) or ENOTTY (kernel <6.9 or + // MaxStackDepth unset). Latch off so we stop attempting per open. + wfs.passthroughDisabled.Store(true) + file.Close() + return 0, nil, fmt.Errorf("RegisterBackingFd: %v", errno) + } + return backingID, file, nil +} + +// materializeFile copies the file's full contents into dst using the same read +// path the kernel Read handler uses, so the backing bytes are identical to what +// a normal read would return. +func materializeFile(fh *FileHandle, dst *os.File, fileSize int64) error { + buf := make([]byte, 1024*1024) + for offset := int64(0); offset < fileSize; { + // readDataByFileHandle reads chunks + dirty pages and maps EOF to nil. + n, err := readDataByFileHandle(buf, fh, offset) + if n > 0 { + if _, werr := dst.WriteAt(buf[:int(n)], offset); werr != nil { + return werr + } + offset += n + } + if err != nil { + return err + } + if n == 0 { + break + } + } + return dst.Sync() +} + +// teardownPassthrough unregisters and closes the backing file. Called from +// ReleaseHandle when the last open of the inode is closed. +func (fh *FileHandle) teardownPassthrough() { + fh.passthroughMu.Lock() + defer fh.passthroughMu.Unlock() + if fh.passthroughBackingID != 0 { + if errno := fh.wfs.fuseServer.UnregisterBackingFd(fh.passthroughBackingID); errno != 0 { + glog.Warningf("UnregisterBackingFd inode %d: %v", fh.inode, errno) + } + fh.passthroughBackingID = 0 + } + if fh.passthroughFile != nil { + fh.passthroughFile.Close() + fh.passthroughFile = nil + } +} diff --git a/weed/mount/weedfs_passthrough_other.go b/weed/mount/weedfs_passthrough_other.go new file mode 100644 index 000000000..14e7789a1 --- /dev/null +++ b/weed/mount/weedfs_passthrough_other.go @@ -0,0 +1,14 @@ +//go:build !linux + +package mount + +import "github.com/seaweedfs/go-fuse/v2/fuse" + +// FUSE_PASSTHROUGH is a Linux-only kernel feature (>= 6.9). On other platforms +// these are no-ops so the mount package builds and behaves as before. + +func (wfs *WFS) tryEnablePassthrough(fh *FileHandle, out *fuse.OpenOut) bool { + return false +} + +func (fh *FileHandle) teardownPassthrough() {}