Repository navigation
Description
The idea
The gofer client assigns inode numbers by recording every host (device, inode) pair it ever sees in filesystem.inoByKey. That includes every looked-up dentry and every getdents entry, even entries that are never stat'ed. The map is never pruned. It costs about 55 B of Sentry heap per file (about 100 B of RSS with GC headroom), and it sits behind a global mutex. A find over a 1M-file volume leaves master's Sentry about 100 MiB larger for the life of the sandbox. A workspace on a host tmpfs (e.g. an emptyDir with medium: Memory) never reuses inode numbers, so long-running agents grow without bound there. This was discussed in #6665 and #10047. #7801 tried passing host inode numbers through with remapped devices, but was dropped over save/restore complexity.
gVisor could instead compute inode numbers statelessly, splitting the space into two ranges below 2^53
- Host inode numbers below 2^52 go through a keyed per-device bijection onto [2^52, 2^53), with a random per-filesystem key. Distinct files on the same host device can never collide.
- Synthetic and other sequentially allocated numbers stay below 2^52. Host and synthetic numbers are therefore disjoint by construction.
- Only files on different host devices within one mount (nested mounts, btrfs subvolumes) can collide, with probability at most about 2^-52 per pair.
- The rare host inode numbers of 2^52 or more (e.g. overlayfs xino) fall back to today's map.
Why 2^52? That is the max size for JavaScript Number types before they lose precision, so it is the boundary after which node.js apps may start to degrade in behavior.
Save/restore is simple: nothing is saved, and the key is regenerated on restore. Dentries alive at checkpoint keep their numbers through the existing remap, and those numbers are reserved. Other files get new numbers after restore, as they do today. #10047's cross-pod stability remains unaddressed.
Performance impact
- Sentry memory after a scan of a 1M-file bind mount (
find,du,ls -lR, 8 parallelfinds,tar): about 122 MiB → 19.5 MiB of Sentry anonymous memory (-84%), and 127 MiB → 24 MiB of non-reclaimable cgroup memory (-81%). It is -85%/-82% at 2 vCPUs and -80%/-75% at 48. The saving lasts for the life of the sandbox. After checkpoint/restore both arms start at about 10 MiB, but this regrows to 124 MiB on the next scan on the current gVisor build, versus 21 MiB with this change. git cloneof linux v6.12 (92k entries) into a bind mount: Sentry memory -27% (28.9 → 21.2 MiB). Cloned into the rootfs, where files live in the overlay's upper layer, it is unchanged.- Agent loop on a host-tmpfs workspace (e.g. an emptyDir with
medium: Memory), copying, searching and deleting a 10k-file tree: master grows with every cycle because tmpfs never reuses inode numbers. Sentry memory is unchanged after 1 cycle, then -8%, -19% and -36% after 5, 20 and 50 cycles. Typical dev workloads (rubydev spec, abseil build) are unchanged within noise. - Large-tree scans are 2-3% faster at 2 and 8 vCPUs (not significant at 48), the post-restore re-scan is 7% faster, and per-entry getdents on a bind mount is 3-6% faster. No
//test/benchmarksmetric changes at p<0.01: startup, checkpoint/restore, size, usage memory, syscallbench, fio, rubydev and bazel ABSL. - The systrap, ptrace, KVM and save/restore syscall suites pass, as do container, docker, overlay and fsstress tests.
- Visible change: gofer inode numbers become large pseudo-random values in [2^52, 2^53).
Beat the autoresearch
The following benchmark results were benchmarked using this patch: master...EtiennePerot:gvisor:autoresearch/gofer-ino-hash-52
Add value by beating these benchmark numbers, or reducing any regressions they cause, or both
goos: linux
goarch: amd64
pkg: 1a-memory: 1M-file bind-mount scans, c4-standard-8
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
find 122.07Mi ± 1% 19.55Mi ± 4% -83.98% (p=0.001 n=7)
du 121.42Mi ± 2% 19.28Mi ± 1% -84.12% (p=0.002 n=6)
lsl 122.49Mi ± 2% 19.35Mi ± 2% -84.20% (p=0.002 n=6)
pfind 125.37Mi ± 1% 22.27Mi ± 2% -82.23% (p=0.001 n=7)
tar 123.64Mi ± 1% 19.55Mi ± 6% -84.19% (p=0.001 n=7)
geomean 123.0Mi 19.97Mi -83.76%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
find 127.12Mi ± 1% 24.38Mi ± 3% -80.82% (p=0.001 n=7)
du 126.11Mi ± 2% 23.74Mi ± 2% -81.17% (p=0.002 n=6)
lsl 127.64Mi ± 2% 24.19Mi ± 2% -81.05% (p=0.002 n=6)
pfind 131.78Mi ± 1% 28.35Mi ± 2% -78.49% (p=0.001 n=7)
tar 128.94Mi ± 1% 24.51Mi ± 6% -80.99% (p=0.001 n=7)
geomean 128.3Mi 24.98Mi -80.53%
pkg: 1b-memory: 1M-file bind-mount scans, c4-standard-2
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
find 121.01Mi ± 1% 17.72Mi ± 4% -85.35% (p=0.000 n=10)
du 120.57Mi ± 1% 17.26Mi ± 2% -85.68% (p=0.000 n=8)
lsl 121.44Mi ± 1% 17.73Mi ± 1% -85.40% (p=0.000 n=14)
pfind 124.78Mi ± 1% 19.16Mi ± 2% -84.64% (p=0.000 n=10)
geomean 121.9Mi 17.96Mi -85.27%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
find 125.51Mi ± 1% 22.05Mi ± 3% -82.43% (p=0.000 n=10)
du 124.80Mi ± 1% 21.29Mi ± 2% -82.94% (p=0.000 n=8)
lsl 125.96Mi ± 1% 22.08Mi ± 1% -82.47% (p=0.000 n=14)
pfind 130.38Mi ± 1% 24.65Mi ± 1% -81.09% (p=0.000 n=10)
geomean 126.6Mi 22.48Mi -82.25%
pkg: 1c-memory: 1M-file bind-mount scans, c4-standard-48
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
find 127.07Mi ± 1% 25.33Mi ± 2% -80.07% (p=0.000 n=16)
du 127.42Mi ± 1% 24.71Mi ± 1% -80.61% (p=0.000 n=16)
lsl 126.72Mi ± 1% 25.77Mi ± 2% -79.66% (p=0.000 n=16)
pfind 131.24Mi ± 1% 28.69Mi ± 4% -78.14% (p=0.000 n=16)
geomean 128.1Mi 26.08Mi -79.64%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
find 135.41Mi ± 1% 34.04Mi ± 3% -74.86% (p=0.000 n=16)
du 135.52Mi ± 1% 33.05Mi ± 1% -75.61% (p=0.000 n=16)
lsl 135.81Mi ± 1% 34.73Mi ± 2% -74.43% (p=0.000 n=16)
pfind 141.80Mi ± 1% 40.04Mi ± 2% -71.76% (p=0.000 n=16)
geomean 137.1Mi 35.37Mi -74.21%
pkg: 1d-memory: checkpoint-restore of a 1M-file find, c4-standard-8
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
cr/after-scan 122.07Mi ± 1% 19.49Mi ± 1% -84.03% (p=0.000 n=16)
cr/after-restore 10.17Mi ± 4% 10.29Mi ± 5% ~ (p=0.860 n=7+11)
cr/after-rescan 124.40Mi ± 2% 21.11Mi ± 2% -83.03% (p=0.000 n=7+11)
geomean 53.66Mi 16.18Mi -69.84%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
cr/after-scan 127.17Mi ± 1% 24.39Mi ± 1% -80.82% (p=0.000 n=16)
cr/after-restore 14.29Mi ± 4% 14.26Mi ± 5% ~ (p=0.791 n=7+11)
cr/after-rescan 129.37Mi ± 2% 25.96Mi ± 2% -79.93% (p=0.000 n=7+11)
geomean 61.72Mi 20.82Mi -66.26%
pkg: 1e-memory: agent loop on a host-tmpfs workspace, c4-standard-8
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
agenttmp1 29.44Mi ± 5% 29.13Mi ± 5% ~ (p=0.616 n=16)
agenttmp5 31.57Mi ± 16% 29.06Mi ± 9% -7.96% (p=0.009 n=6)
agenttmp20 37.11Mi ± 16% 30.04Mi ± 9% -19.06% (p=0.002 n=6)
agenttmp50 49.06Mi ± 12% 31.31Mi ± 7% -36.19% (p=0.002 n=6)
geomean 36.07Mi 29.87Mi -17.19%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
agenttmp1 34.70Mi ± 4% 34.45Mi ± 4% ~ (p=0.616 n=16)
agenttmp5 36.88Mi ± 14% 34.26Mi ± 8% -7.12% (p=0.009 n=6)
agenttmp20 42.43Mi ± 14% 35.31Mi ± 8% -16.78% (p=0.002 n=6)
agenttmp50 54.42Mi ± 11% 36.73Mi ± 6% -32.51% (p=0.002 n=6)
geomean 41.46Mi 35.17Mi -15.17%
pkg: 1f-memory: git clone of linux v6.12, c4-standard-8
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
gitclone-bind 28.93Mi ± 1% 21.17Mi ± 5% -26.81% (p=0.000 n=16)
gitclone-root 337.6Mi ± 0% 337.9Mi ± 0% ~ (p=0.149 n=16)
geomean 98.82Mi 84.58Mi -14.42%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
gitclone-bind 35.48Mi ± 1% 27.84Mi ± 3% -21.54% (p=0.000 n=16)
gitclone-root 347.9Mi ± 0% 348.3Mi ± 0% ~ (p=0.138 n=16)
geomean 111.1Mi 98.46Mi -11.38%
pkg: 1g-memory: typical dev workloads, c4-standard-8
│ before │ after │
│ sentry-anon-B │ sentry-anon-B vs base │
rubyspec 39.60Mi ± 1% 39.20Mi ± 1% ~ (p=0.086 n=16)
absl 81.32Mi ± 2% 80.05Mi ± 4% -1.56% (p=0.023 n=16)
geomean 56.75Mi 56.02Mi -1.28%
│ before │ after │
│ cgroup-nonreclaimable-B │ cgroup-nonreclaimable-B vs base │
rubyspec 49.76Mi ± 1% 49.45Mi ± 1% ~ (p=0.119 n=16)
absl 516.7Mi ± 1% 507.7Mi ± 3% ~ (p=0.086 n=16)
geomean 160.4Mi 158.4Mi -1.19%
pkg: 2a-time: 1M-file bind-mount scans, c4-standard-8
│ before │ after │
│ sec/op │ sec/op vs base │
find 17.98 ± 3% 17.48 ± 2% -2.79% (p=0.007 n=7)
du 17.84 ± 1% 17.34 ± 2% -2.80% (p=0.002 n=6)
lsl 20.64 ± 2% 20.01 ± 2% -3.07% (p=0.002 n=6)
pfind 18.36 ± 1% 17.73 ± 2% -3.42% (p=0.001 n=7)
tar 45.01 ± 1% 44.01 ± 1% -2.22% (p=0.001 n=7)
geomean 22.27 21.63 -2.86%
pkg: 2b-time: 1M-file bind-mount scans, c4-standard-2
│ before │ after │
│ sec/op │ sec/op vs base │
find 21.26 ± 1% 20.87 ± 2% -1.83% (p=0.003 n=10)
du 20.70 ± 2% 20.11 ± 2% -2.85% (p=0.005 n=8)
lsl 24.83 ± 2% 24.35 ± 1% -1.93% (p=0.006 n=14)
pfind 19.15 ± 2% 18.56 ± 3% -3.07% (p=0.007 n=10)
geomean 21.39 20.87 -2.42%
pkg: 2c-time: 1M-file bind-mount scans, c4-standard-48
│ before │ after │
│ sec/op │ sec/op vs base │
find 16.25 ± 2% 15.74 ± 4% -3.14% (p=0.047 n=16)
du 15.91 ± 2% 15.62 ± 2% ~ (p=0.094 n=16)
lsl 17.95 ± 3% 17.59 ± 3% ~ (p=0.210 n=16)
pfind 17.12 ± 1% 17.16 ± 5% ~ (p=0.515 n=16)
geomean 16.79 16.50 -1.69%
pkg: 2d-time: checkpoint-restore of a 1M-file find, c4-standard-8
│ before │ after │
│ sec/op │ sec/op vs base │
cr/after-scan 19.34 ± 1% 18.51 ± 2% -4.30% (p=0.000 n=16)
cr/after-rescan 12.63 ± 3% 11.73 ± 2% -7.07% (p=0.000 n=7+11)
geomean 15.63 14.74 -5.69%
pkg: 2e-time: agent loop on a host-tmpfs workspace, c4-standard-8
│ before │ after │
│ sec/op │ sec/op vs base │
agenttmp1 5.633 ± 1% 5.634 ± 1% ~ (p=0.780 n=16)
agenttmp5 21.18 ± 1% 21.01 ± 1% ~ (p=0.240 n=6)
agenttmp20 79.63 ± 2% 79.88 ± 1% ~ (p=0.240 n=6)
agenttmp50 195.6 ± 2% 195.4 ± 1% ~ (p=1.000 n=6)
geomean 36.92 36.87 -0.15%
pkg: 2f-time: git clone of linux v6.12, c4-standard-8
│ before │ after │
│ sec/op │ sec/op vs base │
gitclone-bind 35.19 ± 1% 35.30 ± 1% ~ (p=0.838 n=16)
gitclone-root 36.03 ± 2% 35.97 ± 2% ~ (p=0.926 n=16)
geomean 35.61 35.63 +0.07%
pkg: 2g-time: typical dev workloads, c4-standard-8
│ before │ after │
│ sec/op │ sec/op vs base │
rubyspec 28.31 ± 1% 28.37 ± 1% ~ (p=0.780 n=16)
absl 62.64 ± 2% 61.45 ± 2% ~ (p=0.196 n=16)
geomean 42.11 41.75 -0.85%
pkg: gvisor.dev/gvisor/test/benchmarks/base
│ before │ after │
│ sec/op │ sec/op vs base │
StartupEmpty 192.4m ± 2% 192.2m ± 2% ~ (p=0.956 n=16)
StartupNginx 510.5m ± 2% 510.3m ± 1% ~ (p=0.724 n=16)
StartupNode 641.0m ± 1% 639.2m ± 2% ~ (p=0.838 n=16)
CheckpointEmpty 73.15m ± 4% 74.46m ± 5% ~ (p=0.287 n=16)
RestoreEmpty 204.5m ± 3% 202.5m ± 2% ~ (p=0.323 n=16)
CheckpointNginx 109.0m ± 3% 111.6m ± 2% ~ (p=0.184 n=16)
RestoreNginx 255.4m ± 2% 254.7m ± 1% ~ (p=0.985 n=16)
SizeEmpty 392.9m ± 1% 391.2m ± 2% ~ (p=0.867 n=16)
SizeNginx 798.0m ± 1% 791.7m ± 2% ~ (p=0.752 n=16)
SizeNode 914.4m ± 1% 914.6m ± 2% ~ (p=0.669 n=16)
UsageEmpty 2.104 ± 0% 2.101 ± 0% ~ (p=0.224 n=16)
UsageNginx 2.146 ± 0% 2.146 ± 0% ~ (p=0.956 n=16)
UsageNode 2.135 ± 0% 2.132 ± 0% ~ (p=0.993 n=16)
Syscallbench/syscall.getpid 3.086µ ± 3% 3.059µ ± 2% ~ (p=0.926 n=16)
Syscallbench/syscall.getpidopt 1.109µ ± 4% 1.086µ ± 4% ~ (p=0.889 n=16)
SyscallUnderSeccomp/cacheable.false 1.120µ ± 4% 1.104µ ± 3% ~ (p=0.450 n=16)
SyscallUnderSeccomp/cacheable.true 1.106µ ± 4% 1.086µ ± 3% ~ (p=0.071 n=16)
geomean 24.08m 24.00m -0.30%
│ before │ after │
│ average_container_size-B │ average_container_size-B vs base │
SizeEmpty 20.28Mi ± 19% 19.10Mi ± 9% ~ (p=0.323 n=16)
SizeNginx 30.97Mi ± 5% 32.62Mi ± 11% ~ (p=0.381 n=16)
SizeNode 55.78Mi ± 4% 55.15Mi ± 5% ~ (p=0.305 n=16)
geomean 32.72Mi 32.51Mi -0.65%
│ before │ after │
│ average_container_memory_usage-B │ average_container_memory_usage-B vs base │
UsageEmpty 14.20Mi ± 1% 14.11Mi ± 1% ~ (p=0.254 n=16)
UsageNginx 22.85Mi ± 0% 22.82Mi ± 1% ~ (p=0.239 n=16)
UsageNode 46.46Mi ± 0% 46.38Mi ± 1% ~ (p=0.128 n=16)
geomean 24.70Mi 24.62Mi -0.32%
pkg: gvisor.dev/gvisor/test/benchmarks/fs
│ before │ after │
│ sec/op │ sec/op vs base │
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 3.066m ± 3% 3.113m ± 3% ~ (p=0.254 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 3.132m ± 2% 3.175m ± 3% ~ (p=0.224 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 3.246m ± 2% 3.280m ± 1% ~ (p=0.254 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 6.719m ± 3% 6.665m ± 2% ~ (p=0.468 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 4.929m ± 1% 4.931m ± 2% ~ (p=0.780 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 6.639m ± 3% 6.674m ± 4% ~ (p=0.119 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 35.77m ± 1% 35.85m ± 0% ~ (p=0.270 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 19.02m ± 0% 19.06m ± 1% ~ (p=0.867 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 19.63m ± 1% 19.55m ± 1% ~ (p=0.724 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 19.19m ± 1% 19.20m ± 2% ~ (p=0.669 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 19.72m ± 1% 19.80m ± 1% ~ (p=0.287 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 19.15m ± 1% 19.25m ± 1% ~ (p=0.468 n=16)
RubyNoOpTest/page_cache.clean/filesystem.bindfs 716.6m ± 7% 686.6m ± 4% -4.19% (p=0.035 n=16)
RubyNoOpTest/page_cache.dirty/filesystem.bindfs 274.4m ± 2% 267.6m ± 4% ~ (p=0.056 n=16)
RubyNoOpTest/page_cache.clean/filesystem.tmpfs 665.3m ± 4% 680.0m ± 4% ~ (p=0.897 n=16)
RubyNoOpTest/page_cache.dirty/filesystem.tmpfs 273.6m ± 4% 269.4m ± 3% ~ (p=0.897 n=16)
RubyNoOpTest/page_cache.clean/filesystem.rootfs 680.6m ± 3% 690.9m ± 5% ~ (p=0.445 n=16)
RubyNoOpTest/page_cache.dirty/filesystem.rootfs 265.7m ± 5% 271.1m ± 5% ~ (p=0.196 n=16)
RubySpecTest/page_cache.clean/filesystem.bindfs 27.09 ± 4% 27.08 ± 4% ~ (p=0.780 n=16)
RubySpecTest/page_cache.dirty/filesystem.bindfs 25.19 ± 5% 25.12 ± 5% ~ (p=0.780 n=16)
RubySpecTest/page_cache.clean/filesystem.tmpfs 25.77 ± 4% 25.94 ± 4% ~ (p=0.696 n=16)
RubySpecTest/page_cache.dirty/filesystem.tmpfs 24.52 ± 5% 24.53 ± 5% ~ (p=0.445 n=16)
RubySpecTest/page_cache.clean/filesystem.rootfs 26.49 ± 5% 26.49 ± 4% ~ (p=0.539 n=16)
RubySpecTest/page_cache.dirty/filesystem.rootfs 25.14 ± 5% 25.19 ± 5% ~ (p=0.590 n=16)
BuildABSL/page_cache.clean/filesystem.bindfs 36.58 ± 2% 36.63 ± 4% ~ (p=0.616 n=16)
BuildABSL/page_cache.dirty/filesystem.bindfs 36.42 ± 2% 36.63 ± 1% ~ (p=0.128 n=16)
BuildABSL/page_cache.clean/filesystem.tmpfs 36.90 ± 1% 36.77 ± 3% ~ (p=0.752 n=16)
BuildABSL/page_cache.dirty/filesystem.tmpfs 36.48 ± 2% 36.07 ± 2% ~ (p=0.423 n=16)
BuildABSL/page_cache.clean/filesystem.rootfs 36.74 ± 1% 36.82 ± 1% ~ (p=0.642 n=16)
BuildABSL/page_cache.dirty/filesystem.rootfs 36.33 ± 2% 35.97 ± 1% ~ (p=0.094 n=16)
geomean 519.6m 519.8m +0.03%
│ before │ after │
│ bandwidth.bytes_per_second │ bandwidth.bytes_per_second vs base │
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 1.076G ± 2% 1.061G ± 2% ~ (p=0.154 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 1.463G ± 2% 1.471G ± 1% ~ (p=0.315 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 902.3M ± 2% 905.3M ± 1% ~ (p=0.325 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 220.6M ± 4% 224.3M ± 4% ~ (p=0.802 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 357.0M ± 1% 353.4M ± 1% ~ (p=0.099 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 224.2M ± 4% 224.1M ± 4% ~ (p=0.283 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 33.14M ± 0% 33.05M ± 0% ~ (p=0.162 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 70.66M ± 1% 70.62M ± 1% ~ (p=0.874 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 68.25M ± 1% 68.41M ± 1% ~ (p=0.570 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 69.98M ± 1% 70.22M ± 2% ~ (p=0.919 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 69.11M ± 1% 69.13M ± 1% ~ (p=0.918 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 70.55M ± 1% 70.36M ± 1% ~ (p=0.356 n=16)
geomean 182.5M 182.5M +0.01%
│ before │ after │
│ bandwidth_all_jobs.bytes_per_second │ bandwidth_all_jobs.bytes_per_second vs base │
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 1.076G ± 2% 1.061G ± 2% ~ (p=0.154 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 1.463G ± 2% 1.471G ± 1% ~ (p=0.315 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 902.3M ± 2% 905.3M ± 1% ~ (p=0.325 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 220.6M ± 4% 224.3M ± 4% ~ (p=0.802 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 357.0M ± 1% 353.4M ± 1% ~ (p=0.099 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 224.2M ± 4% 224.1M ± 4% ~ (p=0.283 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 265.6M ± 0% 265.0M ± 0% ~ (p=0.224 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 564.7M ± 1% 564.6M ± 1% ~ (p=0.993 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 544.2M ± 1% 547.3M ± 1% ~ (p=0.491 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 560.5M ± 2% 558.0M ± 2% ~ (p=0.956 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 552.5M ± 1% 554.3M ± 1% ~ (p=0.809 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 564.9M ± 1% 562.0M ± 1% ~ (p=0.270 n=16)
geomean 516.2M 516.1M -0.02%
│ before │ after │
│ io_ops.ops_per_second │ io_ops.ops_per_second vs base │
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 262.7k ± 2% 259.0k ± 2% ~ (p=0.154 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 357.1k ± 2% 359.1k ± 1% ~ (p=0.315 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 220.3k ± 2% 221.0k ± 1% ~ (p=0.325 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 53.85k ± 4% 54.75k ± 4% ~ (p=0.802 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 87.15k ± 1% 86.29k ± 1% ~ (p=0.099 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 54.73k ± 4% 54.70k ± 4% ~ (p=0.283 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 8.091k ± 0% 8.068k ± 0% ~ (p=0.162 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 17.25k ± 1% 17.24k ± 1% ~ (p=0.874 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 16.66k ± 1% 16.70k ± 1% ~ (p=0.570 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 17.08k ± 1% 17.14k ± 2% ~ (p=0.919 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 16.87k ± 1% 16.88k ± 1% ~ (p=0.918 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 17.22k ± 1% 17.18k ± 1% ~ (p=0.356 n=16)
geomean 44.56k 44.56k +0.01%
│ before │ after │
│ io_ops_all_jobs.ops_per_second │ io_ops_all_jobs.ops_per_second vs base │
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 262.7k ± 2% 259.0k ± 2% ~ (p=0.154 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 357.1k ± 2% 359.1k ± 1% ~ (p=0.315 n=16)
FioWrite/operation.write/ioEngine.sync/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 220.3k ± 2% 221.0k ± 1% ~ (p=0.325 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.bindfs 53.85k ± 4% 54.75k ± 4% ~ (p=0.802 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.tmpfs 87.15k ± 1% 86.29k ± 1% ~ (p=0.099 n=16)
FioRead/operation.read/ioEngine.libaio/jobs.1/blockSize.4K/directIO.false/filesystem.rootfs 54.73k ± 4% 54.70k ± 4% ~ (p=0.283 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 64.83k ± 0% 64.69k ± 0% ~ (p=0.220 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 137.9k ± 1% 137.8k ± 1% ~ (p=0.985 n=16)
FioRandRead/operation.randread/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 132.9k ± 1% 133.6k ± 1% ~ (p=0.491 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.bindfs 136.8k ± 2% 136.2k ± 2% ~ (p=0.956 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.tmpfs 134.9k ± 1% 135.3k ± 1% ~ (p=0.801 n=16)
FioRandWrite/operation.randwrite/ioEngine.libaio/jobs.8/blockSize.4K/directIO.true/filesystem.rootfs 137.9k ± 1% 137.2k ± 1% ~ (p=0.266 n=16)
geomean 126.0k 126.0k -0.02%
│ before │ after │
│ load.ns │ load.ns vs base │
RubySpecTest/page_cache.clean/filesystem.bindfs 5.135G ± 3% 5.120G ± 3% ~ (p=0.648 n=16)
RubySpecTest/page_cache.dirty/filesystem.bindfs 3.940G ± 4% 3.955G ± 5% ~ (p=0.557 n=16)
RubySpecTest/page_cache.clean/filesystem.tmpfs 4.615G ± 4% 4.605G ± 3% ~ (p=0.831 n=16)
RubySpecTest/page_cache.dirty/filesystem.tmpfs 3.810G ± 4% 3.815G ± 5% ~ (p=0.933 n=16)
RubySpecTest/page_cache.clean/filesystem.rootfs 4.695G ± 4% 4.685G ± 4% ~ (p=0.978 n=16)
RubySpecTest/page_cache.dirty/filesystem.rootfs 3.905G ± 4% 3.920G ± 6% ~ (p=0.904 n=16)
geomean 4.322G 4.324G +0.03%
pkg: gvisor.dev/gvisor/test/perf/linux (bind mount, c4-standard-8)
│ before │ after │
│ sec/op │ sec/op vs base │
GetdentsSameFD/1 13.54µ ± 2% 13.46µ ± 3% ~ (p=0.393 n=10)
GetdentsSameFD/8 2.454µ ± 0% 2.422µ ± 2% ~ (p=0.138 n=10)
GetdentsSameFD/64 1.089µ ± 2% 1.060µ ± 1% -2.66% (p=0.000 n=10)
GetdentsSameFD/512 820.0n ± 1% 790.0n ± 1% -3.66% (p=0.000 n=10)
GetdentsSameFD/4096 947.0n ± 1% 895.0n ± 2% -5.49% (p=0.000 n=10)
GetdentsNewFD/1 18.53µ ± 1% 18.72µ ± 2% ~ (p=0.063 n=10)
GetdentsNewFD/8 3.077µ ± 2% 3.073µ ± 2% ~ (p=0.403 n=10)
GetdentsNewFD/64 1.178µ ± 2% 1.145µ ± 3% -2.84% (p=0.010 n=10)
GetdentsNewFD/512 839.5n ± 1% 803.5n ± 1% -4.29% (p=0.000 n=10)
GetdentsNewFD/4096 952.0n ± 1% 894.5n ± 2% -6.04% (p=0.000 n=10)
Stat/1 5.498µ ± 2% 5.478µ ± 2% ~ (p=0.796 n=10)
Stat/8 9.937µ ± 2% 10.026µ ± 2% ~ (p=0.529 n=10)
Stat/64 52.43µ ± 2% 52.43µ ± 2% ~ (p=0.912 n=10)
Stat/100 75.11µ ± 4% 75.48µ ± 1% ~ (p=0.393 n=10)
geomean 4.055µ 3.981µ -1.81%
Benchmark environment
GCE C4 VMs, COS cos-129-19506-448-8 (kernel 6.12), hyperdisk-balanced, docker 27.4.1, runsc with default flags (systrap, directfs, rootfs overlay). before is master at 2511bc60218a, and after is the patch above.
The 1x-memory/2x-time packages come from a harness that starts a fresh container per sample with page caches dropped first. It runs the workload, then records the runsc-sandbox process's RssAnon (sentry-anon-B) and the container cgroup's non-reclaimable memory, memory.current − (file − shmem) − slab_reclaimable (cgroup-nonreclaimable-B). The workloads:
- Scans:
find,du -s,ls -lR, 8 parallelfinds, andtar cf -over 1,001,001 empty files in a bind-mounted host ext4 directory. cr: the samefind, thendocker checkpoint+ restore, then thefindagain. Some restores failed on both arms in docker's checkpoint store, hence the uneven n.agenttmpN: N cycles of copy,grep -r, and delete of the rubydev image's fastlane tree (about 10k files) on a host tmpfs bind mount.gitclone-*:git clone --no-local --depth 1 --branch v6.12from a read-only local bare mirror of torvalds/linux, into a bind mount or the rootfs. A variant that also ranfindandgit statusafter the clone showed the same picture, so it is omitted.- Typical dev workloads: the rubydev fastlane spec suite, and
bazel build absl/base/...in the rootfs.
The test/perf/linux rows are the upstream getdents/stat benchmarks run on a bind mount (n=10, arms rotated). The test/benchmarks rows are the upstream binaries on c4-standard-16, built from master plus harness-only fixes (SizeEmpty cleanup, fio aggregates); the fusefs variants fail on both arms in this harness and are omitted. Every sample is one invocation, with arms interleaved. Memory cells stop once every memory metric has p<0.01 (n≥6) or n=16. The other cells stop once every metric has p<0.01 or n=16.
Activity
EtiennePerot commentedon Oct 22, 2026
@ayushr2 Would like your take on this one.
ayushr2 commentedon Oct 22, 2026
This would be nice. Yeah I can take this.
ayushr2 commentedon Oct 26, 2026
@EtiennePerot I think the proposal still doesn't get rid of O(N) stat calls per
getdents64(2).Now that
--overlay2=root:selfis default, all rootfs upper inode numbers are preserved across checkpoint/restore. Maybe we should just allow the inode number of cached dentries to also change across checkpoint/restore. As you mentioned, they already change for dentries which are not part of checkpoint (any dentry on which we don't have an open FD).EtiennePerot commentedon Oct 26, 2026
I'm not fully understanding. Does allowing upper layer inode numbers to change get rid of the O(n) state calls in getdents?
ayushr2 commentedon Oct 26, 2026
As of right now, for fsimpl/gofer, the inode numbers are only preserved for dentries captured in the checkpoint (i.e. dentries that have refs>1, so open FDs etc). Other files have their inode numbers changed after restore. I'm suggesting that we drop the requirement for "have same inode number for fsimpl/gofer files that have open FDs on them". It already feels "partially supported". This will allow us to passthrough host inode numbers and just virtualize host device IDs (much smaller map). If we're passing through host inode numbers, then getdents(2) no longer needs to
stat(2)all the dirents returned by the getdents(2) syscall.The upper layer helps because the upper layer is tmpfs, which preserves inode numbers through-and-through for all files on itself. So most applications will not hit the case of "inode number changed for a file on which I had an open FD" since most files they'll be working with are on upper layer. It will however still impact:
But IIUC, this is how CRIU works as well and doesn't preserve inode numbers across checkpoint/restore.
EtiennePerot commentedon Oct 26, 2026
Just to re-summarize to make sure I get it right:
stat(2)syscalls ingetdents.If so, LGTM, though I'll note that this will break Go's
os.SameFile(which I'm only mentioning because I just used it in #16412) when used on lower-layer files. Maybe that's OK if it's already partially-broken. I'm sure eventually we'll need a flag to control inode change policy, but until we do, changing it SGTM.What I'm still confused by is how this relates to the current proposal. Are you suggesting that don't do any of the proposal, and instead limit
inoByKeyto upper-layer files only to limit its size?ayushr2 commentedon Oct 26, 2026
Present state:
Your proposal:
Upper layer always preserves inode numbers, because it's tmpfs.
ayushr2 commentedon Oct 26, 2026
Doing "inode passthrough" in fsimpl/gofer allows us to completely drop ever-growing
inoByKeymap. AND also prevents the stat(2) call on all dirents during getdents(2).EtiennePerot commentedon Oct 26, 2026
OK. SGTM, though if it's not too difficult, maybe flag-gate that (with the flag default being to drop the map) so that it's possible for users to revert to the old behavior if they find themselves depending on it, without having to patch it back in. Then drop it a few weeks later if no complaints.
ayushr2 commentedon Oct 26, 2026
SGTM
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client
Pass host inode numbers through the gofer client