From 1e95ec8ad5e6455907bd8d3565c124d3cd092f2d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 9 Sep 2026 00:41:41 +0000 Subject: [PATCH] metrics: use Linux 6.2+ stat fast path for CurrentFDs As of Linux 6.2 (torvalds/linux@f1f1f2569901ec5b9d425f2e91c09a0e320768f3), 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 Change-Id: I1742db2ae1938c77f9b7d137b167ab4a2b6b6167 --- metrics/fds_linux.go | 37 ++++++++++++++++++++++++++++++++++++- metrics/fds_linux_test.go | 20 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 metrics/fds_linux_test.go diff --git a/metrics/fds_linux.go b/metrics/fds_linux.go index b0abf946b..bcd04ecd7 100644 --- a/metrics/fds_linux.go +++ b/metrics/fds_linux.go @@ -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() +} diff --git a/metrics/fds_linux_test.go b/metrics/fds_linux_test.go new file mode 100644 index 000000000..bb9747a5b --- /dev/null +++ b/metrics/fds_linux_test.go @@ -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) + } +}