diff options
| author | Michael Pratt <mpratt@google.com> | 2025-05-05 13:44:26 -0400 |
|---|---|---|
| committer | Michael Pratt <mpratt@google.com> | 2025-05-21 10:21:55 -0700 |
| commit | e6dacf91ffb0a356aa692ab5c46411e2eef913f3 (patch) | |
| tree | fc337b1d3fe594503468a6af639ed6459442ee2a /src/runtime/testdata | |
| parent | f12c66fbed546645389cf184b0e2ffd6ad9f78ec (diff) | |
| download | go-e6dacf91ffb0a356aa692ab5c46411e2eef913f3.tar.xz | |
runtime: use cgroup CPU limit to set GOMAXPROCS
This CL adds two related features enabled by default via compatibility
GODEBUGs containermaxprocs and updatemaxprocs.
On Linux, containermaxprocs makes the Go runtime consider cgroup CPU
bandwidth limits (quota/period) when setting GOMAXPROCS. If the cgroup
limit is lower than the number of logical CPUs available, then the
cgroup limit takes precedence.
On all OSes, updatemaxprocs makes the Go runtime periodically
recalculate the default GOMAXPROCS value and update GOMAXPROCS if it has
changed. If GOMAXPROCS is set manually, this update does not occur. This
is intended primarily to detect changes to cgroup limits, but it applies
on all OSes because the CPU affinity mask can change as well.
The runtime only considers the limit in the leaf cgroup (the one that
actually contains the process), caching the CPU limit file
descriptor(s), which are periodically reread for updates. This is a
small departure from the original proposed design. It will not consider
limits of parent cgroups (which may be lower than the leaf), and it will
not detection cgroup migration after process start.
We can consider changing this in the future, but the simpler approach is
less invasive; less risk to packages that have some awareness of runtime
internals. e.g., if the runtime periodically opens new files during
execution, file descriptor leak detection is difficult to implement in a
stable way.
For #73193.
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I6a6a636c631c1ae577fb8254960377ba91c5dc98
Reviewed-on: https://go-review.googlesource.com/c/go/+/670497
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Diffstat (limited to 'src/runtime/testdata')
| -rw-r--r-- | src/runtime/testdata/testprog/gomaxprocs.go | 152 | ||||
| -rw-r--r-- | src/runtime/testdata/testprog/gomaxprocs_windows.go | 63 |
2 files changed, 215 insertions, 0 deletions
diff --git a/src/runtime/testdata/testprog/gomaxprocs.go b/src/runtime/testdata/testprog/gomaxprocs.go new file mode 100644 index 0000000000..915e3c4dad --- /dev/null +++ b/src/runtime/testdata/testprog/gomaxprocs.go @@ -0,0 +1,152 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "os" + "runtime" + "strconv" + "time" +) + +func init() { + register("PrintGOMAXPROCS", PrintGOMAXPROCS) + register("SetLimitThenDefaultGOMAXPROCS", SetLimitThenDefaultGOMAXPROCS) + register("UpdateGOMAXPROCS", UpdateGOMAXPROCS) + register("DontUpdateGOMAXPROCS", DontUpdateGOMAXPROCS) +} + +func PrintGOMAXPROCS() { + println(runtime.GOMAXPROCS(0)) +} + +func mustSetCPUMax(path string, quota int64) { + q := "max" + if quota >= 0 { + q = strconv.FormatInt(quota, 10) + } + buf := fmt.Sprintf("%s 100000", q) + if err := os.WriteFile(path, []byte(buf), 0); err != nil { + panic(fmt.Sprintf("error setting cpu.max: %v", err)) + } +} + +func mustParseInt64(s string) int64 { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(err) + } + return v +} + +// Inputs: +// GO_TEST_CPU_MAX_PATH: Path to cgroup v2 cpu.max file. +// GO_TEST_CPU_MAX_QUOTA: CPU quota to set. +func SetLimitThenDefaultGOMAXPROCS() { + path := os.Getenv("GO_TEST_CPU_MAX_PATH") + quota := mustParseInt64(os.Getenv("GO_TEST_CPU_MAX_QUOTA")) + + mustSetCPUMax(path, quota) + + runtime.SetDefaultGOMAXPROCS() + println(runtime.GOMAXPROCS(0)) +} + +// Wait for GOMAXPROCS to change from from to to. Times out after 10s. +func waitForMaxProcsChange(from, to int) { + start := time.Now() + for { + if time.Since(start) > 10*time.Second { + panic("no update for >10s") + } + + procs := runtime.GOMAXPROCS(0) + println("GOMAXPROCS:", procs) + if procs == to { + return + } + if procs != from { + panic(fmt.Sprintf("GOMAXPROCS change got %d want %d", procs, to)) + } + + time.Sleep(100*time.Millisecond) + } +} + +// Make sure that GOMAXPROCS does not change from curr. +// +// It is impossible to assert that it never changes, so this just makes sure it +// stays for 5s. +func mustNotChangeMaxProcs(curr int) { + start := time.Now() + for { + if time.Since(start) > 5*time.Second { + return + } + + procs := runtime.GOMAXPROCS(0) + println("GOMAXPROCS:", procs) + if procs != curr { + panic(fmt.Sprintf("GOMAXPROCS change got %d want %d", procs, curr)) + } + + time.Sleep(100*time.Millisecond) + } +} + +// Inputs: +// GO_TEST_CPU_MAX_PATH: Path to cgroup v2 cpu.max file. +func UpdateGOMAXPROCS() { + // We start with no limit. + + ncpu := runtime.NumCPU() + + procs := runtime.GOMAXPROCS(0) + println("GOMAXPROCS:", procs) + if procs != ncpu { + panic(fmt.Sprintf("GOMAXPROCS got %d want %d", procs, ncpu)) + } + + path := os.Getenv("GO_TEST_CPU_MAX_PATH") + + // Drop down to 3 CPU. + mustSetCPUMax(path, 300000) + waitForMaxProcsChange(ncpu, 3) + + // Drop even further. Now we hit the minimum GOMAXPROCS=2. + mustSetCPUMax(path, 100000) + waitForMaxProcsChange(3, 2) + + // Increase back up. + mustSetCPUMax(path, 300000) + waitForMaxProcsChange(2, 3) + + // Remove limit entirely. + mustSetCPUMax(path, -1) + waitForMaxProcsChange(3, ncpu) + + // Setting GOMAXPROCS explicitly disables updates. + runtime.GOMAXPROCS(3) + mustSetCPUMax(path, 200000) + mustNotChangeMaxProcs(3) + + println("OK") +} + +// Inputs: +// GO_TEST_CPU_MAX_PATH: Path to cgroup v2 cpu.max file. +func DontUpdateGOMAXPROCS() { + // The caller has disabled updates. Make sure they don't happen. + + curr := runtime.GOMAXPROCS(0) + println("GOMAXPROCS:", curr) + + path := os.Getenv("GO_TEST_CPU_MAX_PATH") + mustSetCPUMax(path, 300000) + mustNotChangeMaxProcs(curr) + + println("OK") +} diff --git a/src/runtime/testdata/testprog/gomaxprocs_windows.go b/src/runtime/testdata/testprog/gomaxprocs_windows.go new file mode 100644 index 0000000000..bc7a4b1063 --- /dev/null +++ b/src/runtime/testdata/testprog/gomaxprocs_windows.go @@ -0,0 +1,63 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "syscall" + "unsafe" +) + +func init() { + register("WindowsUpdateGOMAXPROCS", WindowsUpdateGOMAXPROCS) + register("WindowsDontUpdateGOMAXPROCS", WindowsDontUpdateGOMAXPROCS) +} + +// Set CPU affinity mask to only two CPUs. +// +// Skips the test if CPUs 0 and 1 are not available. +func setAffinity2() { + kernel32 := syscall.MustLoadDLL("kernel32.dll") + _GetProcessAffinityMask := kernel32.MustFindProc("GetProcessAffinityMask") + _SetProcessAffinityMask := kernel32.MustFindProc("SetProcessAffinityMask") + + h, err := syscall.GetCurrentProcess() + if err != nil { + panic(err) + } + + var mask, sysmask uintptr + ret, _, err := _GetProcessAffinityMask.Call(uintptr(h), uintptr(unsafe.Pointer(&mask)), uintptr(unsafe.Pointer(&sysmask))) + if ret == 0 { + panic(err) + } + + // We're going to restrict to CPUs 0 and 1. Make sure those are already available. + if mask & 0b11 != 0b11 { + println("SKIP: CPUs 0 and 1 not available") + os.Exit(0) + } + + mask = 0b11 + ret, _, err = _SetProcessAffinityMask.Call(uintptr(h), mask) + if ret == 0 { + panic(err) + } +} + +func WindowsUpdateGOMAXPROCS() { + ncpu := runtime.NumCPU() + setAffinity2() + waitForMaxProcsChange(ncpu, 2) + println("OK") +} + +func WindowsDontUpdateGOMAXPROCS() { + ncpu := runtime.NumCPU() + setAffinity2() + mustNotChangeMaxProcs(ncpu) + println("OK") +} |
