容器环境-GOMAXPROCS参数设置

GOMAXPROCS

通过设定 GOMAXPROCS,用户可以调整调度器中 Processor(简称P)的数量。由于每个系统线程,必须要绑定 P ,P 才能把G交给 M 执行。所以 P 的数量会很大程度上影响 Go Runtime 的并发表现。GOMAXPROCS 在版本 1.5后的默认值是机器的 CPU 核数 (runtime.NumCPU)

设置GOMAXPROCS

package main

import (
"fmt"
"runtime"
)

func main() {
n := runtime.NumCPU() // 获取机器的CPU核心数
fmt.Printf("NumCPU: %d\n", n)
if n > 0 {
runtime.GOMAXPROCS(n) // 设置GOMAXPROCS为CPU核心数
}
}
  • 容器内运行输出的是宿主机的CPU核心数

解决方案

go get go.uber.org/automaxprocs

运行Debug:

docker run -d --cpus=2  -v /Users/joohwan/GolandProjects/go-tools:/app   golang:1.21.0   tail -f /dev/null
package main

import (
"fmt"
"runtime"

_ "go.uber.org/automaxprocs"
)

func main() {
fmt.Printf("NumCPU: %d\n", runtime.NumCPU())
}

原理

接口

automaxprocs 组件同时兼容 CGroupsCGroups2, 通过 queryer 接口来抽象隔离具体的版本实现。

type queryer interface {
CPUQuota() (float64, bool, error)
}

var (
_newCgroups2 = cg.NewCGroups2ForCurrentProcess
_newCgroups = cg.NewCGroupsForCurrentProcess
_newQueryer = newQueryer
)

func newQueryer() (queryer, error) {
cgroups, err := _newCgroups2()
if err == nil {
return cgroups, nil
}
if errors.Is(err, cg.ErrNotV2) {
return _newCgroups()
}
return nil, err
}

入口函数-init

通过func init,实现当我们导入包,就能自动init

package automaxprocs // import "go.uber.org/automaxprocs"

import (
"log"

"go.uber.org/automaxprocs/maxprocs"
)

func init() {
maxprocs.Set(maxprocs.Logger(log.Printf))
}

Set函数

func Set(opts ...Option) (func(), error) {
cfg := &config{
// 获取CPU 资源配额的方法
procs: iruntime.CPUQuotaToGOMAXPROCS,
roundQuotaFunc: iruntime.DefaultRoundFunc,
// 最小为1
minGOMAXPROCS: 1,
}
for _, o := range opts {
o.apply(cfg)
}

undoNoop := func() {
cfg.log("maxprocs: No GOMAXPROCS change to reset")
}

// Honor the GOMAXPROCS environment variable if present. Otherwise, amend
// `runtime.GOMAXPROCS()` with the current process' CPU quota if the OS is
// Linux, and guarantee a minimum value of 1. The minimum guaranteed value
// can be overridden using `maxprocs.Min()`.
// 如果设置了环境变量GOMAXPROCS,就用这个
if max, exists := os.LookupEnv(_maxProcsKey); exists {
cfg.log("maxprocs: Honoring GOMAXPROCS=%q as set in environment", max)
return undoNoop, nil
}
// 获取CPU配额
maxProcs, status, err := cfg.procs(cfg.minGOMAXPROCS, cfg.roundQuotaFunc)
if err != nil {
return undoNoop, err
}
// 为设置CPU配额,直接返回
if status == iruntime.CPUQuotaUndefined {
cfg.log("maxprocs: Leaving GOMAXPROCS=%v: CPU quota undefined", currentMaxProcs())
return undoNoop, nil
}
// 获取到当前的GOMAXPROCS
prev := currentMaxProcs()
undo := func() {
cfg.log("maxprocs: Resetting GOMAXPROCS to %v", prev)
runtime.GOMAXPROCS(prev)
}

switch status {
case iruntime.CPUQuotaMinUsed:
cfg.log("maxprocs: Updating GOMAXPROCS=%v: using minimum allowed GOMAXPROCS", maxProcs)
case iruntime.CPUQuotaUsed:
cfg.log("maxprocs: Updating GOMAXPROCS=%v: determined from CPU quota", maxProcs)
}

runtime.GOMAXPROCS(maxProcs)
return undo, nil
}

