From e6dacf91ffb0a356aa692ab5c46411e2eef913f3 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Mon, 5 May 2025 13:44:26 -0400 Subject: 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 Reviewed-by: Michael Knyszek --- src/runtime/debug.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 3 deletions(-) (limited to 'src/runtime/debug.go') diff --git a/src/runtime/debug.go b/src/runtime/debug.go index 57e9ba8d7d..94d1dab34d 100644 --- a/src/runtime/debug.go +++ b/src/runtime/debug.go @@ -10,9 +10,28 @@ import ( ) // GOMAXPROCS sets the maximum number of CPUs that can be executing -// simultaneously and returns the previous setting. It defaults to -// the value of [runtime.NumCPU]. If n < 1, it does not change the current setting. -// This call will go away when the scheduler improves. +// simultaneously and returns the previous setting. If n < 1, it does not change +// the current setting. +// +// If the GOMAXPROCS environment variable is set to a positive whole number, +// GOMAXPROCS defaults to that value. +// +// Otherwise, the Go runtime selects an appropriate default value based on the +// number of logical CPUs on the machine, the process’s CPU affinity mask, and, +// on Linux, the process’s average CPU throughput limit based on cgroup CPU +// quota, if any. +// +// The Go runtime periodically updates the default value based on changes to +// the total logical CPU count, the CPU affinity mask, or cgroup quota. Setting +// a custom value with the GOMAXPROCS environment variable or by calling +// GOMAXPROCS disables automatic updates. The default value and automatic +// updates can be restored by calling [SetDefaultGOMAXPROCS]. +// +// If GODEBUG=containermaxprocs=0 is set, GOMAXPROCS defaults to the value of +// [runtime.NumCPU]. If GODEBUG=updatemaxprocs=0 is set, the Go runtime does +// not perform automatic GOMAXPROCS updating. +// +// The default GOMAXPROCS behavior may change as the scheduler improves. func GOMAXPROCS(n int) int { if GOARCH == "wasm" && n > 1 { n = 1 // WebAssembly has no threads yet, so only one CPU is possible. @@ -28,12 +47,55 @@ func GOMAXPROCS(n int) int { stw := stopTheWorldGC(stwGOMAXPROCS) // newprocs will be processed by startTheWorld + // + // TODO(prattmic): this could use a nicer API. Perhaps add it to the + // stw parameter? newprocs = int32(n) + newprocsCustom = true startTheWorldGC(stw) return ret } +// SetDefaultGOMAXPROCS updates the GOMAXPROCS setting to the runtime +// default, as described by [GOMAXPROCS], ignoring the GOMAXPROCS +// environment variable. +// +// SetDefaultGOMAXPROCS can be used to enable the default automatic updating +// GOMAXPROCS behavior if it has been disabled by the GOMAXPROCS +// environment variable or a prior call to [GOMAXPROCS], or to force an immediate +// update if the caller is aware of a change to the total logical CPU count, CPU +// affinity mask or cgroup quota. +func SetDefaultGOMAXPROCS() { + // SetDefaultGOMAXPROCS conceptually means "[re]do what the runtime + // would do at startup if the GOMAXPROCS environment variable were + // unset." It still respects GODEBUG. + + procs := defaultGOMAXPROCS(0) + + lock(&sched.lock) + curr := gomaxprocs + custom := sched.customGOMAXPROCS + unlock(&sched.lock) + + if !custom && procs == curr { + // Nothing to do if we're already using automatic GOMAXPROCS + // and the limit is unchanged. + return + } + + stw := stopTheWorldGC(stwGOMAXPROCS) + + // newprocs will be processed by startTheWorld + // + // TODO(prattmic): this could use a nicer API. Perhaps add it to the + // stw parameter? + newprocs = procs + newprocsCustom = false + + startTheWorldGC(stw) +} + // NumCPU returns the number of logical CPUs usable by the current process. // // The set of available CPUs is checked by querying the operating system -- cgit v1.3