Go 服务 CPU 使用率异常低:对运行中进程做 off-CPU 分析定位阻塞根因
请求源源不断地进来,CPU 使用率却始终上不去——问题往往不是代码算得慢,而是代码在阻塞等待。off-CPU 分析找的就是这些"在等而不在跑"的代码路径。本文分析一个线上 Go 服务:高负载下 CPU 卡在 2%,我们不改一行代码、不重新发版,用 OpenResty XRay 直接对运行中的进程做 off-CPU 分析,最终定位到一行阻塞的 shell 命令调用。
症状:请求涌入,Go 服务 CPU 使用率却上不去
首先运行 top 命令检查 CPU 的使用情况。
可以看到这个名为 chat-service 的进程。我们已经事先知道这个进程是用 Go 语言实现的。它的 CPU 使用率非常低,只有 2%。即使有很多请求进来,它也不会上升。(如果您遇到的是相反的症状——CPU 使用率过高——请看定位最耗 CPU 的 Go 代码路径。)
让我们看一下这个 golang 应用程序的访问日志。
可以看到,有许多客户端请求正在涌入,可是 CPU 使用率仍然很低。 这意味着有一些东西阻塞了 golang 代码高效运行。我们怎么才能找到原因呢?
用 OpenResty XRay 对运行中的 Go 进程做 off-CPU 分析
让我们把 OpenResty XRay 对准这个未经修改的进程,实时分析——不用埋点,也不用重启。打开 OpenResty XRay 的 Web 控制台,进入 Guided Analysis(引导式分析)页面,选择问题类型 “Low CPU usage and cannot go up”(CPU 使用率低且上不去)。
按向导选中目标 Go 进程(也就是我们在 top 里看到的那个 2% CPU 的进程),语言级别保持默认的 “Go”、分析时长保持默认的 300 秒,然后开始分析。跑完一轮采样即可停止——对这个例子来说已经够了。OpenResty XRay 会自动生成报告,识别出的问题类型正是 off-CPU:这个 Go 服务的时间都花在了阻塞等待上,而不是在执行。
报告的阻塞路径要自底向上看。最底下的 Go 函数是 Syscall6,一个带 6 个参数的系统调用——单看这一帧还判断不出是哪个系统调用,需要顺着相邻的帧找上下文。
相邻的 Process.wait 帧表明底层系统调用是 waitpid。再往上,标准库的 exec.Cmd.Run 说明程序正在运行一条系统 shell 命令并等待它完成。顺着这条路往上走,根因落到了我们自己的业务代码:chat.RateLimit 函数。
这个最显著的阻塞代码路径,是从下面这张 Go 级别的 off-CPU 火焰图中自动推导出来的。同样的 off-CPU 分析方法也适用于 Perl 和 Python 进程。
报告还给出了文字解释和建议:Syscall6 发起一个带 6 个参数的系统调用,这条路径自下而上经过 exec.Cmd.Run 到达 chat.RateLimit,而这个业务函数正在运行一条系统命令并等待它。
要直接跳到出问题的源码,把鼠标悬停在 chat.RateLimit 帧上。提示框里会给出确切的源文件和行号——chat/processor.go 第 46 行。
打开这个文件跳到第 46 行,瓶颈就在眼前:RateLimit 里调用了 exec.Command("/usr/bin/sleep", "0.01"),再执行 cmd.Run()——起一条 shell 命令并阻塞等待。现在优化掉它就很容易了。
全自动分析与日报、周报
您不一定要手动跑引导式分析。OpenResty XRay 也能自动监控您的线上进程,并在 Insights 页面生成日报和周报——同样的 off-CPU 代码路径和 CPU 使用率概览,自动为您产出。这样一来,引导式分析主要用于开发和演示场景。
常见问题
如何对 Go 程序做 off-CPU 分析?
直接对运行中的未修改进程做分析即可。在 OpenResty XRay 的引导式分析(Guided Analysis)里选择 “Low CPU usage and cannot go up”,选中目标 Go 进程,运行一轮采样。系统会自动生成分析报告,画出 Go 层面的 off-CPU 火焰图,并自动推导出最显著的阻塞代码路径——无需埋点、无需改代码、无需重新发版。
为什么 Go 服务在高负载下 CPU 使用率仍然很低?
因为操作系统线程阻塞在等待上,而不是在执行 Go 代码。本文案例中,业务函数 chat.RateLimit 通过 exec.Cmd.Run 运行 shell 命令并等待其完成(底层是 waitpid 系统调用),导致无论多少请求进来,CPU 使用率都停在 2%。
off-CPU 火焰图里的 Syscall6 是什么?
Syscall6 是 Go 运行时中发起带 6 个参数系统调用的函数。单看这一帧无法确定具体是哪个系统调用,需要结合相邻的帧来判断。在本文的火焰图中,相邻的 Process.wait 帧表明底层的系统调用是 waitpid。
关于 OpenResty XRay
OpenResty XRay 是一个动态追踪产品,它可以自动分析运行中的应用程序,以解决性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+、eBPF+、GDB 和 ODB。
关于作者
章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。
章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,并编写过 60 多个开源软件库。
关注我们
如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:
翻译
我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!






























