内容提要
本文介绍Off-CPU分析,用于定位CPU使用率低但延迟高的问题。Off-CPU时间指线程被阻塞等待的时间,与On-CPU互补。文章归纳四类根因:同步网络I/O、子进程调用、文件/管道I/O阻塞及CPU争用,并给出真实案例,如io.popen阻塞致吞吐量提升150倍。通过双层调用栈分析可追踪到具体代码行,OpenResty XRay工具可自动定位瓶颈。
延伸解读
如何判断是否属于 Off-CPU 问题
当 CPU 使用率低但延迟高时,先确认是否属于 off-CPU 问题。典型症状是:top 显示 CPU 卡在低位、access log 持续增长、加压 CPU 也不涨。但并非所有尾延迟尖刺都是 off-CPU 导致,例如文中 50 万 QPS 网关的 244 毫秒尖刺实为 on-CPU 正则回溯问题。因此,需通过 off-CPU 分析确认线程是阻塞在等待还是消耗在计算上,避免误判。
事件驱动架构中阻塞的放大效应
在 Nginx/OpenResty 这类事件驱动服务器中,worker 线程唯一应阻塞的位置是事件循环的 epoll_wait。任何其他位置的 off-CPU 时间都会卡住整个事件循环,导致该 worker 上所有并发请求同时等待。例如,io.popen 阻塞案例中,单次阻塞 75 毫秒时,所有连接都空等,直接造成长尾延迟尖刺。因此,定位 off-CPU 瓶颈时,需特别关注事件循环内的同步阻塞调用。
双层调用栈分析的必要性
仅看 C 级调用栈只能知道阻塞在 poll 或 waitpid 等系统调用,但无法定位到具体业务代码。必须结合语言级调用栈,才能将系统调用映射到源文件和行号。文中案例均通过双层分析追踪到具体行号,如 Perl 第 66 行、Go 第 46 行、Python 第 12 行。这种可操作性正是 off-CPU 分析的核心价值,也是自动化工具如 OpenResty XRay 的优势所在。
CPU 争用型 off-CPU 的识别要点
CPU 争用型 off-CPU 与前三类不同,线程并非主动等待 I/O,而是就绪但抢不到核。识别方法是观察 off-CPU 时间是否出现在纯计算函数上,如 mpi_mul_hlp、free。文中案例因缺少 worker_cpu_affinity 配置,导致 worker 频繁被调度,产生额外上下文切换。这类问题需从调度配置入手,而非阻塞调用点。
Q&A
什么是 off-CPU 时间?
Off-CPU 时间是指线程离开 CPU 后到重新回到 CPU 之间的时间,期间线程无法执行指令。常见原因包括网络 I/O 等待、子进程等待、文件/管道读写阻塞、锁竞争和 CPU 调度争用。
为什么 CPU 使用率很低但延迟很高?
因为线程大部分时间被阻塞在等待上,而不是在 CPU 上执行有用工作。典型根因包括同步网络调用、同步子进程调用、同步文件/管道 I/O,以及 CPU 资源争用。
off-CPU 分析中,如何定位到具体的代码行?
通过双层调用栈分析:C/系统层级显示系统调用栈(如 select、poll、waitpid),语言层级显示业务代码调用栈,将系统调用映射到具体业务函数、源文件和行号。
同步网络 I/O 导致 off-CPU 的典型根因链是什么?
例如 Perl 进程中,阻塞路径为 select 系统调用 → Perl_pp_select → Net::HTTP::Methods::can_read → read_response_headers → 业务函数 remote_fetch,最终定位到源文件第 66 行。
子进程调用如何导致 off-CPU 阻塞?
程序通过 exec/subprocess 启动外部进程后,调用 waitpid 等待其退出,期间调用线程被阻塞。例如 Go 服务中 exec.Command("/usr/bin/sleep", "0.01") 和 cmd.Run() 导致阻塞。
在 OpenResty 中,io.popen 和 file:read() 阻塞如何影响性能?
它们会阻塞整个事件循环,导致所有并发连接等待。案例中修复后单核吞吐量从 126 RPS 提升到 18,537 RPS,提升 150 倍。
CPU 争用型 off-CPU 阻塞的特点是什么?
线程已就绪但抢不到 CPU 核,被迫在就绪队列等待。识别方法是看 off-CPU 时间是否出现在纯 CPU 计算函数上,如 mpi_mul_hlp、free。
如何在不修改代码的情况下定位 off-CPU 瓶颈?
使用 OpenResty XRay 的 Guided Analysis,选择“Low CPU usage and cannot go up”问题类型,直接分析运行中的进程,无需安装额外工具、修改代码或重启进程。