凌晨3点17分,手机铃声划破寂静,值班同事焦急的声音传来:“系统卡死了!网站打不开!” 这大概是每个运维人最不想听到的声音。睡意瞬间全无,立刻进入战斗状态。
登录跳板机,尝试 SSH 连接生产服务器,敲入命令后光标闪烁,响应迟缓。这种黏腻的迟滞感已经预示着不祥。执行 top 命令,屏幕左上角的 CPU 使用率稳稳地贴在 100%。那一刻的感受,就像看到高速公路所有车道都被事故车堵死,后续车辆只能无奈等待。
对于线上业务而言,每一秒的延迟都意味着用户的流失和收入的损失,压力陡增。在云栈社区的技术交流中,类似的线上紧急故障处理经验是大家讨论的热点,毕竟快速恢复是稳定性的基石。
为什么 CPU 100% 如此棘手?
在我多年的运维经历里,CPU 使用率飙升至 100% 是最常见的性能问题之一。你可以把它想象成一个人的大脑试图同时处理一百件紧急事务,结果就是彻底“死机”,任何新任务都只能排队。
CPU 满载的连锁反应非常致命:
- 响应时间激增:用户端感知到的延迟呈指数级增长,体验直线下降。
- 资源挤兑:任务队列堆积,连带内存压力增大,甚至可能触发 OOM(内存溢出)导致进程被系统杀死。
- 系统失控:系统调度器自身资源不足,可能导致连 SSH 都无法登录,失去控制通道。
- 告警雪崩:监控系统基于超时的告警会像雪片一样飞来,掩盖真实根因。
最要命的是,当 CPU 已经达到 100% 时,连执行最基本的排查命令都可能变得异常缓慢或失败。这就像消防员赶赴火场,却发现消防车也被堵在了路上。
实战:我的3分钟定位法则
经历多次“深夜战役”后,我总结了一套固定的排查流程。关键在于保持冷静,按步骤进行,切忌毫无章法地乱试。
第1分钟:快速锁定目标进程
首先,我们需要知道是哪个(或哪些)进程在疯狂吞噬 CPU。
# 使用 top 的非交互模式快速抓取一次状态,比交互式更省资源
top -c -b -n 1 | head -20
如果系统负载太高,top 命令本身执行都困难,可以换用更轻量的 ps 命令组合:
# 打印表头,然后按CPU使用率降序排列
ps aux | head -1; ps aux | sort -rn -k3 | head -10
小技巧:top -b -n 1 参数表示批量模式并只采样一次,类似于拍照,比持续录制的交互模式(top)消耗资源少得多。
曾经有一次,我快速发现是一个 Java 进程占用了 99.8% 的 CPU。但这只是找到了“嫌疑人”,真正的“作案凶手”还藏在进程内部。
第2分钟:深入进程,揪出问题线程
单个进程 CPU 高,通常是其内部某个线程在疯狂运行。我们需要进入进程内部看看。
# 查看指定进程内部的所有线程状态,按 CPU 排序
top -H -p <PID>
或者使用 ps 命令获取更精确的线程信息:
# 查看指定进程的线程信息,并排序
ps -mp <PID> -o THREAD,tid,time | sort -rn -k2 | head -20
找到消耗 CPU 最高的线程 ID(TID)后,关键一步是将其转换为十六进制,因为后续 Java 线程栈需要十六进制的 ID。
printf "%x\n" <线程ID>
第3分钟:定位代码行,真相大白
现在,我们有了十六进制的线程 ID,可以抓取 Java 线程栈来查看这个线程当时正在执行什么代码。
# 导出进程的完整线程栈,并过滤出问题线程的堆栈信息
jstack <PID> | grep -A 30 <16进制线程ID>
如果不是 Java 进程,例如是 C/C++ 或 Go 程序,可以使用 strace 追踪其系统调用,观察它卡在哪个系统调用上。
# 统计进程的系统调用开销
strace -p <PID> -c -f
通过这套组合拳,我曾在 3 分钟内定位到一个问题:一个定时任务在处理一个格式错误的 JSON 数据时,陷入了无限递归循环。这就是典型的业务逻辑 Bug 引发的 CPU 风暴。
排查路上,那些容易踩的“坑”
即便有了流程,一些认知误区也会导致绕远路。
坑1:只盯着 CPU 使用率,忽略了 Load Average
CPU 使用率 100% 不一定真的是计算瓶颈。我曾遇到系统 Load Average 高达 80,但 CPU 使用率却只有 30%。一查 iostat,发现是磁盘 I/O 等待极高,大量进程被阻塞在 IO 上,导致系统负载高。
uptime # 查看 Load Average
iostat -x 1 # 查看 I/O 等待(%util, await)
vmstat 1 # 查看系统整体状态(r, b, si, so)
坑2:条件反射式地 kill -9
新手运维容易紧张,一看 CPU 高就想 kill -9,这是大忌!我见过有人直接 kill 了数据库进程,导致数据文件损坏,恢复用了整整一夜。正确的止损步骤应该是:
# 1. 首先,尽可能保存现场(非常重要!)
jstack <PID> > /tmp/jstack_$(date +%F-%T).txt
# 2. 尝试优雅停止 (SIGTERM)
kill -15 <PID>
# 3. 等待一段时间(如30秒),若进程仍不退出,再考虑强制终止
# kill -9 <PID>
坑3:忽视了僵尸进程(Zombie)的预警
僵尸进程本身不消耗 CPU 和内存,但大量僵尸进程的出现,意味着它们的父进程没有正确调用 wait() 来回收子进程信息。这常常是父进程异常或设计缺陷的信号,可能成为后续 CPU 或其他问题的前兆。
# 检查系统是否存在僵尸进程
ps aux | grep defunct
# 找到创建僵尸进程的父进程
ps -ef | grep <僵尸进程PID> | grep -v grep
进阶:构建防御体系,防患于未然
亡羊补牢不如未雨绸缪。除了被动排查,主动预防更为重要。
1. 资源限制与隔离
为重要进程设置 CPU 使用上限,防止单个应用拖垮整个系统。
# 使用 cgroups 限制进程组CPU配额 (示例:限制为50%个CPU核心)
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
# 使用 nice 调整进程调度优先级(数字越大,优先级越低,越“友好”)
nice -n 10 ./my_process.sh
2. 设置智能化的分级告警
不要等到 CPU 100% 才告警,那时可能已经晚了。根据业务特点设置梯度阈值:
- CPU > 70% 持续5分钟:发出警告,提醒关注。
- CPU > 85% 持续3分钟:严重告警,需要立即检查。
- CPU > 95% 持续1分钟:紧急告警,启动应急预案。
3. 自动化诊断脚本
编写一个自动触发的诊断脚本,当 CPU 超过阈值时自动收集现场信息,方便事后复盘。
#!/bin/bash
# cpu_diagnose.sh
CPU_THRESHOLD=80
CPU_USAGE=$(top -b -n1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
if (( $(echo "$CPU_USAGE > $CPU_THRESHOLD" | bc -l) )); then
DATE=$(date +%F-%T)
DIAG_DIR="/var/log/cpu_diagnose/$DATE"
mkdir -p $DIAG_DIR
top -b -n 3 > $DIAG_DIR/top.txt
ps aux > $DIAG_DIR/ps.txt
iostat -x 3 3 > $DIAG_DIR/iostat.txt
# 如果是Java应用,自动抓取所有Java进程的线程栈
for pid in $(ps aux | grep java | grep -v grep | awk '{print $2}'); do
jstack $pid > $DIAG_DIR/jstack_${pid}.txt 2>&1
done
echo "诊断信息已保存至 $DIAG_DIR"
fi
展望:eBPF——新一代性能观测利器
传统工具(如 top, strace)在系统极端高负载时,本身可能会因为资源竞争而失效或产生较大开销。eBPF(Extended Berkeley Packet Filter)技术为系统观测提供了更底层、更高效的能力。
# 使用BCC工具集里的cpudist,统计CPU时间分布
/usr/share/bcc/tools/cpudist 10 1
# 使用profile生成性能剖析数据,并生成火焰图
/usr/share/bcc/tools/profile -F 99 -p <PID> 30 > out.stacks
flamegraph.pl < out.stacks > flamegraph.svg
eBPF 的优势在于:
- 近乎零开销:在内核中运行,无需将数据拷贝到用户空间,观测成本极低。
- 动态附加:无需重启应用或系统,即可动态注入观测点。
- 内核态可见性:能够追踪系统调用、内核函数、网络包等更底层的事件,对于分析网络/系统层面的复杂问题尤其有力。
处理线上 CPU 爆满问题,既是对技术能力的考验,也是对心理素质的磨练。从最初的慌乱到如今的有条不紊,每一次紧急排查都是宝贵的经验积累。掌握系统化的运维 & 测试方法、善用趁手的工具,并建立起预防体系,才能在“警报”响起时,真正做到心中有数,手中有术。