Worker死循环导致单核CPU打满时,应按四步处理:先用strace/mpstat/top确认空转特征;再通过gdb/jstack定位栈帧中的正则回溯或无限循环;接着kill-15止血并加backtrack_limit等兜底;最后验证epoll行为、添加监控与CI静态检查。

遇到 Worker 进程因死循环(比如低效正则回溯、空 while 循环、未休眠的自定义进程等)导致单核 CPU 瞬间打满,关键不是“压测”或“重启完事”,而是快速定位+精准干预+代码修复。整个过程要快、准、稳,避免误杀或掩盖根因。
第一步:确认是不是真死循环,而不是 I/O 卡住
Workerman/Swoole/Hyperf 类框架是事件驱动模型,正常时大量时间在 epoll_pwait 上挂起等待事件。若 CPU 满了但不是死循环,可能只是同步阻塞(如 curl_exec 未 Hook)——此时 CPU 不一定高,但请求堆积;而死循环的特征非常鲜明:
- 用 strace -tt -p PID 观察:如果每秒刷出几十上百行
epoll_pwait(..., timeout=0)或epoll_pwait(..., timeout=1)且返回值为 0(表示超时没事件),基本就是空转死循环 - 对比 mpstat -P ALL 1:只有一核 %usr 长期 ≥95%,其他核几乎空闲,且 %wa(IO 等待)极低
- 用 top -H 查看线程级 CPU:找那个 TID 占用率持续 99%+ 的线程,它大概率就是执行死循环的 Worker 线程
第二步:快速定位到具体代码位置
拿到高 CPU 的 TID 后,不能直接翻日志猜,要直击栈帧:
- 把 TID 转成十六进制:printf "%x\n" <TID>(例如 12345 → 3039)
- 如果是 PHP 应用(Workerman/Hyperf):gdb -p <PID> → 输入
thread apply all bt,搜索 hex TID 对应的线程栈,看最顶上几层函数调用,重点关注:
–pcntl_signal_dispatch或event_base_loop下是否卡在某个回调里
– 是否在preg_match/preg_replace中深度回溯(尤其带 .*?、嵌套括号、长文本匹配时)
– 是否在自定义AbstractProcess::handle()或Task中写了while(true) { ... }却漏了sleep(1)或Coroutine::sleep(1) - Java 应用同理:用 jstack <PID> | grep -A20 "3039",看线程状态是不是 RUNNABLE,栈顶是否停在
String.replaceAll、Pattern.matcher或某个无限 while 块内
第三步:安全止血 + 防复发
定位清楚后,别急着 kill -9:
- 先发 kill -15 <PID>,让 Worker 主动退出当前循环、释放资源;观察 3–5 秒是否自动结束
- 若无响应,再 kill -9;对多进程架构(如 Workerman 多 worker),可单独干掉问题进程,不影响其他 worker 继续服务
- 临时规避:在问题回调中加兜底保护,例如限制正则最大匹配步数(PHP 7.3+ 可设
ini_set('pcre.backtrack_limit', 100000))、给 while 循环加计数器或超时判断 - 长期修复:替换易回溯正则(用原子组
(?>...)、固化分组、非贪婪改贪婪)、把同步轮询改为协程 sleep、所有自定义循环必须带休眠或 yield
第四步:验证和监控加固
上线修复后,不能只看 CPU 回落:
- 用 strace 再抓几秒,确认 epoll_pwait 是否恢复为“长时间挂起 + 少量唤醒”,而非高频空转
- 在关键循环入口加日志或 Prometheus 计数器(如
loop_count_total{worker="task"}),便于下次快速发现异常增长 - CI 阶段加入静态检查:用工具扫描
while\(true\)、for \(;;\)、无 sleep 的循环,强制 require 注释说明休眠策略

















