mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* mount: readdir enters a child in the inode table only when it takes a reference Only readdirplus into the kernel takes a reference on the children it reports, and only that reference brings a FORGET later to take the entry back out. Every other listing was inserting all its children anyway. On WinFsp that meant a listing looked each child up, took a reference, and immediately gave it back, so a walk of a wide directory paid three write-lock acquisitions per entry to leave the table exactly as it found it. On a plain kernel readdir nothing gives the entry back at all, so listing a directory of 200k files grew both maps by 200k entries that were never reclaimed. A dirent's inode number is informational either way: the kernel must LOOKUP before it can use a nodeid, and the WinFsp adapter re-resolves every operation by path. So report the number and let the mapping be built when something actually looks the entry up. * mount: take the readdirplus reference without a second full lookup The entry has just been resolved a few lines above, so redoing the whole lookup only rebuilds the child path and walks both maps again to reach a counter. Bump it directly, falling back to the full lookup if a Forget removed the entry in between. * mount: benchmark a readdir over a 200k directory Drives doReadDirectory against a meta cache holding 200k entries, one round of 4096 at a time, for the three front ends that behave differently: a plain kernel readdir, kernel readdirplus, and a WinFsp listing that gets attributes but never returns a reference. Reports what each leaves behind in the inode table alongside the usual metrics. The sink declares TakesLookupRef as an ordinary method rather than through the interface, so the same file runs unchanged against an older tree for comparison. * mount: stamp an inode on the benchmark's entries The filer stores one on every entry it writes, so a real listing arrives with an inode and never derives its own. Leaving it zero made every child in the benchmark fall through to the MD5 in AsInode, work no filer-backed mount does, and charged it to both sides of the comparison.
223 lines
6.2 KiB
Go
223 lines
6.2 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const benchDirEntryCount = 200000
|
|
|
|
// benchSink drains a readdir the way a front end does. sinkLimit is how many
|
|
// entries one round accepts, standing in for the kernel reply buffer or the
|
|
// WinFsp adapter's batch.
|
|
type benchSink struct {
|
|
plus bool
|
|
takesRef bool
|
|
sinkLimit int
|
|
count int
|
|
inodes []uint64
|
|
lastOff uint64
|
|
attrs []fuse.EntryOut
|
|
}
|
|
|
|
func (s *benchSink) reset() {
|
|
s.count = 0
|
|
s.inodes = s.inodes[:0]
|
|
s.attrs = s.attrs[:0]
|
|
}
|
|
|
|
func (s *benchSink) AddEntry(entry fuse.DirEntry) bool {
|
|
if s.count >= s.sinkLimit {
|
|
return false
|
|
}
|
|
s.count++
|
|
s.lastOff = entry.Off
|
|
s.inodes = append(s.inodes, entry.Ino)
|
|
return true
|
|
}
|
|
|
|
func (s *benchSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
|
|
if s.count >= s.sinkLimit {
|
|
return nil
|
|
}
|
|
s.count++
|
|
s.lastOff = entry.Off
|
|
s.inodes = append(s.inodes, entry.Ino)
|
|
s.attrs = append(s.attrs, fuse.EntryOut{})
|
|
return &s.attrs[len(s.attrs)-1]
|
|
}
|
|
|
|
func (s *benchSink) TakesLookupRef() bool { return s.takesRef }
|
|
|
|
func inodeTableSize(i *InodeToPath) int {
|
|
i.RLock()
|
|
defer i.RUnlock()
|
|
return len(i.inode2path)
|
|
}
|
|
|
|
func newBenchWFS(tb testing.TB, dir util.FullPath, n int) *WFS {
|
|
tb.Helper()
|
|
|
|
uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
|
|
if err != nil {
|
|
tb.Fatalf("uid/gid mapper: %v", err)
|
|
}
|
|
|
|
root := util.FullPath("/")
|
|
option := &Option{
|
|
ChunkSizeLimit: 1024,
|
|
ConcurrentReaders: 1,
|
|
VolumeServerAccess: "filerProxy",
|
|
FilerAddresses: []pb.ServerAddress{pb.NewServerAddressWithGrpcPort("127.0.0.1:1", 1)},
|
|
GrpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
FilerMountRootPath: "/",
|
|
MountUid: 99,
|
|
MountGid: 100,
|
|
MountMode: 0o777,
|
|
MountMtime: time.Now(),
|
|
MountCtime: time.Now(),
|
|
UidGidMapper: uidGidMapper,
|
|
}
|
|
|
|
wfs := &WFS{
|
|
option: option,
|
|
signature: 1,
|
|
inodeToPath: NewInodeToPath(root, 0),
|
|
fhMap: NewFileHandleToInode(),
|
|
dhMap: NewDirectoryHandleToInode(),
|
|
fhLockTable: util.NewLockTable[FileHandleId](),
|
|
hardLinkLockTable: util.NewLockTable[string](),
|
|
}
|
|
wfs.metaCache = meta_cache.NewMetaCache(
|
|
filepath.Join(tb.TempDir(), "meta"),
|
|
uidGidMapper,
|
|
root,
|
|
false,
|
|
func(path util.FullPath) { wfs.inodeToPath.MarkChildrenCached(path) },
|
|
func(path util.FullPath) bool { return wfs.inodeToPath.IsChildrenCached(path) },
|
|
func(meta_cache.EntryInvalidation) {},
|
|
nil,
|
|
)
|
|
tb.Cleanup(wfs.metaCache.Shutdown)
|
|
|
|
now := time.Now()
|
|
ctx := context.Background()
|
|
if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
|
|
FullPath: dir,
|
|
Attr: filer.Attr{Mode: os.ModeDir | 0o755, Mtime: now, Crtime: now, Uid: 99, Gid: 100},
|
|
}, 0); err != nil {
|
|
tb.Fatalf("insert dir: %v", err)
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
child := dir.Child(fmt.Sprintf("image-%08d.jpg", i))
|
|
if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
|
|
FullPath: child,
|
|
// The filer stamps an inode on every entry it stores, so a listing
|
|
// arrives with one and never has to derive its own.
|
|
Attr: filer.Attr{Mode: 0o644, Mtime: now, Crtime: now, Uid: 99, Gid: 100, FileSize: 4096, Inode: child.AsInode(now.Unix())},
|
|
}, 0); err != nil {
|
|
tb.Fatalf("insert entry %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Mark the tree cached so the listing is served from the meta cache and no
|
|
// filer client is dialled.
|
|
wfs.inodeToPath.MarkChildrenCached(root)
|
|
wfs.inodeToPath.Lookup(dir, now.Unix(), true, false, 0, true)
|
|
wfs.inodeToPath.MarkChildrenCached(dir)
|
|
|
|
return wfs
|
|
}
|
|
|
|
// walkOnce enumerates the whole directory the way a front end does: repeated
|
|
// rounds against one handle until the listing runs dry, returning whatever
|
|
// references the round took.
|
|
func walkOnce(tb testing.TB, wfs *WFS, dirInode uint64, sink *benchSink, forgets bool) int {
|
|
dhid, _ := wfs.AcquireDirectoryHandle()
|
|
defer wfs.ReleaseDirectoryHandle(dhid)
|
|
|
|
total := 0
|
|
offset := uint64(0)
|
|
for {
|
|
sink.reset()
|
|
status := wfs.doReadDirectory(&fuse.ReadIn{
|
|
InHeader: fuse.InHeader{NodeId: dirInode},
|
|
Fh: uint64(dhid),
|
|
Offset: offset,
|
|
Size: 1 << 20,
|
|
}, sink, sink.plus)
|
|
if status != fuse.OK {
|
|
tb.Fatalf("readdir: %v", status)
|
|
}
|
|
if sink.count == 0 {
|
|
return total
|
|
}
|
|
total += sink.count
|
|
if forgets {
|
|
// HasInode keeps this to the entries that really hold a reference,
|
|
// so a listing that took none is not charged for a bogus Forget.
|
|
for _, ino := range sink.inodes {
|
|
if wfs.inodeToPath.HasInode(ino) {
|
|
wfs.Forget(ino, 1)
|
|
}
|
|
}
|
|
}
|
|
if sink.lastOff <= offset {
|
|
return total
|
|
}
|
|
offset = sink.lastOff
|
|
}
|
|
}
|
|
|
|
var benchCases = []struct {
|
|
name string
|
|
plus bool
|
|
// ref mirrors the sink's TakesLookupRef. The kernel's reports true whatever
|
|
// the mode, exactly as fuseDirEntryList does; it is the mode that decides
|
|
// whether a reference is actually granted.
|
|
ref bool
|
|
// forgets is what the front end does after a round: the kernel returns one
|
|
// FORGET per readdirplus entry and none for a plain readdir, and the WinFsp
|
|
// adapter hands back everything a round gave it.
|
|
forgets bool
|
|
}{
|
|
{"kernel_readdir", false, true, false},
|
|
{"kernel_readdirplus", true, true, true},
|
|
{"winfsp_readdirplus", true, false, true},
|
|
}
|
|
|
|
func BenchmarkReadDirectory(b *testing.B) {
|
|
dir := util.FullPath("/images")
|
|
for _, tc := range benchCases {
|
|
b.Run(tc.name, func(b *testing.B) {
|
|
wfs := newBenchWFS(b, dir, benchDirEntryCount)
|
|
dirInode, _ := wfs.inodeToPath.GetInode(dir)
|
|
sink := &benchSink{plus: tc.plus, takesRef: tc.ref, sinkLimit: 4096}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
if got := walkOnce(b, wfs, dirInode, sink, tc.forgets); got != benchDirEntryCount+2 {
|
|
b.Fatalf("listed %d entries, want %d", got, benchDirEntryCount+2)
|
|
}
|
|
}
|
|
b.StopTimer()
|
|
// What the listing left in the inode table, over the root and the
|
|
// directory itself.
|
|
b.ReportMetric(float64(inodeTableSize(wfs.inodeToPath)), "inodes_left")
|
|
})
|
|
}
|
|
}
|