diff options
author | Ayush Ranjan <ayushranjan@google.com> | 2021-04-20 13:36:56 -0700 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2021-04-20 13:38:50 -0700 |
commit | 07a78ecb2918905af030a8cf81ee86ddd1c622c5 (patch) | |
tree | 4fd96d931a2f20055c277ffc1dacb1903be66c91 | |
parent | 8192cccda61d84f6caaf0a37ee3ba41cb6c4826c (diff) |
[perf] Remove non-empty directory dentries from gofer LRU cache.
The gofer client's LRU cache has a default limit of 1000 dentries. Any attempt
to cache more dentries than that will make the LRU cache evict and destroy the
least recently used dentry. However, the eviction is expensive because it
requires holding fs.renameMu for writing - which in turn creates a lot of
contention. All filesystem operations that involve path traversal require
fs.renameMu for reading atleast.
Therefore, it is in our best interest to keep the cache small and clean.
When a dentry is inserted in the dentry tree, it grabs a ref on its parent for
its entire lifetime. Hence the parent is longer evictable (because refs > 0).
This change additionally calls checkCachingLocked on directories that have been
added to so that they can be removed from the LRU cache if needed.
This change implies that the LRU cache will only contain the leaves from the
filesystem tree which significantly reduces the LRU cache size and consequently
reduces the number of expensive LRU cache evictions.
> Why are opened dentries not removed from LRU cache?
When a file description is open(2)-ed, the file description holds a ref on its
dentry for its entire lifetime. However, calling checkCachingLocked() on opened
dentries actually ends up hurting performance. Applications usually open file
descriptors for a short duration. So upon close(2), the dentry is reinserted
into the cache anyway. So the precautionary work done in removing the opened
dentry from the cache went for waste as it did not really reduce an eviction.
Local benchmarking has shown that this change improves performance by 3-4%.
Across 6 runs, without this change it took 296.127 seconds to build runsc while
with this change it took only 285.136 seconds.
PiperOrigin-RevId: 369510494
-rw-r--r-- | pkg/sentry/fsimpl/gofer/filesystem.go | 20 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/gofer/gofer.go | 9 |
2 files changed, 25 insertions, 4 deletions
diff --git a/pkg/sentry/fsimpl/gofer/filesystem.go b/pkg/sentry/fsimpl/gofer/filesystem.go index 77b1fc881..40c9243f0 100644 --- a/pkg/sentry/fsimpl/gofer/filesystem.go +++ b/pkg/sentry/fsimpl/gofer/filesystem.go @@ -259,8 +259,10 @@ func (fs *filesystem) getChildLocked(ctx context.Context, parent *dentry, name s } parent.cacheNewChildLocked(child, name) // For now, child has 0 references, so our caller should call - // child.checkCachingLocked(). + // child.checkCachingLocked(). parent gained a ref so we should also call + // parent.checkCachingLocked() so it can be removed from the cache if needed. *ds = appendDentry(*ds, child) + *ds = appendDentry(*ds, parent) return child, nil } @@ -621,6 +623,8 @@ func (fs *filesystem) GetDentryAt(ctx context.Context, rp *vfs.ResolvingPath, op } } d.IncRef() + // Call d.checkCachingLocked() so it can be removed from the cache if needed. + ds = appendDentry(ds, d) return &d.vfsd, nil } @@ -635,6 +639,8 @@ func (fs *filesystem) GetParentDentryAt(ctx context.Context, rp *vfs.ResolvingPa return nil, err } d.IncRef() + // Call d.checkCachingLocked() so it can be removed from the cache if needed. + ds = appendDentry(ds, d) return &d.vfsd, nil } @@ -673,7 +679,7 @@ func (fs *filesystem) LinkAt(ctx context.Context, rp *vfs.ResolvingPath, vd vfs. // MkdirAt implements vfs.FilesystemImpl.MkdirAt. func (fs *filesystem) MkdirAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.MkdirOptions) error { creds := rp.Credentials() - return fs.doCreateAt(ctx, rp, true /* dir */, func(parent *dentry, name string, _ **[]*dentry) error { + return fs.doCreateAt(ctx, rp, true /* dir */, func(parent *dentry, name string, ds **[]*dentry) error { // If the parent is a setgid directory, use the parent's GID // rather than the caller's and enable setgid. kgid := creds.EffectiveKGID @@ -693,6 +699,7 @@ func (fs *filesystem) MkdirAt(ctx context.Context, rp *vfs.ResolvingPath, opts v kuid: creds.EffectiveKUID, kgid: creds.EffectiveKGID, }) + *ds = appendDentry(*ds, parent) } if fs.opts.interop != InteropModeShared { parent.incLinks() @@ -746,6 +753,7 @@ func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts v kgid: creds.EffectiveKGID, endpoint: opts.Endpoint, }) + *ds = appendDentry(*ds, parent) return nil case linux.S_IFIFO: parent.createSyntheticChildLocked(&createSyntheticOpts{ @@ -755,6 +763,7 @@ func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts v kgid: creds.EffectiveKGID, pipe: pipe.NewVFSPipe(true /* isNamed */, pipe.DefaultPipeSize), }) + *ds = appendDentry(*ds, parent) return nil } // Retain error from gofer if synthetic file cannot be created internally. @@ -803,6 +812,8 @@ func (fs *filesystem) OpenAt(ctx context.Context, rp *vfs.ResolvingPath, opts vf start.IncRef() defer start.DecRef(ctx) unlock() + // start is intentionally not added to ds (which would remove it from the + // cache) because doing so regresses performance in practice. return start.open(ctx, rp, &opts) } @@ -859,6 +870,8 @@ afterTrailingSymlink: child.IncRef() defer child.DecRef(ctx) unlock() + // child is intentionally not added to ds (which would remove it from the + // cache) because doing so regresses performance in practice. return child.open(ctx, rp, &opts) } @@ -1106,6 +1119,7 @@ func (d *dentry) createAndOpenChildLocked(ctx context.Context, rp *vfs.Resolving } // Insert the dentry into the tree. d.cacheNewChildLocked(child, name) + *ds = appendDentry(*ds, d) if d.cachedMetadataAuthoritative() { d.touchCMtime() d.dirents = nil @@ -1302,6 +1316,7 @@ func (fs *filesystem) RenameAt(ctx context.Context, rp *vfs.ResolvingPath, oldPa oldParent.decRefNoCaching() ds = appendDentry(ds, oldParent) newParent.IncRef() + ds = appendDentry(ds, newParent) if renamed.isSynthetic() { oldParent.syntheticChildren-- newParent.syntheticChildren++ @@ -1445,6 +1460,7 @@ func (fs *filesystem) BoundEndpointAt(ctx context.Context, rp *vfs.ResolvingPath if d.isSocket() { if !d.isSynthetic() { d.IncRef() + ds = appendDentry(ds, d) return &endpoint{ dentry: d, path: opts.Addr, diff --git a/pkg/sentry/fsimpl/gofer/gofer.go b/pkg/sentry/fsimpl/gofer/gofer.go index f491aa641..21692d2ac 100644 --- a/pkg/sentry/fsimpl/gofer/gofer.go +++ b/pkg/sentry/fsimpl/gofer/gofer.go @@ -1412,8 +1412,13 @@ func (d *dentry) OnZeroWatches(ctx context.Context) { d.checkCachingLocked(ctx, false /* renameMuWriteLocked */) } -// checkCachingLocked should be called after d's reference count becomes 0 or it -// becomes disowned. +// checkCachingLocked should be called after d's reference count becomes 0 or +// it becomes disowned. +// +// For performance, checkCachingLocked can also be called after d's reference +// count becomes non-zero, so that d can be removed from the LRU cache. This +// may help in reducing the size of the cache and hence reduce evictions. Note +// that this is not necessary for correctness. // // It may be called on a destroyed dentry. For example, // renameMu[R]UnlockAndCheckCaching may call checkCachingLocked multiple times |