Linux 服务器 CPU 使用率突增的定位思路
CPU 突增排障最容易犯的错误,是看到一个高百分比就重启服务。重启可能暂时恢复,却会丢失现场,并把代码热点、锁竞争、中断风暴、内存回收或容器限流混成同一种“CPU 高”。本文以 RHEL 8/9、systemd、cgroup v2 为基线,目标是建立可复核的证据链:确认时间窗与口径,定位主机、进程、线程和函数,再关联日志、发布、流量与内核事件,最后才选择限流、降级、扩容或修复。
文中的 <进程PID>、<线程TID>、<服务名>、<网卡名>、<采集目录>、<命名空间> 和 <Pod名> 均需替换为现场值。只读命令也可能产生一定开销,perf、抓包和高频采样尤其应限定时间。
确认告警口径
监控中的 CPU 可能是整机利用率、单核百分比、进程累计时间、容器配额比例或负载平均值。8 核主机上单线程跑满通常约占整机 12.5%,但在 top 的进程视图中可能显示 100%。先记录 UTC 与本地时间、告警起止、主机核数和监控公式。
date --iso-8601=seconds
timedatectl status
nproc
lscpu
uptime
load average 是可运行和不可中断任务的队列长度,不是 CPU 百分比。高 load 可能来自磁盘 I/O 阻塞,必须结合运行队列和 CPU 状态判断。
查看整体 CPU 构成:
关注 %usr、%sys、%iowait、%irq、%soft、%steal 和 %idle。%iowait 高说明 CPU 空闲时存在未完成 I/O,不等于 CPU 算力耗尽;虚拟机 %steal 高通常需要关联宿主机竞争证据。
用 vmstat 观察运行队列、换页和上下文切换:
r 持续显著高于逻辑 CPU 数且 idle 很低,支持 CPU 排队;b 高、wa 高更偏向 I/O;si/so 非零提示换页压力。单个样本不足以下结论。
找到谁在消耗 CPU
按进程采样用户态、内核态、I/O 与上下文切换:
%usr 高常见于计算、序列化、正则或垃圾回收;%system 高需继续看系统调用、网络和内核路径;大量 voluntary/involuntary context switches 可能来自锁、线程过多或调度竞争。
生成稳定的进程排序快照:
ps -eo pid,ppid,user,stat,psr,pcpu,pmem,etimes,comm,args \
--sort=-pcpu | head -n 30
ps 的 %CPU 是进程生命周期平均值,可能淡化短时峰值;应与 pidstat 的区间采样互补。命令行可能包含敏感参数,保存前需脱敏。
识别服务归属和 cgroup:
systemctl status <服务名> --no-pager
systemctl show <服务名> \
-p MainPID -p ControlGroup -p CPUUsageNSec -p CPUQuotaPerSecUSec
cat /proc/<进程PID>/cgroup
如果 MainPID 与高 CPU PID 不一致,可能是 worker 子进程、容器或服务外进程。不要凭进程名直接杀进程。
下钻到线程
多线程进程必须确认具体线程:
pidstat -t -u -p <进程PID> 1 10
持续高 CPU 的 TID 才值得进一步采样。瞬时线程可能已退出,应同步记录时间。
查看线程名和调度状态:
ps -L -p <进程PID> \
-o pid,tid,psr,stat,pcpu,comm,wchan:32 --sort=-pcpu | head -n 30
cat /proc/<进程PID>/task/<线程TID>/status
wchan 只对睡眠线程有意义;运行中的热点线程通常显示 -。线程名由应用设置时,可直接关联线程池、GC 或业务 worker。
Java 线程转储使用十六进制 nid。先转换 TID:
printf'0x%x\n' <线程TID>
jcmd <进程PID> Thread.print -l > <采集目录>/threads-$(date +%s).txt
jcmd 应使用与目标 JVM 兼容的 JDK 工具,并以同一用户执行。线程转储可能短暂进入 safepoint 且包含业务栈,应控制次数和文件权限。
连续获取三次线程栈,确认热点是否稳定而非偶然:
#!/usr/bin/env bash
set -euo pipefail
PID="<进程PID>"
OUT_DIR="<采集目录>"
install -d -m 0700 "${OUT_DIR}"
for i in 1 2 3; do
jcmd "${PID}" Thread.print -l >"${OUT_DIR}/thread-${i}.txt"
if (( i < 3 )); thensleep 5; fi
done
相同线程在多份栈中持续处于 RUNNABLE 且落在同一业务函数,才构成代码热点证据。仅一次栈不足以排除采样偶然性。
区分用户态、内核态与中断
系统态高时查看系统调用摘要。strace 会增加开销,生产中限定 20 秒,并先在低风险实例评估:
sudotimeout 20 strace -f -c -p <进程PID>
大量 futex 可能是锁等待或线程同步,不自动等于锁竞争根因;大量 read/write/sendto/recvfrom 需结合文件描述符、网络和调用栈。
检查中断分布:
watch -n 1 'cat /proc/interrupts | head -n 30'
cat /proc/softirqs
若某网卡队列中断在单核快速增长,同时 %soft 高,应检查 RSS/RPS、驱动、流量与丢包。watch 用 Ctrl-C 结束。
查看网卡统计和软中断压力:
ip -s link show dev <网卡名>
ethtool -S <网卡名> | grep -Ei 'drop|miss|error|timeout|queue'
sar -n DEV,EDEV 1 10
驱动统计字段不统一,必须以实际网卡驱动输出为准。错误计数应观察增量,而不是只看累计绝对值。
检查 irqbalance 与中断亲和性:
systemctl status irqbalance --no-pager
grep -H . /proc/irq/<IRQ编号>/smp_affinity_list
ethtool -l <网卡名>
<IRQ编号> 来自 /proc/interrupts。手工修改亲和性可能与 irqbalance 冲突,属于性能策略变更,应先备份、灰度并准备恢复。
检查内存、I/O 与回收造成的假象
内存紧张会让 CPU 花在 reclaim、压缩或换页上:
free -h
sar -B 1 10
sar -W 1 10
cat /proc/pressure/memory
pgscan、pgsteal、swap in/out 和 memory PSI 同时升高,支持内存回收压力。仅看到缓存占用高不是内存不足,Linux 会主动使用页缓存。
检查 I/O 延迟和队列:
iostat -xz 1 10
cat /proc/pressure/io
高 %util 在并行设备上不必然表示饱和,应结合 await、队列、吞吐和设备类型;CPU 告警同时存在高 iowait 时,优化 CPU 代码可能无效。
查看内核日志中的 OOM、hung task 和硬件错误:
sudo journalctl -k --since '-30 minutes' --no-pager \
| grep -Ei 'oom|out of memory|hung task|soft lockup|hard lockup|mce|thermal|throttl|rcu'
日志时间必须与告警窗口对齐。温度降频可能导致同样工作量占用更长 CPU 时间,可继续查看硬件传感器和平台监控。
使用 perf 找到函数热点
perf 依赖对应内核工具包、符号和权限。先检查版本与内核限制:
perf --version
uname -r
sysctl kernel.perf_event_paranoid kernel.kptr_restrict
不要为了采样长期关闭安全限制。若策略不允许 perf,应使用语言运行时自带 profiler 或在预生产复现。
进行 30 秒、99Hz 的调用栈采样:
sudo perf record -F 99 -g -p <进程PID> -- sleep 30
sudo perf report --stdio --sortcomm,dso,symbol | head -n 80
99Hz 可降低与常见定时频率同步偏差。perf.data 可能较大并包含符号信息,应在受控目录采集。热点函数占比必须与进程/线程 CPU 同期证据结合。
仅看系统调用和调度事件的计数:
sudo perf stat -p <进程PID> \
-e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions \
-- sleep 20
IPC(instructions/cycles)低可能来自缓存未命中、分支或停顿,但不能单独判定根因。虚拟化环境计数器也可能受限。
若怀疑调度延迟,短时记录调度事件:
sudo perf sched record -- sleep 10
sudo perf sched latency --sort max | head -n 40
这是全机采样,繁忙主机开销和数据量更高,应先灰度。不要在长期高峰持续运行。
语言运行时专项证据
Java 应查看 GC 与编译活动:
jstat -gcutil <进程PID> 1000 10
jcmd <进程PID> VM.flags
jcmd <进程PID> Compiler.queue
jcmd <进程PID> GC.heap_info
jstat 字段随 GC 算法不同。频繁 Full GC、GC 线程高 CPU 和堆占用变化要共同出现,才支持 GC 根因;不要只因日志中出现 GC 就调大堆。
短时 Java Flight Recorder 适合生产诊断,但会有开销,先确认 JDK 版本和许可策略:
jcmd <进程PID> JFR.start \
name=cpu-investigation settings=profile duration=60s \
filename=<采集目录>/cpu-investigation.jfr
jcmd <进程PID> JFR.check
settings=profile 比默认配置开销更高。JFR 文件可能包含类名、路径和业务事件,应安全传输和保存。
Python 进程可用 py-spy 等外部工具,但安装和 ptrace 权限需遵守组织政策。若已安装,可限定采样:
sudotimeout 30 py-spy top --pid <进程PID> --rate 50
不要在线安装来源不明的二进制。若工具不可用,优先使用应用内置 profiling 或在隔离环境复现。
容器与 cgroup 限流
容器内看到 CPU 高,必须确认配额。cgroup v2 的 cpu.stat 可显示使用与 throttling:
CGROUP_PATH="$(systemctl show -p ControlGroup --value <服务名>)"
sudocat"/sys/fs/cgroup${CGROUP_PATH}/cpu.max"
sudocat"/sys/fs/cgroup${CGROUP_PATH}/cpu.stat"
sudocat"/sys/fs/cgroup${CGROUP_PATH}/cpu.pressure"
cpu.max 的 max 表示无配额,否则是“配额 微秒周期”。nr_throttled 和 throttled_usec 应观察增量;限流高不代表主机 CPU 一定高。
Kubernetes 中查看资源配置与当前使用:
kubectl -n <命名空间> get pod <Pod名> -o jsonpath='{.spec.containers[*].resources}{"\n"}'
kubectl -n <命名空间> top pod <Pod名> --containers
kubectl -n <命名空间> describe pod <Pod名>
kubectl top 依赖 Metrics Server,是近期聚合值,不适合复原已经结束的秒级尖峰。需要 Prometheus 历史指标补齐时间线。
容器 CPU 使用率查询。指标以实际 exporter/cAdvisor 暴露为准:
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace="<命名空间>",pod="<Pod名>",container!=""
}[5m])
)
结果单位是 CPU 核。若要算相对 limit,需与 kube_pod_container_resource_limits{resource="cpu"} 关联,并处理无 limit 情况。
检查限流比例,指标以实际 exporter 暴露为准:
sum(rate(container_cpu_cfs_throttled_periods_total{namespace="<命名空间>",pod="<Pod名>"}[5m]))
/
sum(rate(container_cpu_cfs_periods_total{namespace="<命名空间>",pod="<Pod名>"}[5m]))
该指标在部分 cgroup v2/cAdvisor 版本中可能变化,应先检查实际指标。提高 limit 前确认节点可分配 CPU 和同节点邻居影响。
关联发布、流量和定时任务
CPU 上升经常与版本发布、配置切换、缓存失效、批处理或流量结构变化同步。查看 systemd 日志和服务启动时间:
systemctl show <服务名> -p ActiveEnterTimestamp -p ExecMainStartTimestamp -p MainPID
sudo journalctl -u <服务名> --since '-1 hour' --no-pager
sudo journalctl --since '-1 hour' _PID=<进程PID> --no-pager | tail -n 200
日志出现错误必须结合频率和时间;某错误既可能是 CPU 高的原因,也可能是超载后的结果。
检查定时任务:
systemctl list-timers --all
sudo grep -RIn --exclude='*.bak' . /etc/cron.d /etc/cron.hourly 2>/dev/null
sudo crontab -l
备份、压缩、日志轮转、病毒扫描和统计任务可能引起周期性 CPU。不要直接禁用,先确认业务目的和补跑方案。
检查最近安装和配置变更:
sudo dnf history list | head -n 20
sudo find /etc -xdev -type f -mmin -120 -printf'%TY-%Tm-%Td %TH:%TM %p\n' \
2>/dev/null | sort
文件时间只能证明修改发生,不能证明它导致 CPU。根因必须由差异内容、时间相关性和可复现行为支持。
自动采集现场
下面脚本在不修改系统状态的前提下采集 30 秒基础证据;命令行与日志可能含敏感信息,输出目录权限设为 700:
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="<采集目录>/cpu-$(date -u +%Y%m%dT%H%M%SZ)"
DURATION=30
install -d -m 0700 "${OUT_DIR}"
date --iso-8601=seconds >"${OUT_DIR}/time.txt"
uptime >"${OUT_DIR}/uptime.txt"
lscpu >"${OUT_DIR}/lscpu.txt"
ps -eo pid,ppid,user,stat,psr,pcpu,pmem,etimes,comm,args --sort=-pcpu \
>"${OUT_DIR}/processes.txt"
mpstat -P ALL 1 "${DURATION}" >"${OUT_DIR}/mpstat.txt" &
p1=$!
pidstat -u -r -d -w 1 "${DURATION}" >"${OUT_DIR}/pidstat.txt" &
p2=$!
vmstat 1 "${DURATION}" >"${OUT_DIR}/vmstat.txt" &
p3=$!
wait"${p1}""${p2}""${p3}"
journalctl -k --since '-15 minutes' --no-pager >"${OUT_DIR}/kernel.txt"
printf'saved=%s\n'"${OUT_DIR}"
并行采样会产生轻微负载。若主机已极端繁忙,可缩短持续时间或先采集单项。脚本不包含 perf、strace 和抓包,因为这些需要单独评估风险。
计算采集文件校验和,保证传递后可验证:
find <采集目录> -maxdepth 1 -type f -print0 \
| sort -z \
| xargs -0 sha256sum > <采集目录>/SHA256SUMS
采集目录本身需替换为本次生成的精确目录,避免扫描无关数据。
处置选择与风险
如果已确认单个实例代码热点,可先从负载均衡器摘除该实例,保留现场并在其他实例承载能力允许时分析。若全实例随流量升高,优先限流、缓存、扩容;若数据库或下游变慢导致重试风暴,应先限制重试并恢复依赖。提高 CPU 配额只能缓解限流,不能修复死循环。
终止进程属于高风险操作:会中断请求、丢失内存态数据,并可能由 systemd 立即拉起。先确认副本、健康检查、优雅停止时间和持久化状态。优先通过服务管理器:
sudo systemctl show <服务名> \
-p MainPID -p Restart -p TimeoutStopUSec -p KillMode
sudo systemctl stop <服务名>
sudo systemctl status <服务名> --no-pager
执行前必须完成流量摘除并确认容量,执行后验证请求、日志、端口与依赖。回滚是 systemctl start <服务名> 并重新加入流量;若启动失败,查 journal,不要循环重启。
临时限制 systemd 服务 CPU 可保护整机,但会增加该服务延迟,必须灰度:
sudo systemctl set-property --runtime <服务名> CPUQuota=200%
systemctl show <服务名> -p CPUQuotaPerSecUSec
cat /proc/pressure/cpu
200% 约等于两个 CPU 核的配额。--runtime 重启系统后失效,适合应急验证。回滚使用 systemctl set-property --runtime <服务名> CPUQuota=infinity,并再次检查服务延迟和整机 CPU。
若决定回滚应用版本,先保存当前版本、配置差异、线程栈和 profiler 结果。Kubernetes 回滚示例:
kubectl -n <命名空间> rollout history deployment/<部署名>
kubectl -n <命名空间> rollout undo deployment/<部署名> --to-revision=<修订号>
kubectl -n <命名空间> rollout status deployment/<部署名> --timeout=10m
kubectl -n <命名空间> get pods -l app=<应用标签> -o wide
回滚会重建 Pod,必须检查 PDB、容量、数据库迁移兼容性和不可逆数据变更。若版本包含不向后兼容的 schema 迁移,不得只回滚二进制。
验证根因而不是验证“CPU 降了”
合格结论应能回答:哪个主机或容器、哪个进程和线程、用户态还是内核态、哪个函数或等待路径、从何时开始、与什么变化同步、采取措施后哪些指标按预期变化。例如“某 Java worker 高 CPU”仍不是根因;“三次线程栈与 30 秒 perf 均指向同一解析函数,发布后该路径调用量上升,回滚后函数采样占比与 CPU 同时恢复”才是完整证据链。
修复后至少重复采集 mpstat、pidstat、线程/函数证据和业务延迟,覆盖一个原本会触发峰值的窗口。确认没有把压力转移到 I/O、数据库、下游或其他节点。保留命令、时间窗、版本、配置差异和回滚结果,后续才能把一次排障转化为告警阈值、容量规划和发布验证规则。
进程消失或尖峰已经结束怎么办
短时尖峰结束后,top 和 /proc/<进程PID> 无法还原现场。此时应依赖历史监控、journald、进程记账和预先部署的低开销持续采样。不要根据当前正常状态否定告警,也不要把监控的一个点当成完整持续时间。
若已启用 sysstat,可查询历史 sar 文件:
sar -u -f /var/log/sa/sa<日期> -s <开始时间> -e <结束时间>
sar -q -f /var/log/sa/sa<日期> -s <开始时间> -e <结束时间>
sar -w -f /var/log/sa/sa<日期> -s <开始时间> -e <结束时间>
<日期> 通常是两位日号,路径与格式依 sysstat 配置和版本而异;用 ls /var/log/sa 和 sar --help 确认。历史采样粒度若为 10 分钟,无法证明 20 秒尖峰内部细节。
检查进程是否被 OOM、systemd 或人工操作结束:
sudo journalctl --since '<开始时间>' --until'<结束时间>' --no-pager \
| grep -Ei 'killed process|oom-kill|Main process exited|Stopped|Started|sudo|session opened'
last -x | head -n 30
last -x 来自 wtmp,可能受轮转影响。人工操作归因应结合 sudo/audit 日志和变更记录,不能只凭登录时间猜测。
如果组织允许进程记账,acct/psacct 能记录已结束进程的命令和累计 CPU,但启用本身会增加磁盘写入,并且只能在启用后提供数据:
systemctl status psacct --no-pager
lastcomm | head -n 50
sa --sort-cpu | head -n 30
工具是否安装、服务名和日志保留应按发行版确认。它不能还原线程栈或函数热点,只能帮助发现短命高 CPU 命令。
调度器、NUMA 与 CPU 频率
在多路 NUMA 服务器上,进程跨节点访问内存会增加延迟和 CPU 周期。先查看拓扑和分配,不要直接绑核:
numactl --hardware
numastat -p <进程PID>
taskset -cp <进程PID>
远端内存比例高只是线索,还需结合应用内存分配和 CPU 亲和性。手工 taskset 可能破坏服务管理器、容器编排或 irq 分布,应通过正式配置灰度并记录原值。
检查 CPU 频率与 governor:
cpupower frequency-info
grep -H . /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null | head
某些虚拟机不暴露 cpufreq。频率下降可能由节能策略、温度或平台限制引起;修改 governor 会改变功耗和热设计,必须由平台团队评估。
调度延迟还可能来自实时优先级任务。列出策略和优先级:
ps -eLo pid,tid,cls,rtprio,pri,ni,psr,stat,pcpu,comm \
--sort=-rtprio,-pcpu | head -n 40
FF/RR 实时线程配置不当可能饿死普通任务。降低实时优先级属于行为变更,先确认进程用途、看门狗和厂商建议。
虚拟机中的 steal time
虚拟机 %steal 持续升高意味着 vCPU 想运行但被 hypervisor 调度走。客户机内部增加进程副本可能使竞争更严重。证据应包含 mpstat/sar 的 steal 时间、同宿主机其他虚机趋势、云平台宿主指标和实例规格限制。
sar -u ALL 1 10
grep -E '^cpu ' /proc/stat
systemd-detect-virt
若是可突发实例,还要检查 CPU credit/积分余额;这是云平台动态指标,需从对应监控系统获取。没有平台证据时,只能把 steal 列为疑点,不能断言宿主机超卖。
网络包率而非带宽
小包高 PPS 可能让带宽不高但软中断 CPU 很高。应同时比较包率、字节率、丢包和 softnet 统计:
sar -n DEV 1 10
awk '{for (i=1;i<=NF;i++) printf "%s%s", strtonum("0x" $i), (i==NF?ORS:OFS)}' \
/proc/net/softnet_stat | head
/proc/net/softnet_stat 字段含义依内核文档,第二列常用于观察丢包累计值;必须看增量并核对当前内核。上面的 awk 依赖 gawk 的 strtonum,若不可用可直接保留十六进制原值。
限制 20 秒抓取包头用于协议分布分析:
sudotimeout 20 tcpdump -ni <网卡名> -s 128 -c 20000 \
-w <采集目录>/cpu-spike.pcap
抓包涉及敏感数据且会消耗 CPU/磁盘。-s 128 限制每包长度,-c 限制数量;执行前确认合规、空间和主机余量。分析完成后按数据保留策略删除或归档。
安全事件与异常进程
未知进程高 CPU 可能是失控脚本,也可能涉及安全事件。不要立即删除二进制或清空日志。先隔离风险、保留哈希、路径、父进程、网络连接和打开文件,再交由安全流程判断。
sudoreadlink -f /proc/<进程PID>/exe
sudosha256sum /proc/<进程PID>/exe
sudotr'\0'' ' < /proc/<进程PID>/cmdline; echo
sudo lsof -nP -p <进程PID> | head -n 100
sudo ss -tpn | grep "pid=<进程PID>,"
命令行和连接信息可能敏感。进程映像已被删除时 /proc/.../exe 会标记 (deleted),仍不应未经取证直接终止。若存在横向移动或挖矿疑点,应按事件响应要求隔离网络并保存证据。
为下一次尖峰预置可观测性
秒级尖峰需要秒级或更细粒度指标。Prometheus 主机 CPU 查询以 node_exporter 实际指标为准:
100 * (1 - avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[2m])
))
短窗口反应快但噪声大;告警可同时要求利用率高、运行队列高或业务延迟恶化。多核进程热点还需进程 exporter、eBPF profiler 或运行时指标补足。
记录 CPU 压力,指标名称以实际 exporter 暴露为准:
rate(node_pressure_cpu_waiting_seconds_total[5m])
PSI 能反映任务因 CPU 竞争等待的时间,比单纯利用率更接近“是否真的缺 CPU”。不同 node_exporter 版本可能不暴露该指标,需检查 /metrics。
生产环境可通过 systemd timer 定期运行低开销采集脚本,但不得每秒执行重型命令。服务和定时器示意:
# /etc/systemd/system/cpu-snapshot.service
[Unit]
Description=Collect a lightweight CPU snapshot
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cpu-snapshot
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/cpu-snapshot.timer
[Unit]
Description=Run CPU snapshot every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
AccuracySec=5s
Persistent=false
[Install]
WantedBy=timers.target
部署前对脚本做权限、磁盘配额、轮转和敏感数据审查;先在单台灰度。停用回滚使用 systemctl disable --now cpu-snapshot.timer,不会自动删除已采集文件,应按保留策略处理。
排查顺序的实际取舍
面对仍在持续的高 CPU,优先级通常是:先保存时间与整体状态;用 pidstat 找进程和线程;并行确认业务影响;根据 usr/sys/soft/steal 选择 perf、strace、中断或平台路径;再关联发布和流量。面对已经结束的尖峰,则优先历史指标、journal、发布记录和记账数据。面对整机失去响应,先通过限流、摘流或故障转移保护业务,再在仍有余量的副本保留现场。
任何根因报告都应区分事实、推断和待验证项。事实是命令、日志和指标在明确时间窗内的结果;推断是它们支持的机制;验证是修复或复现实验产生的预期变化。这样才能避免“CPU 高因为流量大”这类无法指导修复的结论。