metrics: use Linux 6.2+ stat fast path for CurrentFDs

As of Linux 6.2 (torvalds/linux@f1f1f25699),
the stat size of the /proc/self/fd directory is the number of open file
descriptors, so CurrentFDs no longer needs to walk the directory on
modern kernels. The walk is O(n) in the number of open file descriptors,
which adds up on servers like derper that hold many connections and
report this metric on every scrape: at 10k open fds, the walk takes
about 2.2ms while an fstat takes a constant 300ns.

We fstat a directory file descriptor held open for the process lifetime
rather than calling stat with the path, because syscall.Stat copies the
path string to a NUL-terminated buffer on every call and the metric
must remain allocation-free. Kernels older than 6.2 report a size of
zero, in which case we fall back to the directory walk.

Updates #2784

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I1742db2ae1938c77f9b7d137b167ab4a2b6b6167
This commit is contained in:
Brad Fitzpatrick authored and Brad Fitzpatrick committed 2026-09-08 18:01:59 -07:00
1 parent 85c1efb466
commit 1e95ec8ad5
2 files changed
+56 -1

No files matched your search

+36 -1
View File
@@ -6,6 +6,7 @@ package metrics
import (
"io/fs"
"sync"
"syscall"
"go4.org/mem"
"tailscale.com/util/dirwalk"
@@ -32,10 +33,44 @@ func (c *counter) count(name mem.RO, de fs.DirEntry) error {
return nil
}
func currentFDs() int {
// procSelfFDFd returns a file descriptor for the /proc/self/fd
// directory, kept open for the process lifetime so that
// currentFDsFast can fstat it without allocating.
var procSelfFDFd = sync.OnceValues(func() (int, error) {
return syscall.Open("/proc/self/fd", syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
})
// currentFDsFast returns the number of open file descriptors and
// whether it was able to determine it. It uses the fact that as of
// Linux 6.2 (torvalds/linux@f1f1f2569901), the stat size of the
// /proc/self/fd directory is the number of open file descriptors.
// On older kernels the reported size is zero.
func currentFDsFast() (n int, ok bool) {
fd, err := procSelfFDFd()
if err != nil {
return 0, false
}
var st syscall.Stat_t
if err := syscall.Fstat(fd, &st); err != nil {
return 0, false
}
return int(st.Size), st.Size > 0
}
// currentFDsDirwalk returns the number of open file descriptors by
// walking /proc/self/fd. It works on all kernels but is O(n) in the
// number of open file descriptors.
func currentFDsDirwalk() int {
c := counterPool.Get().(*counter)
defer counterPool.Put(c)
c.n = 0
dirwalk.WalkShallow(mem.S("/proc/self/fd"), c.cb)
return c.n
}
func currentFDs() int {
if n, ok := currentFDsFast(); ok {
return n
}
return currentFDsDirwalk()
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package metrics
import "testing"
func TestCurrentFDsFast(t *testing.T) {
fast, ok := currentFDsFast()
if !ok {
t.Skip("fast path unavailable; kernel older than 6.2")
}
// The dirwalk count includes the directory file descriptor that
// the walk itself has open, so it reports one more than the fast
// path does.
slow := currentFDsDirwalk() - 1
if fast != slow {
t.Errorf("currentFDsFast = %v; currentFDsDirwalk-1 = %v", fast, slow)
}
}