线上CPU 100%故障定位和应急处理

线上 CPU 100% 是典型的性能故障,会直接导致服务响应延迟、超时甚至不可用,处理核心原则是先应急止损,再定位根因,最后彻底解决并预防。以下是分阶段的详细方案,涵盖 Linux 环境下的通用操作及 Java、Go 等主流语言的针对性工具。

一、应急处理:优先恢复业务可用

应急阶段的目标是快速降低 CPU 负载,保障业务不中断,无需等待根因定位,核心操作围绕 “分流、扩容、隔离” 展开:

1. 快速判断故障范围

首先通过监控平台(如 Prometheus、Grafana、Zabbix)确认:
  • 单个实例还是集群批量CPU 高?(单个可能是实例异常,批量可能是代码 / 配置 / 流量问题)
  • 高 CPU 是否伴随流量突增(如秒杀、爬虫)、新发布版本(灰度 / 全量后出现)?

2. 核心应急操作(按优先级排序)

操作场景 具体步骤 注意事项
单个实例 CPU 高 1. 临时下线该实例(通过网关 / 负载均衡剔除,如 Nginx、K8s Service);
2. 重启实例(systemctl restart 服务名或容器重启);
3. 重启后观察 CPU 是否恢复,若恢复则保留日志待后续分析。
避免直接重启集群,仅下线异常实例,防止业务中断。
集群批量 CPU 高 1. 紧急扩容(增加实例数,如 K8s HPA 自动扩容或手动扩缩容);
2. 流量限流 / 降级(通过网关限制过载接口的 QPS,如 Sentinel、Nginx 限流);
3. 若刚发布新版本,立即回滚到上一稳定版本。
扩容仅临时分担负载,需后续定位根因;回滚前确认新版本是故障触发点。
流量突增导致 CPU 高 1. 识别突增流量来源(如爬虫、恶意请求),通过防火墙 / 网关拦截 IP;
2. 对非核心接口降级(返回默认值,如 “服务繁忙”),优先保障核心业务。
需区分 “正常流量峰值” 和 “异常流量”,避免误拦截正常用户。

二、故障定位:从系统层到应用层拆解

应急后需精准定位根因,避免问题复现。定位逻辑遵循 **“进程→线程→代码 / 任务”** 的递进思路,依赖 Linux 系统命令 + 应用层工具。

阶段 1:系统层定位 —— 找到高 CPU 的进程与线程

Linux 环境下通过命令行快速锁定 “罪魁祸首”,核心工具:toppspidstat

步骤 1:找到高 CPU 的进程(PID)

执行top命令(按P键按 CPU 使用率排序),重点关注两列:
  • %us:用户态 CPU 占比(若高,说明应用代码消耗 CPU 多,如死循环、计算密集);
  • %sy:内核态 CPU 占比(若高,说明系统调用频繁,如频繁 IO、线程切换)。
示例输出中,PID=1234 的进程 CPU 使用率 100%,用户态占比 98%,说明是应用代码问题:
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 1234 appuser   20   0 20.5g   8.3g   1.2g R 100.0  21.5   5:30.12 java

步骤 2:找到进程内高 CPU 的线程(TID)

通过top -Hp <PID>查看进程内线程的 CPU 占用(按P排序),获取高 CPU 线程的TID(线程 ID,十进制):
top -Hp 1234  # 查看PID=1234的进程内线程
示例中,TID=1245 的线程 CPU 占比 99%,是核心线程。

步骤 3:将 TID 转为十六进制(适配 Java 等工具)

Java 的jstack、Go 的pprof等工具需用十六进制线程 ID匹配栈信息,执行以下命令转换(以 TID=1245 为例):
printf "%x\n" 1245  # 输出:4db(十六进制,小写)

阶段 2:应用层定位 —— 分析线程栈与业务逻辑

根据应用语言选择工具,核心是获取线程的调用栈信息,定位到具体代码行。

场景 1:Java 应用(最常见)

依赖 JDK 自带工具:jpsjstackjstat(排查 GC 相关 CPU 高)。
1. 导出线程栈日志
jstack导出目标进程的全量线程栈,并搜索十六进制 TID(如 4db)对应的线程:
# 导出栈信息到文件(避免终端刷屏)
jstack 1234 > java_stack.log

# 搜索高CPU线程的栈信息(TID=4db,注意小写)
grep -A 20 "nid=0x4db" java_stack.log
2. 分析栈日志核心信息
重点关注线程状态(RUNNABLE最可能导致 CPU 高)和调用栈:
  • 若线程状态为RUNNABLE,且调用栈反复出现某段代码(如com.example.service.UserService.calculate()),说明该方法存在死循环高频计算
  • 若线程状态为WAITING/BLOCKED但 CPU 高,需排查是否有自旋锁(如AtomicInteger循环 CAS)或频繁 GC
示例栈日志(死循环特征):
"Thread-1" #10 prio=5 os_prio=0 cpu=99999ms elapsed=10.0s tid=0x00007f8a1c004800 nid=0x4db runnable [0x00007f8a08f9e000]
   java.lang.Thread.State: RUNNABLE
        at com.example.service.UserService.calculate(UserService.java:45)  # 反复执行该方法
        at com.example.service.UserService.process(UserService.java:30)
        at java.lang.Thread.run(Thread.java:748)
