mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Preallocate host kernel FD table before Sentry boot.
This avoids an expensive RCU stall during the Sentry boot process, when
the Linux-side FD table needs to be expanded to accommodate the Sentry's
512-sized FD table.
The kernel imposes an RCU stall when this occurs for
multithreaded programs (which all Go programs are), but not
single-threaded ones. Hence the introduction of a minimal C binary that
does the magic. There is some storytime explanation in the C source
file itself.
Even though this is an extra `exec` in the hot startup path, the
benefits are obvious:
```
runsc do /bin/true:
│ A (baseline) │ B (prewarmer) │
RunscDo 202.8m ± 3% 147.1m ± 5% -27.45% (n=100)
docker run --rm alpine true:
│ A (baseline) │ B (prewarmer) │
DockerRunStartup 778.9m ± 2% 738.0m ± 2% -5.25% (p=0.000 n=100)
OCi operations ("total" = create + start + wait + delete):
│ A (baseline) │ B (prewarmer) │
OCICreate 145.6m ± 5% 102.3m ± 8% -29.71% (n=100)
OCIReady 165.8m ± 4% 122.6m ± 6% -26.10% (n=100)
OCITotal 178.6m ± 3% 138.3m ± 5% -22.59% (n=100)
```
I also reduced the Sentry's FD table to 256 rather than 512, which saves
one or two pages of host kernel memory per sandbox instance.
Also threw in a test to make sure the C program remains
single-page-sized to avoid potential unintended bloating down the line.
This commit is contained in:
+11
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/moby/sys/capability"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/cleanup"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
@@ -492,7 +493,14 @@ const (
|
||||
// startingStdioFD is the starting stdioFD number used during sandbox
|
||||
// start and restore. This makes sure the stdioFDs are always the same
|
||||
// on initial start and on restore.
|
||||
startingStdioFD = 256
|
||||
// It must be above the highest FD number that donation can assign at boot
|
||||
// (donated FDs are numbered densely from 3, and should ideally be such
|
||||
// that adding 2 to it (i.e. index stderr gets) does *not* cross a
|
||||
// power of 2. See `//runsc/prewarmer/prewarmer.c` for some fun Linux
|
||||
// kernel lore that explains why.
|
||||
// LINT.IfChange
|
||||
startingStdioFD = 253
|
||||
// LINT.ThenChange(../prewarmer/prewarmer.c)
|
||||
|
||||
// containerSpecsKey is the key used to add and pop the container specs to the
|
||||
// kernel during save/restore.
|
||||
@@ -610,6 +618,7 @@ func New(args Args) (*Loader, error) {
|
||||
// used.
|
||||
newfd := startingStdioFD
|
||||
|
||||
remapStart := gtime.Now()
|
||||
for _, stdioFD := range args.StdioFDs {
|
||||
// Check that newfd is unused to avoid clobbering over it.
|
||||
if _, err := unix.FcntlInt(uintptr(newfd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
|
||||
@@ -627,6 +636,7 @@ func New(args Args) (*Loader, error) {
|
||||
_ = unix.Close(stdioFD)
|
||||
newfd++
|
||||
}
|
||||
log.Infof("Remapped stdio FDs to [%d, %d] in %v", startingStdioFD, newfd-1, gtime.Since(remapStart)) // Load-bearing log message! See `//runsc/prewarmer/prewarmer_speedup_test.go` which parses this log line.
|
||||
for _, goferFD := range args.GoferFDs {
|
||||
l.root.goferFDs = append(l.root.goferFDs, fd.New(goferFD))
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ const (
|
||||
checkpointGoferName = "checkpointgofer"
|
||||
gvisorSentryName = "gvisor_sentry"
|
||||
gvisorSentryPluginStackName = "gvisor_sentry_plugin_stack"
|
||||
prewarmerName = "gvisor-sentry-prewarmer"
|
||||
)
|
||||
|
||||
// binDirName is the name of the directory holding sidecar binaries.
|
||||
@@ -217,10 +218,12 @@ var (
|
||||
// network stack linked in.
|
||||
// Only present in plugin-enabled installations, so not part of `All`.
|
||||
GvisorSentryPluginStack = Binary{Name: gvisorSentryPluginStackName}
|
||||
// GvisorSentryPrewarmer is the C binary that runs ahead of `GvisorSentry`.
|
||||
GvisorSentryPrewarmer = Binary{Name: prewarmerName}
|
||||
)
|
||||
|
||||
// All lists every sidecar present in a standard installation.
|
||||
var All = []*Binary{&MetricServer, &CheckpointGofer, &GvisorSentry}
|
||||
var All = []*Binary{&MetricServer, &CheckpointGofer, &GvisorSentry, &GvisorSentryPrewarmer}
|
||||
|
||||
// Options is the set of options used to execute a sidecar binary.
|
||||
type Options struct {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
load("//tools:defs.bzl", "cc_binary", "go_binary", "go_test")
|
||||
|
||||
package(
|
||||
default_applicable_licenses = ["//:license"],
|
||||
licenses = ["notice"],
|
||||
)
|
||||
|
||||
# gvisor-sentry-prewarmer must remain a statically-linked, freestanding (no libc),
|
||||
# single-threaded C binary that fits in a single 4KiB page.
|
||||
# Do NOT rewrite it in Go; it must be single-threaded.
|
||||
# Read its comments as to why.
|
||||
cc_binary(
|
||||
name = "gvisor-sentry-prewarmer",
|
||||
srcs = ["prewarmer.c"],
|
||||
copts = [
|
||||
"-Os",
|
||||
"-ffreestanding",
|
||||
"-fno-stack-protector",
|
||||
"-fno-asynchronous-unwind-tables",
|
||||
"-fno-unwind-tables",
|
||||
],
|
||||
linkopts = [
|
||||
"-static",
|
||||
"-nostdlib",
|
||||
"-nostartfiles",
|
||||
"-Wl,--build-id=none",
|
||||
"-Wl,--gc-sections",
|
||||
"-Wl,-s",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "prewarmer_single_page_test",
|
||||
size = "small",
|
||||
srcs = ["prewarmer_single_page_test.go"],
|
||||
data = [":gvisor-sentry-prewarmer"],
|
||||
deps = ["//pkg/test/testutil"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "fdtable_expander",
|
||||
srcs = ["fdtable_expander.go"],
|
||||
pure = True,
|
||||
deps = ["@org_golang_x_sys//unix:go_default_library"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "prewarmer_speedup_test",
|
||||
size = "small",
|
||||
srcs = ["prewarmer_speedup_test.go"],
|
||||
data = [
|
||||
":fdtable_expander",
|
||||
":gvisor-sentry-prewarmer",
|
||||
"//:release",
|
||||
],
|
||||
deps = ["//pkg/test/testutil"],
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Binary `fdtable_expander` successively forces the kernel to expand this
|
||||
// process's FD table through the sizes that `gvisor-sentry-prewarmer` is
|
||||
// meant to have already paid for:
|
||||
// - 64 slots (the kernel default, should be a no-op but who knows)
|
||||
// - 128
|
||||
// - 256
|
||||
// See `prewarmer.c` for the underlying kernel details that make this worth
|
||||
// testing.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
for _, target := range []int{63, 127, 255} {
|
||||
if err := unix.Dup3(0, target, unix.O_CLOEXEC); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fdtable_expander: dup3(0, %d) failed: %v\n", target, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2026 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// gvisor-sentry-prewarmer grows its host file descriptor table, then execs
|
||||
// the binary at argv[1], handing it argv[2:] as its argv (so argv[2] is the
|
||||
// target's argv[0]). It runs just before `runsc boot` (the Sentry) does, and
|
||||
// is meant to be invoked as such, i.e.:
|
||||
// `gvisor-sentry-prewarmer /path/to/gvisor_sentry runsc-sandbox <flags...> boot <flags...>`
|
||||
//
|
||||
// You may be asking: wtf, wat, but why? Excellent questions.
|
||||
// Sit down for some deep kernel lore.
|
||||
//
|
||||
// We hold the following truths to be not self-evident, but true nonetheless:
|
||||
//
|
||||
// - FDs in Linux are reused, serially by index, but some system calls allow
|
||||
// the userspace program to pick an FD number, so the table can be sparse.
|
||||
// - The Linux kernel backs a process's FD table as an array with sizes that
|
||||
// are always powers of 2.
|
||||
// - When the table needs to grow, it grows to the smallest power of 2 that
|
||||
// accommodates the largest FD number that the program needs.
|
||||
// - When a program `exec`s, its FD table size is maintained, even when the FDs
|
||||
// would be closed-on-exec. (Yes, this implies the FD table never shrinks as
|
||||
// the genealogy of a process tree goes deeper.)
|
||||
// - When the table needs to grow, if the program is a multi-threaded program,
|
||||
// the kernel waits for a full RCU grace period (which is >10ms, ouch).
|
||||
// However, if the process is *not* multi-threaded, there is no such wait!
|
||||
// - All Go programs are multi-threaded from the kernel's perspective, because
|
||||
// the Go runtime spawns off some threads on startup before `main()` is even
|
||||
// called.
|
||||
// - gVisor has this funny behavior of needing to segment its FD table range
|
||||
// so that the stdin/stdout/stderr FDs are always remapped to some high
|
||||
// indexes (see `startingStdioFD` in `//runsc/boot/loader.go`).
|
||||
//
|
||||
// You can see where this is going. We need a single-threaded program that
|
||||
// runs before `runsc boot` does that inflates its FD table to be large enough
|
||||
// so that `runsc boot` never hits the FD table expansion RCU grace period
|
||||
// that the kernel would hit it with otherwise.
|
||||
// That's what this prewarmer program does.
|
||||
// It's written in C with very minimal dependencies and fitting in a single
|
||||
// page (4KiB) because it runs on the hot path of gVisor startup. And,
|
||||
// despite its existence, it makes gVisor startup faster.
|
||||
|
||||
#include <asm/unistd.h> // __NR_* syscall numbers for the target arch.
|
||||
#include <linux/fcntl.h> // F_DUPFD_CLOEXEC, O_* flags, AT_FDCWD.
|
||||
|
||||
// The FD number that the boot process remaps its first stdio FD to.
|
||||
// Must match `startingStdioFD` in `runsc/boot/loader.go`.
|
||||
// LINT.IfChange
|
||||
#define STARTING_STDIO_FD 253
|
||||
// LINT.ThenChange(../boot/loader.go)
|
||||
|
||||
// The minimum FD number that the prewarmed fdtable must accommodate.
|
||||
// Since `startingStdioFD` is mapped to stdin, then `startingStdioFD+1` is
|
||||
// mapped to stdout, and `startingStdioFD+2` is mapped to stderr.
|
||||
// We choose this to be just under what would cause the FD table to expand to
|
||||
// its next power-of-2 size. Here, STARTING_STDIO_FD + 2 = 253 + 2 = 255,
|
||||
// so the host kernel FD table is of size 256. If we were to bump this any
|
||||
// further, we would needlessly waste 50% of the host kernel memory dedicated
|
||||
// to the FD table (which isn't that much, but I write this because the
|
||||
// previous value of this constant was committing this exact wastefulness).
|
||||
#define PREWARM_MIN_FD (STARTING_STDIO_FD + 2)
|
||||
|
||||
// Raw 3-argument syscall function.
|
||||
#if defined(__x86_64__)
|
||||
static long sys3(long nr, long a0, long a1, long a2) {
|
||||
long ret;
|
||||
__asm__ volatile("syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), "D"(a0), "S"(a1), "d"(a2)
|
||||
: "rcx", "r11", "memory");
|
||||
return ret;
|
||||
}
|
||||
#elif defined(__aarch64__)
|
||||
static long sys3(long nr, long a0, long a1, long a2) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a0;
|
||||
register long x1 __asm__("x1") = a1;
|
||||
register long x2 __asm__("x2") = a2;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory", "cc");
|
||||
return x0;
|
||||
}
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
|
||||
static __attribute__((noreturn)) void sys_exit(long code) {
|
||||
for (;;) {
|
||||
sys3(__NR_exit_group, code, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Basic stderr logging function. Look ma, no libc.
|
||||
static void write_stderr(const char* s) {
|
||||
long len = 0;
|
||||
while (s[len] != '\0') {
|
||||
len++;
|
||||
}
|
||||
sys3(__NR_write, 2, (long)s, len);
|
||||
}
|
||||
|
||||
// Forces the host kernel to grow the FD table to allow the FD with index
|
||||
// `PREWARM_MIN_FD` to exist without needing later FD table expansion.
|
||||
static void prewarm_fdtable(void) {
|
||||
// F_DUPFD_CLOEXEC atomically picks a free FD >= PREWARM_MIN_FD, so this
|
||||
// cannot clobber any inherited (donated) FD.
|
||||
long fd = sys3(__NR_fcntl, 0, F_DUPFD_CLOEXEC, PREWARM_MIN_FD);
|
||||
if (fd < 0) {
|
||||
// FD 0 may not be open. Fall back to duplicating an FD we open ourselves.
|
||||
long base = sys3(__NR_openat, AT_FDCWD, (long)"/", O_RDONLY | O_CLOEXEC);
|
||||
if (base < 0) {
|
||||
// OK, whatever is going on is weird. We will hit the RCU stall, but not
|
||||
// worth failing on.
|
||||
write_stderr("gvisor-sentry-prewarmer: failed to prewarm the FD table. This slows down gVisor startup.\n");
|
||||
return;
|
||||
}
|
||||
fd = sys3(__NR_fcntl, base, F_DUPFD_CLOEXEC, PREWARM_MIN_FD);
|
||||
sys3(__NR_close, base, 0, 0);
|
||||
}
|
||||
if (fd >= 0) {
|
||||
sys3(__NR_close, fd, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by `_start` with a pointer to the initial process stack, which per
|
||||
// the Linux ABI holds: `argc`, `argv[0..argc-1]`, NULL, `envp`, NULL.
|
||||
__attribute__((noreturn, used)) void prewarmer_main(long* stack) {
|
||||
long argc = stack[0];
|
||||
char** argv = (char**)(stack + 1);
|
||||
char** envp = argv + argc + 1;
|
||||
if (argc < 3) {
|
||||
write_stderr(
|
||||
"gvisor-sentry-prewarmer: usage: gvisor-sentry-prewarmer <binary> <argv[0]> [argv[1:]...]\n"
|
||||
"Do not run this by hand; it is exec'd by runsc ahead of the sandbox "
|
||||
"process.\n");
|
||||
sys_exit(1);
|
||||
}
|
||||
prewarm_fdtable();
|
||||
// The argv array is NULL-terminated, so &argv[2] is a valid argv.
|
||||
sys3(__NR_execve, (long)argv[1], (long)&argv[2], (long)envp);
|
||||
write_stderr("gvisor-sentry-prewarmer: exec failed: ");
|
||||
write_stderr(argv[1]);
|
||||
write_stderr("\n");
|
||||
sys_exit(127);
|
||||
}
|
||||
|
||||
// Process entry point. Hand the initial stack pointer to `prewarmer_main`.
|
||||
#if defined(__x86_64__)
|
||||
__asm__(
|
||||
".globl _start\n"
|
||||
".type _start, @function\n"
|
||||
"_start:\n"
|
||||
" movq %rsp, %rdi\n"
|
||||
" call prewarmer_main\n"
|
||||
" ud2\n");
|
||||
#elif defined(__aarch64__)
|
||||
__asm__(
|
||||
".globl _start\n"
|
||||
".type _start, @function\n"
|
||||
"_start:\n"
|
||||
" mov x0, sp\n"
|
||||
" bl prewarmer_main\n"
|
||||
" brk #0\n");
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package prewarmer_test verifies size constraints on the gvisor-sentry-prewarmer
|
||||
// sidecar binary.
|
||||
package prewarmer_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/testutil"
|
||||
)
|
||||
|
||||
// TestPrewarmerFitsInOnePage verifies that the compiled gvisor-sentry-prewarmer
|
||||
// binary fits in a single 4KiB page.
|
||||
// This ensures its faulting cost remains as small as we can make it,
|
||||
// given its trivial nature and the fact that it's on the hot-path
|
||||
// of gVisor startup.
|
||||
func TestPrewarmerFitsInOnePage(t *testing.T) {
|
||||
const maxSize = 4096
|
||||
path, err := testutil.FindFile("runsc/prewarmer/gvisor-sentry-prewarmer")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find gvisor-sentry-prewarmer binary: %v", err)
|
||||
}
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot stat %q: %v", path, err)
|
||||
}
|
||||
if st.Size() > maxSize {
|
||||
t.Errorf("gvisor-sentry-prewarmer is %d bytes, which does not fit in a single %d-byte page", st.Size(), maxSize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright 2026 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package prewarmer_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/testutil"
|
||||
)
|
||||
|
||||
// highestOpenFD returns the highest FD number currently open in this
|
||||
// process.
|
||||
func highestOpenFD(t *testing.T) int {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir("/proc/self/fd")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot list /proc/self/fd: %v", err)
|
||||
}
|
||||
highest := -1
|
||||
for _, e := range entries {
|
||||
fd, err := strconv.Atoi(e.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if fd > highest {
|
||||
highest = fd
|
||||
}
|
||||
}
|
||||
if highest == -1 {
|
||||
t.Fatalf("cannot determine highest open FD")
|
||||
}
|
||||
return highest
|
||||
}
|
||||
|
||||
// timedRun runs argv[0] with the given arguments and returns its wall time.
|
||||
func timedRun(t *testing.T, argv ...string) time.Duration {
|
||||
t.Helper()
|
||||
cmd := exec.Command(argv[0], argv[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
start := time.Now()
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("%v failed: %v", argv, err)
|
||||
}
|
||||
return time.Since(start)
|
||||
}
|
||||
|
||||
func p50(durations []time.Duration) time.Duration {
|
||||
sorted := append([]time.Duration(nil), durations...)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
|
||||
return sorted[len(sorted)/2]
|
||||
}
|
||||
|
||||
// TestPrewarmerSpeedsUpFDTableExpansion verifies the assumptions about Linux
|
||||
// kernel implementation details made in `prewarmer.c` which make it
|
||||
// worthwhile.
|
||||
// If this test fails, then either the Linux kernel or the Go runtime
|
||||
// may have changed, and it is time to consider retiring `prewarmer.c`.
|
||||
func TestPrewarmerSpeedsUpFDTableExpansion(t *testing.T) {
|
||||
prewarmer, err := testutil.FindFile("runsc/prewarmer/gvisor-sentry-prewarmer")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find gvisor-sentry-prewarmer binary: %v", err)
|
||||
}
|
||||
expander, err := testutil.FindFile("runsc/prewarmer/fdtable_expander")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find fdtable_expander binary: %v", err)
|
||||
}
|
||||
|
||||
// The children we'll spawn inherit their FD table size from the test
|
||||
// process, so if we already have an FD table large enough, the prewarmer
|
||||
// will be a no-op. We cannot ask the kernel to shrink the FD table, so
|
||||
// all we can do is skip the test in such a case.
|
||||
if fd := highestOpenFD(t); fd >= 64 {
|
||||
t.Skipf("test process has FD %d open, so child processes inherit a pre-expanded FD table; this environment cannot exercise the expansion the prewarmer exists to avoid", fd)
|
||||
}
|
||||
|
||||
// One untimed run with both binaries involved just to warm the page cache:
|
||||
timedRun(t, prewarmer, expander, "fdtable_expander")
|
||||
|
||||
const runs = 32 // Number of runs to do.
|
||||
|
||||
var alone, prewarmed []time.Duration
|
||||
for i := 0; i < runs; i++ {
|
||||
alone = append(alone, timedRun(t, expander))
|
||||
}
|
||||
for i := 0; i < runs; i++ {
|
||||
prewarmed = append(prewarmed, timedRun(t, prewarmer, expander, "fdtable_expander"))
|
||||
}
|
||||
|
||||
p50Alone, p50Prewarmed := p50(alone), p50(prewarmed)
|
||||
t.Logf("fdtable_expander alone: p50 = %v over %d runs", p50Alone, runs)
|
||||
t.Logf("fdtable_expander with prewarmer: p50 = %v over %d runs", p50Prewarmed, runs)
|
||||
if p50Prewarmed >= p50Alone {
|
||||
t.Errorf("prewarming did not speed up FD table expansion: p50 with prewarmer (%v) >= p50 without (%v); either the kernel no longer stalls multi-threaded FD table expansion (and the prewarmer should be deleted), or the prewarmer is broken", p50Prewarmed, p50Alone)
|
||||
}
|
||||
}
|
||||
|
||||
// remapLogRegexp matches the timing log line emitted by the Sentry when it
|
||||
// remaps its stdio FDs to high FD numbers in `loader.go`.
|
||||
// Used to parse the duration out of the logs.
|
||||
var remapLogRegexp = regexp.MustCompile(`Remapped stdio FDs to \[\d+, \d+\] in (\S+)`)
|
||||
|
||||
// runscDo runs `runsc --rootless do /bin/true` with the given sidecar
|
||||
// directory and returns the stdio remap duration parsed from the Sentry's
|
||||
// logs (and the whole logs too).
|
||||
func runscDo(t *testing.T, runsc, sidecarDir, rootDir string) (time.Duration, string, error) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(runsc,
|
||||
"--rootless",
|
||||
"--network=none",
|
||||
"--root="+rootDir,
|
||||
"--debug-log=/dev/stderr",
|
||||
"do", "/bin/true")
|
||||
cmd.Env = append(os.Environ(), "GVISOR_SIDECAR_BINARIES_DIR="+sidecarDir)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
if err := cmd.Run(); err != nil {
|
||||
return 0, output.String(), fmt.Errorf("runsc do failed: %w", err)
|
||||
}
|
||||
m := remapLogRegexp.FindStringSubmatch(output.String())
|
||||
if m == nil {
|
||||
return 0, output.String(), fmt.Errorf("no stdio remap timing line in runsc debug log")
|
||||
}
|
||||
d, err := time.ParseDuration(m[1])
|
||||
if err != nil {
|
||||
return 0, output.String(), fmt.Errorf("cannot parse stdio remap duration %q: %w", m[1], err)
|
||||
}
|
||||
return d, output.String(), nil
|
||||
}
|
||||
|
||||
// TestPrewarmerSpeedsUpRealBoot verifies that the Linux kernel implementation
|
||||
// details that the `prewarmer.c` program takes advantage of actually help
|
||||
// with gVisor's startup latency in practice.
|
||||
// It runs `runsc --rootless --network=none do /bin/true` and extracts the FD
|
||||
// remap timing from the logs, and verifies that this is faster with the
|
||||
// prewarmer in the startup path vs without the prewarmer.
|
||||
func TestPrewarmerSpeedsUpRealBoot(t *testing.T) {
|
||||
runsc, err := testutil.FindFile("release/runsc")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find runsc binary in the release layout: %v", err)
|
||||
}
|
||||
releaseBinDir, err := testutil.FindFile("release/gvisor-bin")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find gvisor-bin directory in the release layout: %v", err)
|
||||
}
|
||||
|
||||
// Create a writable symlink-based mirror of the gvisor-bin directory,
|
||||
// so that we can delete the prewarmer binary from it later.
|
||||
tmp := t.TempDir()
|
||||
sidecarDir := filepath.Join(tmp, "gvisor-bin")
|
||||
if err := os.Mkdir(sidecarDir, 0755); err != nil {
|
||||
t.Fatalf("cannot create sidecar directory: %v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(releaseBinDir)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot list %q: %v", releaseBinDir, err)
|
||||
}
|
||||
prewarmerCopy := ""
|
||||
for _, e := range entries {
|
||||
src := filepath.Join(releaseBinDir, e.Name())
|
||||
dst := filepath.Join(sidecarDir, e.Name())
|
||||
if err := os.Symlink(src, dst); err != nil {
|
||||
t.Fatalf("cannot symlink %q into sidecar directory: %v", src, err)
|
||||
}
|
||||
if e.Name() == "gvisor-sentry-prewarmer" {
|
||||
prewarmerCopy = dst
|
||||
}
|
||||
}
|
||||
if prewarmerCopy == "" {
|
||||
t.Fatalf("release gvisor-bin directory %q does not contain gvisor-sentry-prewarmer; the release is missing the prewarmer sidecar", releaseBinDir)
|
||||
}
|
||||
|
||||
// One untimed boot to warm everything up; if this one cannot run at all,
|
||||
// that is the environment's limitation, not the prewarmer's.
|
||||
if _, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-prewarmed")); err != nil {
|
||||
t.Skipf("cannot run rootless sandboxes in this environment: %v; output:\n%s", err, output)
|
||||
}
|
||||
|
||||
const runs = 8 // Number of runs to do per side.
|
||||
|
||||
var prewarmed []time.Duration
|
||||
for i := 0; i < runs; i++ {
|
||||
d, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-prewarmed"))
|
||||
if err != nil {
|
||||
t.Fatalf("runsc do with the prewarmer failed: %v; output:\n%s", err, output)
|
||||
}
|
||||
prewarmed = append(prewarmed, d)
|
||||
}
|
||||
|
||||
if err := os.Remove(prewarmerCopy); err != nil {
|
||||
t.Fatalf("cannot delete prewarmer from sidecar directory: %v", err)
|
||||
}
|
||||
|
||||
var unprewarmed []time.Duration
|
||||
for i := 0; i < runs; i++ {
|
||||
d, output, err := runscDo(t, runsc, sidecarDir, filepath.Join(tmp, "root-unprewarmed"))
|
||||
if err != nil {
|
||||
t.Fatalf("runsc do without the prewarmer failed: %v; output:\n%s", err, output)
|
||||
}
|
||||
unprewarmed = append(unprewarmed, d)
|
||||
}
|
||||
|
||||
p50Prewarmed, p50Unprewarmed := p50(prewarmed), p50(unprewarmed)
|
||||
t.Logf("stdio remap with prewarmer: p50 = %v over %d boots", p50Prewarmed, runs)
|
||||
t.Logf("stdio remap without prewarmer: p50 = %v over %d boots", p50Unprewarmed, runs)
|
||||
if p50Prewarmed >= p50Unprewarmed {
|
||||
t.Errorf("prewarming did not speed up the Sentry's stdio remap: p50 with prewarmer (%v) >= p50 without (%v); either the kernel no longer stalls multi-threaded FD table expansion (and the prewarmer should be deleted), or the prewarmer is broken", p50Prewarmed, p50Unprewarmed)
|
||||
}
|
||||
}
|
||||
@@ -964,10 +964,19 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
|
||||
Setsid: true,
|
||||
}
|
||||
|
||||
// Set Args[0] to make easier to spot the sandbox process. Otherwise it's
|
||||
// shown as `exe`.
|
||||
// Set Args[0] to make easier to spot the sandbox process.
|
||||
cmd.Args[0] = "runsc-sandbox"
|
||||
|
||||
// If the prewarmer sidecar is available, exec it ahead of the boot binary.
|
||||
// Its argv is `gvisor-prewarmer <binary> <argv[0]> [argv[1:]...]`.
|
||||
if p, err := gvisorbinaries.GvisorSentryPrewarmer.Path(); err == nil {
|
||||
log.Infof("Sidecar %q found: prepending Sentry boot command with %s", gvisorbinaries.GvisorSentryPrewarmer.Name, p)
|
||||
cmd.Args = append([]string{p, cmd.Path}, cmd.Args[0:]...)
|
||||
cmd.Path = p
|
||||
} else {
|
||||
log.Warningf("Sidecar %q not found or usable (%v). This slows down gVisor startup significantly.", gvisorbinaries.GvisorSentryPrewarmer.Name, err)
|
||||
}
|
||||
|
||||
// Transfer FDs that need to be present before the "boot" command.
|
||||
// Start at 3 because 0, 1, and 2 are taken by stdin/out/err.
|
||||
nextFD := donations.Transfer(cmd, 3)
|
||||
|
||||
@@ -6,6 +6,7 @@ SIDECARS = {
|
||||
"//runsc/checkpointgofer:checkpointgofer_binary": "checkpointgofer",
|
||||
"//runsc/cmd/metricserver:runsc-metric-server": "runsc-metric-server",
|
||||
"//runsc/cmd/sentry:gvisor_sentry": "gvisor_sentry",
|
||||
"//runsc/prewarmer:gvisor-sentry-prewarmer": "gvisor-sentry-prewarmer",
|
||||
}
|
||||
|
||||
def _single_file(target):
|
||||
|
||||
Reference in New Issue
Block a user