Go 在容器运行时要注意的细节

在云原生时代,Go语言由于在部署时只需要一个二进制文件就能够运行起来而备受青睐。

但有一个细节问题,如果不妥善处理,则很可能导致Go程序出现明显的性能下降和延迟。

1 问题描述

在Go语言中,Go scheduler的P数量非常重要,因为它会极大地影响Go在运行时的表现。在目前的Go语言中,P的数量默认是系统的CPU核数。

在容器化的环境中,Go程序所获取的CPU核数是错误的,它所获取的是宿主机的CPU核数。

即使容器和宿主机的CPU核数是共享的,但在集群中我们会针对每个Pod分配指定的核数,因此实际上我们需要的是Pod的核数,而不是宿主机的CPU核数。

2 会造成什么后果

前文曾提到Go的M: N调度模型,其要求M必须与P进行绑定,然后才能不断地在M上循环寻找可运行的G来执行相应的任务。

注意,M必须与P进行绑定,其绑定的这个P,要求必须是空闲状态。但在容器化的部署环境中,P的数量由于被“错误”设置,因此拥有大量空闲的P。可以这样理解,只要有足够多的M,那么P就可以都被绑定。

这时又产生了另外一个问题,M的数量是否会不断增加呢?答案是会的。在程序运行过程中,由于产生了网络I/O阻塞,导致M会随着程序的不断执行而不断增加,,即能够达到前面假设的情况。最终导致Go程序的延迟加大,程序响应缓慢。

3 解决方法

产生这个问题的本质原因是Go程序没有正确地获得我们所期望的CPU核数(应当获取具体分配给Pod的配额),因此解决方案有两种:

  • 结合部署情况,主动设置正确的GOMAXPROCS核数。
  • 通过cgroup信息,读取容器内的正确GOMAXPROCS核数。

目前,Go尚没有非常完美的办法来解决这个问题,因此这里推荐使用Uber公司推出的 uber-go/automaxprocs开源库,它会在Go程序运行时根据cgroup的挂载信息来修改 GOMAXPROCS核数,并基于一定规则选择一个最合适的数值。

使用方式如下:

import _ "go.uber.org/automaxprocs"
func main() {...}

我们只需在Go程序启动时进行引用即可,如果有特殊的需求,那么主动设置GOMAXPROCS也是可以的。


最后修改 March 23, 2023: 整理,减少顶级目录 (8b75fa89)