3. 排查是否为 GC 导致 CPU 高
top中 Java 进程 % us 高,但无明显死循环线程,需检查 GC 情况(频繁 GC 会导致 CPU 占用飙升):
# 每1秒输出一次GC统计(持续观察)
jstat -gc 1234 1000
重点关注:
  • FGC(Full GC)次数:若每秒多次 FGC,且FGCT(Full GC 耗时)持续增加,说明内存泄漏(对象无法回收,JVM 反复 GC);
  • YGCT(Young GC 耗时):若 YGCT 过高,可能是对象创建过于频繁(如循环内 new 对象)。

场景 2:Go 应用

依赖 Go 自带工具pprof和系统工具gdb,核心定位goroutine 泄漏高频计算
1. 开启 pprof 监控(两种方式)
  • 方式 1:代码中嵌入 pprof(推荐线上):
    import _ "net/http/pprof" // 导入后自动注册pprof接口
    func main() {
        go func() {
            log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) // 暴露pprof端口
        }()
        // 业务代码
    }
    
  • 方式 2:离线导出 profile(无端口时):
    go tool pprof -seconds 30 http://localhost:6060/debug/pprof/profile?seconds=30
    # 30秒内采样CPU使用,生成profile文件
    
2. 分析 CPU 占用热点
进入 pprof 交互界面后,执行top查看高 CPU 的函数:
(pprof) top 10  # 显示前10个高CPU函数
Showing nodes accounting for 990ms, 99% of 1000ms total
Dropped 10 nodes (cum <= 5ms)
      flat  flat%   sum%        cum   cum%
     500ms 50.00% 50.00%      500ms 50.00%  com.example/calc.Sum
     490ms 49.00% 99.00%      490ms 49.00%  com.example/loop.Run  # 高频循环函数
执行list <函数名>查看具体代码行:
(pprof) list Run
Total: 1s
ROUTINE ======================== com.example/loop.Run in /app/loop.go
  490ms      490ms (flat, cum) 49.00% of Total
         .          .     1:package loop
         .          .     2:
         .          .     3:func Run() {
         .          .     4:    for { // 死循环!
  490ms      490ms     5:        i := 0
         .          .     6:        i++
         .          .     7:    }
         .          .     8:}

场景 3:Python 应用

依赖工具py-spy(非侵入式,适合线上)或cProfile(侵入式,适合线下)。
1. 用 py-spy 查看 CPU 热点
# 安装py-spy(无需修改代码,直接attach进程)
pip install py-spy

# 查看PID=1234的Python进程CPU使用,输出到文件
py-spy record -o python_profile.svg --pid 1234
打开python_profile.svg,通过火焰图直观看到高 CPU 的函数调用链(火焰越高,CPU 占用越高)。

阶段 3:根因验证与确认

定位到疑似代码后,需结合业务日志监控数据验证:
  1. 查看应用日志(如tail -f /var/log/app.log),确认高 CPU 线程对应的业务场景(如某个接口、定时任务);
  2. 对比故障前后的监控(如接口 QPS、耗时、线程数),确认是否与定位的代码逻辑关联(如 QPS 突增导致高频调用某计算函数);
  3. 线下复现:在测试环境模拟相同参数(如输入导致死循环的异常数据),验证 CPU 是否飙升,确认根因。

三、根本解决:针对根因修复

根据定位结果,针对性解决问题,常见根因及方案如下:
常见根因 解决方案
死循环(代码逻辑错误) 1. 修复循环条件(如将while(true)改为有限条件,处理异常边界值);
2. 代码审查时重点检查循环、递归逻辑。
频繁 GC / 内存泄漏 1. 排查内存泄漏点(如未关闭的连接、静态集合无限添加);
2. 优化对象创建(如使用对象池);
3. 调整 JVM 参数(如增大堆内存-Xmx)。
资源竞争(锁冲突) 1. 减小锁粒度(如用ConcurrentHashMap代替HashMap+ReentrantLock);
2. 用无锁结构(如Atomic类);
3. 避免同步阻塞(如异步化处理)。
计算密集型任务突增 1. 拆分任务(如分布式计算);
2. 异步化处理(如用消息队列削峰);
3. 扩容计算节点,单独部署计算任务。
第三方服务 / 接口超时 1. 增加超时时间(避免线程长期阻塞);
2. 降级熔断(如 Hystrix,超时后返回默认值);
3. 替换不稳定的第三方服务。

四、复盘与预防:避免再次发生

故障解决后需复盘,形成闭环,核心动作:

1. 完善监控与告警

  • 新增 CPU 相关告警:如 “单实例 CPU>90% 持续 30 秒”“集群 CPU 平均 > 80%”“Java 进程 FGC>5 次 / 分钟”;
  • 补充业务监控:如接口 QPS、耗时、线程数、goroutine 数,提前发现流量或代码异常。

2. 优化发布与测试流程

  • 灰度发布:新代码先小流量(如 10%)验证,观察 CPU 等指标,无异常再全量;
  • 压力测试:上线前用 JMH、JMeter 模拟高流量,验证 CPU 是否存在瓶颈;
  • 代码审查:重点检查死循环、锁使用、内存申请、第三方调用超时逻辑。

3. 文档沉淀

  • 记录本次故障的 “时间线、根因、解决方案、优化措施”;
  • 整理《线上 CPU 100% 应急手册》,明确不同角色的职责(如运维负责应急止损,开发负责定位根因)。

总结

线上 CPU 100% 处理的核心是 “快止损、准定位、彻解决”:应急阶段优先保障业务可用,定位阶段通过 “系统命令 + 应用工具” 层层拆解,解决阶段针对性修复代码或配置,最后通过复盘建立预防机制。熟练掌握topjstackpprof等工具,可大幅提升定位效率,减少故障影响范围。
阅读剩余
THE END