Skip to content

Optimization: Derive gofer inode numbers from host keys instead of the ever-growing inoByKey map #16197

Description

@EtiennePerot

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 parallel finds, 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 clone of 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/benchmarks metric 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 parallel finds, and tar cf - over 1,001,001 empty files in a bind-mounted host ext4 directory.
  • cr: the same find, then docker checkpoint + restore, then the find again. 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.12 from a read-only local bare mirror of torvalds/linux, into a bind mount or the rootfs. A variant that also ran find and git status after 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

  1. added this to the Lightspeed milestone on Oct 22, 2026
  2. EtiennePerot commented on Oct 22, 2026

    @EtiennePerot
    CollaboratorAuthor

    @ayushr2 Would like your take on this one.

  3. ayushr2 commented on Oct 22, 2026

    @ayushr2
    Collaborator

    This would be nice. Yeah I can take this.

  4. ayushr2 commented on Oct 26, 2026

    @ayushr2
    Collaborator

    @EtiennePerot I think the proposal still doesn't get rid of O(N) stat calls per getdents64(2).

    Now that --overlay2=root:self is 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).

  5. EtiennePerot commented on Oct 26, 2026

    @EtiennePerot
    CollaboratorAuthor

    I'm not fully understanding. Does allowing upper layer inode numbers to change get rid of the O(n) state calls in getdents?

  6. ayushr2 commented on Oct 26, 2026

    @ayushr2
    Collaborator

    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:

    • files on lower layer which application has read-only FDs on
    • files on bind mounts

    But IIUC, this is how CRIU works as well and doesn't preserve inode numbers across checkpoint/restore.

  7. EtiennePerot commented on Oct 26, 2026

    @EtiennePerot
    CollaboratorAuthor

    Just to re-summarize to make sure I get it right:

    • Present state:
      • Lower layer inode numbers with no open ref do change across C/R
      • Upper layer inode numbers with no open ref do change across C/R
      • Lower layer inode numbers with open ref do not change across C/R
      • Upper layer inode numbers with open ref do not change across C/R
    • Your proposal:
      • Lower layer inode numbers with no open ref do change across C/R
      • Upper layer inode numbers with no open ref do change across C/R
      • Lower layer inode numbers with open ref do change across C/R
      • Upper layer inode numbers with open ref do not change across C/R
    • CRIU already changes inode numbers across C/R in all cases.
    • Removing inode number persistence for no-ref lower-layer inodes will get rid of stat(2) syscalls in getdents.

    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 inoByKey to upper-layer files only to limit its size?

  8. ayushr2 commented on Oct 26, 2026

    @ayushr2
    Collaborator

    Present state:

    • Lower layer inode numbers with no open ref do change across C/R
    • Upper layer inode numbers with no open ref do not change across C/R
    • Lower layer inode numbers with open ref do not change across C/R
    • Upper layer inode numbers with open ref do not change across C/R

    Your proposal:

    • Lower layer inode numbers with no open ref do change across C/R
    • Upper layer inode numbers with no open ref do not change across C/R
    • Lower layer inode numbers with open ref do change across C/R
    • Upper layer inode numbers with open ref do not change across C/R

    Upper layer always preserves inode numbers, because it's tmpfs.

  9. ayushr2 commented on Oct 26, 2026

    @ayushr2
    Collaborator

    What I'm still confused by is how this relates to the current proposal.

    Doing "inode passthrough" in fsimpl/gofer allows us to completely drop ever-growing inoByKey map. AND also prevents the stat(2) call on all dirents during getdents(2).

  10. EtiennePerot commented on Oct 26, 2026

    @EtiennePerot
    CollaboratorAuthor

    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.

  11. ayushr2 commented on Oct 26, 2026

    @ayushr2
    Collaborator

    SGTM

  12. added 5 commits that reference this issue on Oct 28, 2026
    aa69c5a
    d2ab214
    36dde11
    fcb35e7
    271e52f
  13. added 3 commits that reference this issue on Oct 28, 2026
    ad1d273
    fb3b157
    45e321e
  14. added a commit that references this issue on Oct 28, 2026
    8ead67b
  15. added 6 commits that reference this issue on Oct 28, 2026
    29d9c4f
    70f02c5
    39e2473
    5786f93
    e02f706
    2679420
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: performanceIssue related to performance & benchmarks

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions