The idea
The fdbased recvMMsgDispatcher reads up to MaxMsgsPerRecv (8) packets per recvmmsg, and each slot holds a receive buffer sized for a GSO packet (about 64 KiB). On its first read, the dispatcher fills all 8 slots, and the buffer pools zero every chunk. So every sandbox keeps about 512 KiB of zero-buffers for its whole lifetime, even when they don't use any networking. Even when they do use networking, request/response traffic rarely has more than one packet queued per read, so slots 1-7 never receive data.
The dispatcher could instead start with a single active slot, and pass only the active prefix to recvmmsg. Slots are already filled lazily, so inactive slots hold no buffers. When a read fills every active slot without having to poll first (packets are queuing faster than they are read), the active count would double, and stop doubling at 8. After many consecutive reads that don't fill every active slot (e.g. 256), it would drop back to one slot and release the other slots' buffers. This needs BlockingRecvMMsgUntilStopped to report whether it polled, and the runsc seccomp rule to allow vlen <= MaxMsgsPerRecv instead of vlen == MaxMsgsPerRecv.
Performance impact
//test/benchmarks/base:usage_testcontainer memory: -3.3% (Empty), -2.8% (Nginx), -1.0% (Node), p < 0.001.- An nginx sandbox uses about 350 KiB less anonymous memory after boot, after small HTTP requests, and after checkpoint/restore.
- After a bulk download the slots grow to 8, so memory matches today's (by design).
- Bulk receive throughput and sandbox CPU per GiB show no measurable change in any of 14 iperf3 conditions: CPU quota 0.25/1/none, 1 or 4 streams, GSO on or off, MTU-sized packets.
//test/benchmarksiperf, nginx, httpd, redis, node, ruby, startup and size: no metric changes at p < 0.01.
Beat the autoresearch
The following benchmark results were benchmarked using this patch: https://github.com/google/gvisor/compare/master...EtiennePerot:gvisor:autoresearch/recv-buffer-slots
Add value by beating these benchmark numbers, or reducing any regressions they cause, or both
goos: linux
goarch: amd64
cpu: INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
pkg: gvisor.dev/gvisor/test/benchmarks/base
│ before │ after │
│ average_container_memory_usage.bytes │ average_container_memory_usage.bytes vs base │
SizeEmpty-16 14.94M ± 1% 14.45M ± 1% -3.30% (p=0.000 n=16)
SizeNginx-16 23.94M ± 1% 23.27M ± 1% -2.80% (p=0.000 n=16)
SizeNode-16 48.75M ± 0% 48.28M ± 0% -0.97% (p=0.000 n=16)
geomean 25.93M 25.32M -2.36%
pkg: gvisor.dev/gvisor/test/benchmarks/database
│ before │ after │
│ sec/op │ sec/op vs base │
Redis/operation.SET-16 32.36µ ± 2% 32.26µ ± 1% ~ (p=0.867 n=16)
Redis/operation.LPUSH-16 32.54µ ± 2% 32.01µ ± 2% ~ (p=0.283 n=16)
Redis/operation.LRANGE_100-16 50.36µ ± 1% 50.54µ ± 1% ~ (p=0.696 n=16)
geomean 37.57µ 37.37µ -0.53%
│ before │ after │
│ SET.rps │ SET.rps vs base │
Redis/operation.SET-16 67.23k ± 1% 67.57k ± 1% +0.51% (p=0.013 n=16)
│ before │ after │
│ LPUSH.rps │ LPUSH.rps vs base │
Redis/operation.LPUSH-16 67.45k ± 1% 67.91k ± 1% ~ (p=0.095 n=16)
│ before │ after │
│ LRANGE_100.rps │ LRANGE_100.rps vs base │
Redis/operation.LRANGE_100-16 55.33k ± 1% 55.40k ± 1% ~ (p=0.164 n=16)
pkg: gvisor.dev/gvisor/test/benchmarks/network
│ before │ after │
│ sec/op │ sec/op vs base │
IperfOneConnection/operation.Upload-16 2.569µ ± 38% 1.607µ ± 60% ~ (p=0.157 n=16)
IperfOneConnection/operation.Download-16 1.580µ ± 59% 2.502µ ± 39% ~ (p=0.274 n=16)
IperfManyConnections/operation.Upload/parallel.4-16 2.497µ ± 1% 2.485µ ± 40% ~ (p=0.174 n=16)
IperfManyConnections/operation.Download/parallel.4-16 2.483µ ± 1% 2.476µ ± 1% ~ (p=0.533 n=16)
IperfManyConnections/operation.Upload/parallel.16-16 2.422µ ± 0% 2.422µ ± 0% ~ (p=0.773 n=16)
IperfManyConnections/operation.Download/parallel.16-16 2.515µ ± 1% 2.522µ ± 1% ~ (p=0.151 n=16)
IperfManyConnections/operation.Upload/parallel.64-16 2.430µ ± 0% 2.435µ ± 1% ~ (p=0.831 n=16)
IperfManyConnections/operation.Download/parallel.64-16 2.622µ ± 36% 2.590µ ± 38% ~ (p=0.468 n=16)
NginxDocSize/filesize.10Mb/concurrency.1/filesystem.tmpfs-16 7.515m ± 1% 7.504m ± 1% ~ (p=0.239 n=16)
NginxDocSize/filesize.10Mb/concurrency.100/filesystem.tmpfs-16 7.089m ± 1% 7.018m ± 1% ~ (p=0.086 n=16)
NginxDocSize/filesize.10Mb/concurrency.1000/filesystem.tmpfs-16 6.441m ± 2% 6.381m ± 4% ~ (p=0.196 n=16)
NginxDocSize/filesize.1Kb/concurrency.1/filesystem.tmpfs-16 391.0µ ± 1% 390.7µ ± 3% ~ (p=0.696 n=16)
NginxDocSize/filesize.1Kb/concurrency.100/filesystem.tmpfs-16 327.9µ ± 4% 333.9µ ± 4% ~ (p=0.539 n=16)
NginxDocSize/filesize.1Kb/concurrency.1000/filesystem.tmpfs-16 406.6µ ± 3% 396.3µ ± 5% ~ (p=0.184 n=16)
NginxDocSize/filesize.100Kb/concurrency.1/filesystem.tmpfs-16 497.7µ ± 2% 494.9µ ± 2% ~ (p=0.196 n=16)
NginxDocSize/filesize.100Kb/concurrency.100/filesystem.tmpfs-16 400.7µ ± 3% 394.2µ ± 3% ~ (p=0.254 n=16)
NginxDocSize/filesize.100Kb/concurrency.1000/filesystem.tmpfs-16 584.3µ ± 23% 474.4µ ± 21% ~ (p=0.171 n=16)
NginxDocSize/filesize.10Mb/concurrency.1/filesystem.bind-16 8.159m ± 1% 8.145m ± 1% ~ (p=0.381 n=16)
NginxDocSize/filesize.10Mb/concurrency.100/filesystem.bind-16 7.882m ± 1% 7.837m ± 0% ~ (p=0.149 n=16)
NginxDocSize/filesize.10Mb/concurrency.1000/filesystem.bind-16 7.483m ± 1% 7.553m ± 3% ~ (p=0.669 n=16)
NginxDocSize/filesize.1Kb/concurrency.1/filesystem.bind-16 394.2µ ± 1% 395.6µ ± 2% ~ (p=0.926 n=16)
NginxDocSize/filesize.1Kb/concurrency.100/filesystem.bind-16 336.7µ ± 3% 338.4µ ± 4% ~ (p=0.867 n=16)
NginxDocSize/filesize.1Kb/concurrency.1000/filesystem.bind-16 404.8µ ± 3% 424.2µ ± 5% ~ (p=0.067 n=16)
NginxDocSize/filesize.100Kb/concurrency.1/filesystem.bind-16 510.7µ ± 2% 508.0µ ± 3% ~ (p=0.642 n=16)
NginxDocSize/filesize.100Kb/concurrency.100/filesystem.bind-16 423.2µ ± 2% 411.4µ ± 3% ~ (p=0.051 n=16)
NginxDocSize/filesize.100Kb/concurrency.1000/filesystem.bind-16 473.2µ ± 22% 483.6µ ± 19% ~ (p=0.616 n=16)
geomean 166.1µ 164.6µ -0.92%
│ before │ after │
│ bandwidth.bytes_per_second │ bandwidth.bytes_per_second vs base │
IperfOneConnection/operation.Upload-16 3.443G ± 3% 3.640G ± 9% ~ (p=0.275 n=16)
IperfOneConnection/operation.Download-16 3828.5M ± 78% 834.6M ± 439% ~ (p=0.341 n=16)
IperfManyConnections/operation.Upload/parallel.4-16 1.446G ± 10% 1.446G ± 10% ~ (p=0.669 n=16)
IperfManyConnections/operation.Download/parallel.4-16 1.504G ± 13% 1.435G ± 10% ~ (p=0.838 n=16)
IperfManyConnections/operation.Upload/parallel.16-16 789.9M ± 1% 786.7M ± 1% ~ (p=0.128 n=16)
IperfManyConnections/operation.Download/parallel.16-16 492.0M ± 14% 561.2M ± 12% ~ (p=0.110 n=16)
IperfManyConnections/operation.Upload/parallel.64-16 188.3M ± 3% 194.8M ± 8% +3.44% (p=0.021 n=16)
IperfManyConnections/operation.Download/parallel.64-16 158.4M ± 20% 159.6M ± 19% ~ (p=0.867 n=16)
geomean 871.4M 736.4M -15.48%
│ before │ after │
│ requests_per_second.QPS │ requests_per_second.QPS vs base │
NginxDocSize/filesize.10Mb/concurrency.1/filesystem.tmpfs-16 138.4 ± 1% 138.6 ± 0% ~ (p=0.327 n=16)
NginxDocSize/filesize.10Mb/concurrency.100/filesystem.tmpfs-16 147.0 ± 1% 148.8 ± 1% ~ (p=0.081 n=16)
NginxDocSize/filesize.10Mb/concurrency.1000/filesystem.tmpfs-16 162.7 ± 1% 164.2 ± 4% ~ (p=0.171 n=16)
NginxDocSize/filesize.1Kb/concurrency.1/filesystem.tmpfs-16 9.057k ± 1% 9.068k ± 1% ~ (p=0.361 n=16)
NginxDocSize/filesize.1Kb/concurrency.100/filesystem.tmpfs-16 17.67k ± 25% 18.62k ± 16% ~ (p=1.000 n=16)
NginxDocSize/filesize.1Kb/concurrency.1000/filesystem.tmpfs-16 8.290k ± 9% 8.525k ± 13% ~ (p=0.287 n=16)
NginxDocSize/filesize.100Kb/concurrency.1/filesystem.tmpfs-16 4.698k ± 1% 4.722k ± 0% ~ (p=0.243 n=16)
NginxDocSize/filesize.100Kb/concurrency.100/filesystem.tmpfs-16 8.441k ± 4% 8.700k ± 4% ~ (p=0.341 n=16)
NginxDocSize/filesize.100Kb/concurrency.1000/filesystem.tmpfs-16 3.330k ± 72% 5.355k ± 37% ~ (p=0.121 n=16)
NginxDocSize/filesize.10Mb/concurrency.1/filesystem.bind-16 126.9 ± 1% 127.2 ± 0% ~ (p=0.345 n=16)
NginxDocSize/filesize.10Mb/concurrency.100/filesystem.bind-16 131.8 ± 1% 132.4 ± 1% ~ (p=0.180 n=16)
NginxDocSize/filesize.10Mb/concurrency.1000/filesystem.bind-16 138.8 ± 2% 138.1 ± 2% ~ (p=0.744 n=16)
NginxDocSize/filesize.1Kb/concurrency.1/filesystem.bind-16 8.567k ± 1% 8.601k ± 1% ~ (p=0.396 n=16)
NginxDocSize/filesize.1Kb/concurrency.100/filesystem.bind-16 17.57k ± 20% 18.58k ± 12% ~ (p=0.210 n=16)
NginxDocSize/filesize.1Kb/concurrency.1000/filesystem.bind-16 7.755k ± 13% 7.064k ± 10% ~ (p=0.239 n=16)
NginxDocSize/filesize.100Kb/concurrency.1/filesystem.bind-16 4.356k ± 0% 4.359k ± 1% ~ (p=0.752 n=16)
NginxDocSize/filesize.100Kb/concurrency.100/filesystem.bind-16 7.274k ± 2% 7.456k ± 5% ~ (p=0.254 n=16)
NginxDocSize/filesize.100Kb/concurrency.1000/filesystem.bind-16 5.224k ± 38% 5.090k ± 33% ~ (p=0.956 n=16)
geomean 1.999k 2.065k +3.29%
pkg: sandbox memory harness (nginx under runsc, per-container cgroup)
│ before │ after │
│ anon-B │ anon-B vs base │
SandboxMemory/phase=boot 13.07Mi ± 1% 12.72Mi ± 2% -2.67% (p=0.000 n=42+48)
SandboxMemory/phase=rpc 15.54Mi ± 1% 15.20Mi ± 1% -2.19% (p=0.000 n=41+48)
SandboxMemory/phase=bulk 18.38Mi ± 1% 18.39Mi ± 1% ~ (p=0.885 n=41+48)
SandboxMemory/phase=bulkrpc 20.76Mi ± 2% 20.25Mi ± 2% -2.43% (p=0.015 n=41+48)
SandboxMemory/phase=restored 14.76Mi ± 1% 14.41Mi ± 1% -2.38% (p=0.000 n=48+52)
SandboxMemory/phase=restoredrpc 16.41Mi ± 1% 16.24Mi ± 1% ~ (p=0.307 n=48+52)
geomean 16.30Mi 16.01Mi -1.78%
│ before │ after │
│ nonreclaim-B │ nonreclaim-B vs base │
SandboxMemory/phase=boot 15.49Mi ± 1% 15.03Mi ± 1% -2.95% (p=0.000 n=42+48)
SandboxMemory/phase=rpc 17.88Mi ± 1% 17.59Mi ± 1% -1.58% (p=0.003 n=41+48)
SandboxMemory/phase=bulk 20.53Mi ± 1% 20.55Mi ± 0% ~ (p=0.780 n=41+48)
SandboxMemory/phase=bulkrpc 23.18Mi ± 2% 22.73Mi ± 2% -1.92% (p=0.039 n=41+48)
SandboxMemory/phase=restored 16.77Mi ± 1% 16.58Mi ± 1% -1.15% (p=0.002 n=48+52)
SandboxMemory/phase=restoredrpc 18.45Mi ± 1% 18.44Mi ± 1% ~ (p=0.254 n=48+52)
geomean 18.55Mi 18.32Mi -1.26%
pkg: sandbox-receive iperf3 harness (server in runsc, client on host)
│ before │ after │
│ Gbps │ Gbps vs base │
IperfReceive/default/cpus=none/streams=1 35.38 ± 4% 35.47 ± 3% ~ (p=0.353 n=10)
IperfReceive/default/cpus=none/streams=4 40.07 ± 1% 40.32 ± 1% ~ (p=0.912 n=10)
IperfReceive/default/cpus=1/streams=1 10.343 ± 6% 9.955 ± 6% ~ (p=0.529 n=10)
IperfReceive/default/cpus=1/streams=4 14.90 ± 2% 14.94 ± 3% ~ (p=0.853 n=10)
IperfReceive/default/cpus=0.25/streams=1 2.659 ± 4% 2.664 ± 5% ~ (p=0.684 n=10)
IperfReceive/default/cpus=0.25/streams=4 3.625 ± 2% 3.656 ± 2% ~ (p=0.796 n=10)
IperfReceive/gso=off/cpus=none/streams=1 35.51 ± 1% 35.49 ± 0% ~ (p=0.912 n=10)
IperfReceive/gso=off/cpus=none/streams=4 40.64 ± 1% 40.68 ± 1% ~ (p=0.896 n=10)
IperfReceive/gso=off/cpus=1/streams=4 14.91 ± 1% 14.99 ± 3% ~ (p=0.481 n=10)
IperfReceive/mtu-sized/cpus=none/streams=1 3.792 ± 2% 3.802 ± 3% ~ (p=0.971 n=10)
IperfReceive/mtu-sized/cpus=none/streams=4 3.747 ± 2% 3.697 ± 3% ~ (p=0.448 n=10)
IperfReceive/mtu-sized/cpus=1/streams=1 1.559 ± 4% 1.572 ± 4% ~ (p=0.315 n=10)
IperfReceive/mtu-sized/cpus=1/streams=4 1.775 ± 4% 1.795 ± 4% ~ (p=0.739 n=10)
IperfReceive/mtu-sized/cpus=0.25/streams=4 446.7m ± 4% 446.6m ± 3% ~ (p=0.796 n=10)
geomean 7.080 7.079 -0.01%
│ before │ after │
│ cpu-sec/GiB │ cpu-sec/GiB vs base │
IperfReceive/default/cpus=none/streams=1 854.9m ± 5% 854.9m ± 4% ~ (p=0.404 n=10)
IperfReceive/default/cpus=none/streams=4 842.6m ± 1% 838.2m ± 2% ~ (p=0.853 n=10)
IperfReceive/default/cpus=1/streams=1 835.5m ± 6% 867.4m ± 6% ~ (p=0.617 n=10)
IperfReceive/default/cpus=1/streams=4 578.2m ± 2% 577.2m ± 3% ~ (p=0.796 n=10)
IperfReceive/default/cpus=0.25/streams=1 800.4m ± 5% 805.4m ± 4% ~ (p=0.912 n=10)
IperfReceive/default/cpus=0.25/streams=4 587.1m ± 2% 584.9m ± 2% ~ (p=0.971 n=10)
IperfReceive/gso=off/cpus=none/streams=1 852.1m ± 1% 853.2m ± 0% ~ (p=0.869 n=10)
IperfReceive/gso=off/cpus=none/streams=4 831.2m ± 1% 830.7m ± 1% ~ (p=0.739 n=10)
IperfReceive/gso=off/cpus=1/streams=4 578.5m ± 1% 575.5m ± 3% ~ (p=0.684 n=10)
IperfReceive/mtu-sized/cpus=none/streams=1 8.311 ± 2% 8.302 ± 3% ~ (p=0.796 n=10)
IperfReceive/mtu-sized/cpus=none/streams=4 10.46 ± 2% 10.60 ± 4% ~ (p=0.912 n=10)
IperfReceive/mtu-sized/cpus=1/streams=1 5.535 ± 3% 5.496 ± 4% ~ (p=0.353 n=10)
IperfReceive/mtu-sized/cpus=1/streams=4 4.883 ± 4% 4.824 ± 4% ~ (p=0.796 n=10)
IperfReceive/mtu-sized/cpus=0.25/streams=4 4.884 ± 5% 4.886 ± 3% ~ (p=0.684 n=10)
geomean 1.607 1.610 +0.16%Benchmark environment
GCE c4-standard-16, COS cos-129-19506-448-8 (kernel 6.12.105+), hyperdisk-balanced, Docker with the systemd cgroup driver (cgroup v2), default runsc flags (systrap). before is master at 2511bc60218a, and after is the patch above. The two arms ran interleaved on the same VM.
- The
//test/benchmarksrows are the upstream binaries with runsc registered as Docker runtimes, n=16 per arm. Only the ns/op, bandwidth, QPS and rps metrics are shown. httpd, node and ruby were also run, with no p < 0.01 changes. - iperf
OneConnectionis bimodal on both arms because of an existing ~1 s close stall at the end of some runs. - The sandbox memory harness reads the per-container cgroup after each phase:
bootis 3 s after start,rpcfollows 300 sequential 1 KiB GETs from the host,bulkfollows a 512 MiB download inside the sandbox,bulkrpcfollows another 300 GETs, andrestored/restoredrpccome after a checkpoint and restore following the download.anon-Bis the cgroup'sanon, andnonreclaim-Bismemory.current - file - slab_reclaimable. One sample is one fresh container. - The receive harness runs an iperf3 server in the sandbox and a host-network client for 10 s per sample.
gso=offpasses--gso=false --software-gso=falseto runsc, andmtu-sizeddisables TSO/GSO on the host bridge and veth so the sandbox receives MTU-sized frames.
Hey @EtiennePerot, I'd like to take this one. Plan is to start with one active slot and grow it when recvmmsg fills all active slots.