获取CPU 资源配额的方法

  • CPUQuotaToGOMAXPROCS
// CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
// to a valid GOMAXPROCS value. The quota is converted from float to int using round.
// If round == nil, DefaultRoundFunc is used.
func CPUQuotaToGOMAXPROCS(minValue int, round func(v float64) int) (int, CPUQuotaStatus, error) {
if round == nil {
round = DefaultRoundFunc
}
cgroups, err := _newQueryer()
if err != nil {
return -1, CPUQuotaUndefined, err
}

quota, defined, err := cgroups.CPUQuota()
if !defined || err != nil {
return -1, CPUQuotaUndefined, err
}

maxProcs := round(quota)
// 如果 CPU 配额比配置的最小值还要小
// 就以配置的最小值为准
return minValue, CPUQuotaMinUsed, nil
}
return maxProcs, CPUQuotaUsed, nil
}

newQueryer 函数

newQueryer 函数并没有判断当前系统的 CGroups 版本,而是优先获取 CGroups2 的配置,如果获取不到,再获取 CGroups 的配置

func newQueryer() (queryer, error) {
cgroups, err := _newCgroups2()
if err == nil {
return cgroups, nil
}
if errors.Is(err, cg.ErrNotV2) {
return _newCgroups()
}
return nil, err
}

获取 CGroups 版本的配置

NewCGroupsForCurrentProcess 函数用来获取 CGroups 版本的配置,具体的实现是 NewCGroups 函数完成的。

// NewCGroupsForCurrentProcess returns a new *CGroups instance for the current
// process.
func NewCGroupsForCurrentProcess() (CGroups, error) {
return NewCGroups(_procPathMountInfo, _procPathCGroup)
}

获取CPU信息

  • 通过读取"/proc/self/cgroup""/proc/self/cgroup"
	cgroupSubsystems, err := parseCGroupSubsystems(procPathCGroup)
const (
_procPathCGroup = "/proc/self/cgroup"
_procPathMountInfo = ""/proc/self/cgroup"
)

CPUQuota 方法

CGroups 版本的 CPU 资源配额是如何计算出来的,该功能由 CPUQuota 方法完成。

  • 优先读取:cpu.cfs_quota_uscpu.cfs_period_us字段

cpu.cfs_quota_us: CPU 周期内最多可使用的时间 cpu.cfs_period_us: CPU 周期时间

例如:如果 cpu.cfs_quota_uscpu.cfs_period_us 的 4 倍,表示允许容器使用 4 个 CPU Core

如果上述两个字段任意一个未设置,方法返回 -1,表示不限制对 CPU 资源的使用,如果两个字段都设置了,使用下面的公式返回资源配额:

  • cpu.cfs_quota_us / cpu.cfs_period_us
// CPUQuota returns the CPU quota applied with the CPU cgroup controller.
// It is a result of `cpu.cfs_quota_us / cpu.cfs_period_us`. If the value of
// `cpu.cfs_quota_us` was not set (-1), the method returns `(-1, nil)`.
func (cg CGroups) CPUQuota() (float64, bool, error) {
cpuCGroup, exists := cg[_cgroupSubsysCPU]
if !exists {
return -1, false, nil
}

cfsQuotaUs, err := cpuCGroup.readInt(_cgroupCPUCFSQuotaUsParam)
if defined := cfsQuotaUs > 0; err != nil || !defined {
return -1, defined, err
}

cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam)
if defined := cfsPeriodUs > 0; err != nil || !defined {
return -1, defined, err
}

return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil
}