基本介绍
众所周知,GOMAXPROCS
是 Golang
提供的非常重要的一个环境变量设定。通过设定 GOMAXPROCS
,用户可以调整 Runtime Scheduler
中 Processor
(简称P
)的数量。由于每个系统线程,必须要绑定 P
才能真正地进行执行。所以 P
的数量会很大程度上影响 Golang Runtime
的并发表现。GOMAXPROCS
的默认值是 CPU
核数,而以 Docker
为代表的容器虚拟化技术,会通过 cgroup
等技术对 CPU
资源进行隔离。以 Kubernetes
为代表的基于容器虚拟化实现的资源管理系统,也支持这样的特性。这类技术对 CPU
的隔离限制,会影响到 Golang
中的 GOMAXPROCS
,进而影响到 Golang Runtime
的并发表现。
理论上来讲,过多的调度器P
设置以及过少的CPU
资源限制,将会增大调度器内部的上下文切换以及资源竞争成本。如何优雅地让Golang
程序资源设置与外部cgroup
资源限制匹配是本文讨论的主题。
Golang资源设置
Golang
引擎已经在运行时提供了两个比较重要的资源设置参数,通过环境变量来设置/改变:
GOMAXPROCS
:设置调度器使用的Processor
数量,进而控制并发数量。GOMEMLIMIT
:设置进程可用的最大内存大小,进而影响GC
的表现。的表现。在Go 1.19
版本后支持。需要注意,在内存泄漏的程序中,该设置会延迟OOM
的出现,但不会阻止OOM
的出现。
测试过程
测试环境
使用的Kubernetes
集群,每台Node
节点32
核128G
内存。
我们通过给容器设置环境变量,来实现对容器中Golang
程的资源设置:
Code Block |
---|
|
env:
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu |
不限制资源
不传递环境变量
我们先来看看不设置资源limits
的时候也不传递环境变量的时候Golang
程序使用的资源情况:
Code Block |
---|
|
apiVersion: v1
kind: Pod
metadata:
name: gomaxprocs-test
spec:
containers:
- name: main
image: golang:1.22
imagePullPolicy: IfNotPresent
args:
- bash
- -c
- |
cat <<EOF > /tmp/code.go
package main
import (
"fmt"
"os"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("GOMAXPROCS(env): %s\n", os.Getenv("GOMAXPROCS"))
fmt.Printf("GOMEMLIMIT(env): %s\n", os.Getenv("GOMEMLIMIT"))
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("GOMEMLIMIT: %d\n", debug.SetMemoryLimit(-1))
}
EOF
go run /tmp/code.go |
运行后,我们查看终端输出结果:
Code Block |
---|
|
$ kubectl logs pod/gomaxprocs-test
GOMAXPROCS(env):
GOMEMLIMIT(env):
GOMAXPROCS: 32
GOMEMLIMIT: 9223372036854775807 |
可以看到,Golang
程序使用的是当前运行Node
的CPU
并且内存没有任何的限制。 并且内存没有任何的限制。 其中默认的内存值9223372036854775807
表示int64
类型的最大值。
传递环境变量
我们再来看看不设置资源limits
但传递环境变量的时候Golang
程序使用的资源情况:
Code Block |
---|
|
apiVersion: v1
kind: Pod
metadata:
name: gomaxprocs-test
spec:
containers:
- name: main
image: golang:1.22
imagePullPolicy: IfNotPresent
args:
- bash
- -c
- |
cat <<EOF > /tmp/code.go
package main
import (
"fmt"
"os"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("GOMAXPROCS(env): %s\n", os.Getenv("GOMAXPROCS"))
fmt.Printf("GOMEMLIMIT(env): %s\n", os.Getenv("GOMEMLIMIT"))
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("GOMEMLIMIT: %d\n", debug.SetMemoryLimit(-1))
}
EOF
go run /tmp/code.go
resources:
env:
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu |
运行后,我们查看终端输出结果:
Code Block |
---|
|
$ kubectl logs pod/gomaxprocs-test
GOMAXPROCS(env): 32
GOMEMLIMIT(env): 127719542784
GOMAXPROCS: 32
GOMEMLIMIT: 127719542784 |
可以看到,Golang
程序使用的是当前运行Node
的CPU
并且内存没有任何的限制。在没有设置资源limits
的情况下,环境变量中的limits.cpu
及limits.memory
传递的是当前Node
节点的CPU
和内存值。
设置资源限制
不传递环境变量
如果只设置资源limits
但是不传递环境变量会怎么样呢?我们来试试看。
Code Block |
---|
|
apiVersion: v1
kind: Pod
metadata:
name: gomaxprocs-test
spec:
containers:
- name: main
image: golang:1.22
imagePullPolicy: IfNotPresent
args:
- bash
- -c
- |
cat <<EOF > /tmp/code.go
package main
import (
"fmt"
"os"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("GOMAXPROCS(env): %s\n", os.Getenv("GOMAXPROCS"))
fmt.Printf("GOMEMLIMIT(env): %s\n", os.Getenv("GOMEMLIMIT"))
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("GOMEMLIMIT: %d\n", debug.SetMemoryLimit(-1))
}
EOF
go run /tmp/code.go
resources:
limits:
cpu: 200m
memory: 512M
requests:
cpu: 100m
memory: 128M |
运行后,我们查看终端输出结果:
Code Block |
---|
|
$ kubectl logs pod/gomaxprocs-test
GOMAXPROCS(env):
GOMEMLIMIT(env):
GOMAXPROCS: 32
GOMEMLIMIT: 9223372036854775807 |
可以看到,同上面没有设置资源限制一样,Golang
程序仍然使用的是默认资源设置,并不能感知cgroup
的资源限制。
传递环境变量
我们仍然需要环境变量来通知容器中的Golang
程序cgroup
资源的限制。
Code Block |
---|
|
apiVersion: v1
kind: Pod
metadata:
name: gomaxprocs-test
spec:
containers:
- name: main
image: golang:1.22
imagePullPolicy: IfNotPresent
args:
- bash
- -c
- |
cat <<EOF > /tmp/code.go
package main
import (
"fmt"
"os"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("GOMAXPROCS(env): %s\n", os.Getenv("GOMAXPROCS"))
fmt.Printf("GOMEMLIMIT(env): %s\n", os.Getenv("GOMEMLIMIT"))
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("GOMEMLIMIT: %d\n", debug.SetMemoryLimit(-1))
}
EOF
go run /tmp/code.go
resources:
limits:
cpu: 200m
memory: 512M
requests:
cpu: 100m
memory: 128M
env:
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu |
运行后,我们查看终端输出结果:
Code Block |
---|
|
$ kubectl logs pod/gomaxprocs-test
GOMAXPROCS(env): 1
GOMEMLIMIT(env): 512000000
GOMAXPROCS: 1
GOMEMLIMIT: 512000000 |
可以看到,Golang
程序的CPU
及内存受限制给定的limits
限制。并且CPU
的限制使用的是limits
中CPU核心的中CPU
核心数的向上取整设置,例如给定的CPU limits
为1200m
100m
时,容器Golang
程序获取到的GOMAXPROCS
变量将会是2
变量是1
。
一些总结
容器中的Golang
程序并不能动态感知外部cgroup
的资源限制,需要通过环境变量的方式来同步外部cgroup
对资源的限制到容器中的Golang
程序中。此外,也有一些第三方开源组件可以使得Golang
程序动态感知cgroup
的资源限制:https://github.com/uber-go/automaxprocs 其原理也是通过程序动态读取cgroup
配置来实现的。
参考资料