Go version
golang:1.25rc1 / ImageID: d86a0805aa18
Output of go env
in your module/workspace:
I am running Go using the golang:1.25rc1 image
What did you do?
I am running a Go service with CPU limits set to 250m, but GOMAXPROCS is reporting 2 instead of 1.
You can see that setting under /zarf/k8s/dev/sales/dev-sales-patch-deploy.yaml
in the bill/bug
branch.
I am running on a: - Apple M2 Max OS 15.5 (24F74) - Docker(Engine: 28.2.2, Compose: v2.36.2-desktop.1, Credential Helper: v0.9.3, Kubernetes: v1.32.2) - Resources: CPU: 4, Memory: 12 GiB
Here are the steps to reproduce the problem:
- Clone
https://github.com/ardanlabs/service
- git switch bill/bug
- Run
make bug-run
to build the images, start K8s, and apply manifests. - Run
make live
to see the GOMAXPROCS number
All of the Docker and K8s stuff is under /zarf/
You can bring down the system using make dev-down
You can restart the pod is necessary using make dev-restart
What did you see happen?
After running make live
I see that the GOMAXPROCS variable is 2.
{"status":"up","build":"0.0.1","host":"ardan-starter-cluster-control-plane","name":"sales-79b7b6b5d5-qhx6q","podIP":"172.19.0.2","node":"ardan-starter-cluster-control-plane","namespace":"sales-system","GOMAXPROCS":2}
What did you expect to see?
After running make live
I expected the service to have a GOMAXPROCS of 1.
{"status":"up","build":"0.0.1","host":"ardan-starter-cluster-control-plane","name":"sales-79b7b6b5d5-qhx6q","podIP":"172.19.0.2","node":"ardan-starter-cluster-control-plane","namespace":"sales-system","GOMAXPROCS":1}
Comment From: gabyhelp
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: seankhliao
This is working as intended from #73193
Comment From: ardan-bkennedy
I do not believe what I am seeing on my machine is working as designed.
In my workshop this week, I had students running Linux and not MacOS on the host machine, we saw the correct number of GOMAXPROCS=1 on those machines. A GOMAXPROCS of 2 is not the correct setting for a 250m CPU limit.
@seankhliao @prattmic I would like this issue to be re-opened please.
Comment From: ardan-bkennedy
Another thing after reading more of the proposal.
I am confused how my students saw a GOMAXPROCS=1 unless they forgot to comment out the YAML below, which I did check when they told me they were getting the right number.
env:
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu
Why use more threads than you have cores when you know you will just eat the quota faster and get less work done over time? I don't think you can consider the GC in this scenario for adding a second thread when the limit is set at or below 1000m.
I ran my small performance test as well and GOMAXPROCS=1 is always faster than any larger number when using 250m, which is expected. Not by a little under the small load I apply.
Comment From: seankhliao
From the proposal:
The CPU quota limit specifies a minimum of GOMAXPROCS=2. That is, with a quota less than or equal to 1, we will round up GOMAXPROCS all the way to 2. GOMAXPROCS=1 disables all parallelism in the Go scheduler, which can cause surprising effects like GC workers temporarily “pausing” the application while the Go runtime switches back and forth between application goroutines and GC worker goroutines. Additionally, I consider a CPU quota less than 1 to be an indication that a workload is bursty, since it must be to avoid hitting the limit. Thus we can take advantage of the bursty nature to allow the runtime itself to burst and avoid GOMAXPROCS=1 pitfalls. If the number of logical or affinity mask CPUs is 1, we will still set GOMAXPROCS=1, as there is definitely no additional parallelism available
Load testing a < 1 cpu limit isn't quite the intended use case.
Comment From: ardan-bkennedy
I've been teaching for a while now how to set GOMAXPROCS and this goes against what I've learned and teach. It goes against what Uber has been doing. I haven't seen any optimal performance with this approach.
I've wondered about the GC when being single threaded, but like I said it hasn't been a problem AFAIK.
This is creating a complicated problem.
Plus, I wonder why in my class with students that had a Linux host environment I got the expected value?
@prattmic I would love your thoughts.
Comment From: prattmic
Hi Bill, apologies this totally slipped my mind when we talked in person. For cgroup-based GOMAXPROCS, 2 is indeed the minimum setting. The idea is to allow GC in parallel with the application (instead of making GC effectively stop-the-world, as with GOMAXPROCS=1). Plus, applications with a limit of <1 CPU must by definition be bursty. Of course, whether applications actually prefer this behavior will of course vary from one application to another.
I will reopen w.r.t. the Linux behavior you saw. I agree that it seems they should have seen GOMAXPROCS=1.
In addition to explicit GOMAXPROCS=1, another possibility is that the CPU affinity (sched_getaffinity
) was limited to 1 CPU. In that case, Go will set GOMAXPROCS=1, as it means that Go can literally only run on a single physical core (as opposed to cgroup CPU limits which allow bursting on multiple cores).
Comment From: ardan-bkennedy
Thanks for the reply.
I can't reproduce the linux situation so I don't want you to spend too much time on that. I did have 2 students experience this, but like you said, maybe they had CPU affinity or something else going on.
I understand your reasoning for using 2, but since the limit time is reached twice as fast, I have been seeing software run slower. This leaves less time for both application work and GC. It might be interesting for you to try and run some load tests on services and see the results.
One thing I was going to try is to run my load test at 250m with GOMAXPROCS at 1, and then try the same load at 500m with GOMAXPROCS at 2. If this essentially gives me the same results, then I think I can provide guidance to people who have already been setting GOMAXPROCS and want to remove that code.