基本介绍

众所周知,GOMAXPROCSGolang 提供的非常重要的一个环境变量设定。通过设定 GOMAXPROCS,用户可以调整 Runtime SchedulerProcessor(简称P)的数量。由于每个系统线程,必须要绑定 P 才能真正地进行执行。所以 P 的数量会很大程度上影响 Golang Runtime 的并发表现。GOMAXPROCS 的默认值是 CPU 核数,而以 Docker 为代表的容器虚拟化技术,会通过 cgroup 等技术对 CPU 资源进行隔离。以 Kubernetes 为代表的基于容器虚拟化实现的资源管理系统,也支持这样的特性。这类技术对 CPU 的隔离限制,会影响到 Golang 中的 GOMAXPROCS,进而影响到 Golang Runtime 的并发表现。

更多关于调度器的介绍请参考:Goroutine调度器

理论上来讲,过多的调度器P设置以及过少的CPU资源限制,将会增大调度器内部的上下文切换以及资源竞争成本。如何优雅地让Golang程序资源设置与外部cgroup资源限制匹配是本文讨论的主题。

Golang资源设置

Golang引擎已经在运行时提供了两个比较重要的资源设置参数,通过环境变量来设置/改变:

  • GOMAXPROCS :设置调度器使用的Processor数量,进而控制并发数量。
  • GOMEMLIMIT :设置进程可用的最大内存大小,进而影响GC的表现。在Go 1.19版本后支持。需要注意,如果是在内存泄漏的程序中,该设置会延迟OOM的出现,但不会阻止OOM的出现。

测试过程

测试环境

使用的Kubernetes集群,每台Node节点32128G内存。

我们通过给容器设置环境变量,来实现对容器中Golang程的资源设置:

env:
- name: GOMEMLIMIT
  valueFrom:
    resourceFieldRef:
      resource: limits.memory
- name: GOMAXPROCS
  valueFrom:
    resourceFieldRef:
      resource: limits.cpu

不限制资源

不传递环境变量

我们先来看看不设置资源limits也不传递环境变量的时候Golang程序使用的资源情况:

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        

运行后,我们查看终端输出结果:

$ kubectl logs pod/gomaxprocs-test 
GOMAXPROCS(env): 
GOMEMLIMIT(env): 
GOMAXPROCS: 32
GOMEMLIMIT: 9223372036854775807

可以看到,Golang程序使用的是当前运行NodeCPU并且内存没有任何的限制。 其中默认的内存值9223372036854775807表示int64类型的最大值。

传递环境变量

我们再来看看不设置资源limits但传递环境变量的时候Golang程序使用的资源情况:

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       

运行后,我们查看终端输出结果:

$ kubectl logs pod/gomaxprocs-test 
GOMAXPROCS(env): 32
GOMEMLIMIT(env): 127719542784
GOMAXPROCS: 32
GOMEMLIMIT: 127719542784

可以看到,Golang程序使用的是当前运行NodeCPU并且内存没有任何的限制。在没有设置资源limits的情况下,环境变量中的limits.cpulimits.memory传递的是当前Node节点的CPU和内存值。 

设置资源限制

不传递环境变量

如果只设置资源limits但是不传递环境变量会怎么样呢?我们来试试看。

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   

运行后,我们查看终端输出结果:

$ kubectl logs pod/gomaxprocs-test 
GOMAXPROCS(env): 
GOMEMLIMIT(env): 
GOMAXPROCS: 32
GOMEMLIMIT: 9223372036854775807

 可以看到,同上面没有设置资源限制一样,Golang程序仍然使用的是默认资源设置,并不能感知cgroup的资源限制。

传递环境变量

我们仍然需要环境变量来通知容器中的Golang程序cgroup资源的限制。

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     

 运行后,我们查看终端输出结果:

$ kubectl logs pod/gomaxprocs-test 
GOMAXPROCS(env): 1
GOMEMLIMIT(env): 512000000
GOMAXPROCS: 1
GOMEMLIMIT: 512000000

 可以看到,Golang程序的CPU及内存受限制给定的limits限制。并且CPU的限制使用的是limitsCPU核心数的向上取整设置,例如给定的CPU limits100m时,容器Golang程序获取到的GOMAXPROCS变量是1

一些总结

容器中的Golang程序并不能动态感知外部cgroup的资源限制,需要通过环境变量的方式来同步外部cgroup对资源的限制到容器中的Golang程序中。此外,也有一些第三方开源组件可以使得Golang程序动态感知cgroup的资源限制:https://github.com/uber-go/automaxprocs 其原理也是通过程序动态读取cgroup配置来实现的。

参考资料






Content Menu

  • No labels