aboutsummaryrefslogtreecommitdiff
path: root/src/runtime/proc.go
diff options
context:
space:
mode:
authorMichael Pratt <mpratt@google.com>2025-05-05 13:44:26 -0400
committerMichael Pratt <mpratt@google.com>2025-05-21 10:21:55 -0700
commite6dacf91ffb0a356aa692ab5c46411e2eef913f3 (patch)
treefc337b1d3fe594503468a6af639ed6459442ee2a /src/runtime/proc.go
parentf12c66fbed546645389cf184b0e2ffd6ad9f78ec (diff)
downloadgo-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/proc.go')
-rw-r--r--src/runtime/proc.go113
1 files changed, 112 insertions, 1 deletions
diff --git a/src/runtime/proc.go b/src/runtime/proc.go
index 55cb630b5d..4925528783 100644
--- a/src/runtime/proc.go
+++ b/src/runtime/proc.go
@@ -210,6 +210,7 @@ func main() {
}()
gcenable()
+ defaultGOMAXPROCSUpdateEnable() // don't STW before runtime initialized.
main_init_done = make(chan bool)
if iscgo {
@@ -897,12 +898,24 @@ func schedinit() {
// mcommoninit runs before parsedebugvars, so init profstacks again.
mProfStackInit(gp.m)
+ defaultGOMAXPROCSInit()
lock(&sched.lock)
sched.lastpoll.Store(nanotime())
- procs := numCPUStartup
+ var procs int32
if n, ok := strconv.Atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
+ sched.customGOMAXPROCS = true
+ } else {
+ // Use numCPUStartup for initial GOMAXPROCS for two reasons:
+ //
+ // 1. We just computed it in osinit, recomputing is (minorly) wasteful.
+ //
+ // 2. More importantly, if debug.containermaxprocs == 0 &&
+ // debug.updatemaxprocs == 0, we want to guarantee that
+ // runtime.GOMAXPROCS(0) always equals runtime.NumCPU (which is
+ // just numCPUStartup).
+ procs = defaultGOMAXPROCS(numCPUStartup)
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
@@ -1714,6 +1727,7 @@ func startTheWorldWithSema(now int64, w worldStop) int64 {
procs := gomaxprocs
if newprocs != 0 {
procs = newprocs
+ sched.customGOMAXPROCS = newprocsCustom
newprocs = 0
}
p1 := procresize(procs)
@@ -6146,6 +6160,7 @@ func sysmon() {
checkdead()
unlock(&sched.lock)
+ lastgomaxprocs := int64(0)
lasttrace := int64(0)
idle := 0 // how many cycles in succession we had not wokeup somebody
delay := uint32(0)
@@ -6259,6 +6274,11 @@ func sysmon() {
startm(nil, false, false)
}
}
+ // Check if we need to update GOMAXPROCS at most once per second.
+ if debug.updatemaxprocs != 0 && lastgomaxprocs+1e9 <= now {
+ sysmonUpdateGOMAXPROCS()
+ lastgomaxprocs = now
+ }
if scavenger.sysmonWake.Load() != 0 {
// Kick the scavenger awake if someone requested it.
scavenger.wake()
@@ -6526,6 +6546,97 @@ func schedtrace(detailed bool) {
unlock(&sched.lock)
}
+type updateGOMAXPROCSState struct {
+ lock mutex
+ g *g
+ idle atomic.Bool
+
+ // Readable when idle == false, writable when idle == true.
+ procs int32 // new GOMAXPROCS value
+}
+
+var (
+ updateGOMAXPROCS updateGOMAXPROCSState
+
+ updatemaxprocs = &godebugInc{name: "updatemaxprocs"}
+)
+
+// Start GOMAXPROCS update helper goroutine.
+//
+// This is based on forcegchelper.
+func defaultGOMAXPROCSUpdateEnable() {
+ go updateGOMAXPROCSHelper()
+}
+
+func updateGOMAXPROCSHelper() {
+ updateGOMAXPROCS.g = getg()
+ lockInit(&updateGOMAXPROCS.lock, lockRankUpdateGOMAXPROCS)
+ for {
+ lock(&updateGOMAXPROCS.lock)
+ if updateGOMAXPROCS.idle.Load() {
+ throw("updateGOMAXPROCS: phase error")
+ }
+ updateGOMAXPROCS.idle.Store(true)
+ goparkunlock(&updateGOMAXPROCS.lock, waitReasonUpdateGOMAXPROCSIdle, traceBlockSystemGoroutine, 1)
+ // This goroutine is explicitly resumed by sysmon.
+
+ stw := stopTheWorldGC(stwGOMAXPROCS)
+
+ // Still OK to update?
+ lock(&sched.lock)
+ custom := sched.customGOMAXPROCS
+ unlock(&sched.lock)
+ if custom {
+ startTheWorldGC(stw)
+ return
+ }
+
+ // newprocs will be processed by startTheWorld
+ //
+ // TODO(prattmic): this could use a nicer API. Perhaps add it to the
+ // stw parameter?
+ newprocs = updateGOMAXPROCS.procs
+ newprocsCustom = false
+
+ startTheWorldGC(stw)
+
+ // We actually changed something.
+ updatemaxprocs.IncNonDefault()
+ }
+}
+
+func sysmonUpdateGOMAXPROCS() {
+ // No update if GOMAXPROCS was set manually.
+ lock(&sched.lock)
+ custom := sched.customGOMAXPROCS
+ curr := gomaxprocs
+ unlock(&sched.lock)
+ if custom {
+ return
+ }
+
+ // Don't hold sched.lock while we read the filesystem.
+ procs := defaultGOMAXPROCS(0)
+
+ if procs == curr {
+ // Nothing to do.
+ return
+ }
+
+ // Sysmon can't directly stop the world. Run the helper to do so on our
+ // behalf. If updateGOMAXPROCS.idle is false, then a previous update is
+ // still pending.
+ if updateGOMAXPROCS.idle.Load() {
+ lock(&updateGOMAXPROCS.lock)
+ updateGOMAXPROCS.procs = procs
+ updateGOMAXPROCS.idle.Store(false)
+ var list gList
+ list.push(updateGOMAXPROCS.g)
+ injectglist(&list)
+ unlock(&updateGOMAXPROCS.lock)
+ }
+}
+
// schedEnableUser enables or disables the scheduling of user
// goroutines.
//