最近,我们的一个 Go 工作负载出现了意外的性能下降,CPU 使用率几乎是预期的 2 倍。原来的问题是我们没有明确设置GOMAXPROCS
Go 运行时参数。我们将回顾调查过程、发现的问题以及如何解决它。
背景
Metoro 是一个用于在 Kubernetes 中运行的系统的可观测性平台。为了收集关于集群及其运行的工作负载的遥测数据,我们在受监控的集群中部署了一个守护进程集。这个守护进程集在每个主机上创建一个名为节点代理的 pod,该 pod 收集关于工作负载的信息并将其发送出集群进行存储。
节点代理通过 eBPF 对许多内核操作进行检测,以自动生成 分布式跟踪 和其他遥测数据,这意味着节点代理的 CPU 使用率随着节点上对 pod 的请求数量而扩展。通常,一个节点代理可以预期使用大约 1 秒的 CPU 时间(在最新的 EC2 主机上)来处理 12,000 个 HTTP 请求。
问题
在部署到一个新的客户集群时,我们注意到一些节点代理使用的 CPU 比我们预期的要多得多。这个集群中的主机每分钟处理多达 200,000 个请求,所以在一分钟内,我们应该期望节点代理使用大约 17 秒的 CPU 时间(一个核心的 28%)来处理这些请求。然而,我们注意到节点代理使用了 30 秒的 CPU 时间(一个核心的 50%),这几乎是该工作负载预期的 2 倍。
幸运的是,Metoro 运行一个基于 eBPF 的 CPU 分析器,我们可以使用它来检查其自身的 CPU 使用情况。这就是我们看到的。
乍一看,有一些东西很突出:Go 运行时函数使用了大量的 CPU 时间。以下是突出显示 Go 运行时函数的同一图表。
CPU 使用的两个主要消费者是:
runtime.schedule
- 约 30%的 CPU 使用量runtime.gcBgMarkWorker
- 约 20%的 CPU 使用量runtime.Schedule
似乎负责找到要运行的 goroutine + 在操作系统线程上启动它们。runtime.gcBgMarkWorker
似乎负责确定哪些内存片段可以标记为稍后由垃圾收集器清理。
为了了解为什么会这样,我们在一个具有相同请求数量的开发集群中设置了一个复现环境。这就是我们看到的:
突出显示的区域是runtime.Schedule
- 这次只有 5%。runtime.gcBgMarkWorker
是另外的 6%。
我们的复现失败了。我们开始查看环境中的其他差异,直到我们查看了主机大小。在 Go 运行时函数上使用 50%CPU 时间的节点代理在一个 192 核心的主机上。我们的复现在一个 4 核心的主机上运行。我们在集群中添加了一个 192 核心的主机,再次运行实验。复现成功!50%的时间都花在了这两个 Go 运行时函数上。
这很令人困惑。为什么在 Go 程序本身没有做任何不同的情况下,Go 运行时在更大的主机上会使用 5 倍多的 CPU 使用率?
经过大量的谷歌搜索,我们发现了一些 问题 和 issue,将高运行时 CPU 使用率与GOMAXPROCS
联系起来。
那么GOMAXPROCS
是什么?根据 文档:GOMAXPROCS
变量限制了可以同时执行用户级 Go 代码的操作系统线程的数量。代表 Go 代码在系统调用中被阻塞的线程数量没有限制;这些不计算在GOMAXPROCS
限制内。
那么它是如何设置的呢?我们没有明确设置它,那么默认值是多少?
在 Go 源代码中:procs := ncpu if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { procs = n }
所以如果我们不设置它,它就是
ncpu
。ncpu
根据操作系统的不同而以不同的方式计算,在 Linux 上它会变得有点复杂:它最终是程序创建时 CPU 掩码中的设置位的数量。这通常是主机上的核心数量,但实际上可能会有所不同。
现在这就说得通了,这两个函数:runtime.Schedule
和runtime.gcBgMarkWorker
随着 Go 程序使用的操作系统进程数量而扩展,在我们的大主机上是 192,在我们的小主机上只有 4。这就是为什么它们使用了更多的 CPU 时间。
如果在容器化环境中运行,这个默认值不是特别有用。在这种环境中,很可能在同一主机上运行着许多其他容器,并且您的程序只分配了主机的一小部分。
在我们的案例中,我们在节点代理上有一个单个核心的默认 CPU 限制,这样我们就不会影响客户的工作负载。所以在我们的案例中,我们希望GOMAXPROCS
为 1,因为我们无论如何都不能利用超过 1 个核心,所以超过一个操作系统线程是多余的。
那么我们如何解决这个问题呢?
解决方案
我们需要将GOMAXPROCS
设置为一个更合理的值。这里有一些遇到相同问题的人的现有解决方案:
- https://github.com/uber-go/automaxprocs - 一个库,它以编程方式将
GOMAXPROCS
设置为容器 CPU 配额 Kubernetes downward api - 允许您通过环境变量将 pod / 容器字段暴露给正在运行的容器
我们总是在 k8s 中运行,所以我们决定在部署时使用向下 api 在节点代理容器上设置GOMAXPROCS
环境变量。它看起来像 这样。env: - name: GOMAXPROCS valueFrom: resourceFieldRef: resource: limits.cpu divisor: "1"
在我们的 192 核心主机上再次运行基准测试,我们得到了预期的 CPU 使用率和以下火焰图:
恢复正常!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。