Linux 服务 CPU 飙高:区分单核热点、cgroup 限流与宿主机争用

110 次浏览5 条回复

适用于 Linux 服务延迟升高、CPU 告警或吞吐下降,且需要判断是应用计算、内核开销、虚拟机争用还是 CPU 配额所致的场景。命令基线为 Linux 5.4+、procps-ng 3.3+、systemd 245+;mpstatpidstat 来自 sysstat 12.2+,perf 为可选工具且应与运行内核兼容。读取其他用户进程、cgroup 和性能事件通常需要 root 或对应权限。先保留告警窗口,不要一开始就重启服务、扩大 CPU 配额或在线执行高开销跟踪。

1. 固定主机、进程与时间边界

date -u
uname -r
systemctl --version | head -n 1
uptime
nproc --all
systemctl status app.service --no-pager -l
systemctl show app.service \
  -p MainPID -p ControlGroup -p CPUAccounting -p CPUQuotaPerSecUSec

示例单元 app.service 必须替换为实际名称。MainPID 只代表主进程,prefork、工作进程或边车可能消耗更多 CPU,因此还要按上一步得到的 cgroup 核对完整进程集合。容器环境应从实际运行时取得宿主机 PID、CPU request/limit 和所在 cgroup;容器内看到的 CPU 数量、进程列表与宿主机视图可能不同。

先把 CPU 告警、请求延迟、发布、流量变化和服务重启放到同一 UTC 窗口。累计计数只能做前后差分,不能用事故结束后的单次快照反推峰值。

2. 区分整机饱和、单核热点与虚拟机争用

vmstat 1 10
mpstat -P ALL 1 10
ps -eLo pid,tid,psr,stat,pcpu,comm --sort=-pcpu | head -n 30

mpstatpidstat 未安装时,可先用 vmstatps 初筛,但 ps%CPU 不是严格的短窗口采样。重点对照:

  • vmstatr 持续高于可用 CPU 数,且各 CPU 的 idle 都很低,更像整机运行队列饱和。
  • 只有一个 CPU 接近满载、某个线程长期居首,而其他 CPU 空闲,更像单线程热点、锁竞争后的忙等或 CPU 亲和性限制。
  • %system、软中断或上下文切换明显升高时,应继续查系统调用、网络包处理、缺页和调度开销,不能只优化业务计算。
  • 虚拟机中的 %steal 持续升高表示 vCPU 等待宿主机调度,应与虚拟化平台同窗口指标核对;来宾机内增加线程通常不会解决宿主机争用。

/proc/pressure/cpu 可补充任务等待 CPU 的压力趋势:

cat /proc/pressure/cpu

PSI 接口依赖内核配置;avg10avg60avg300 是不同时间窗口,total 是启动以来的累计等待时间。应记录多次采样的变化,不要把一次累计值当作当前利用率。

3. 单独检查 cgroup v2 配额与节流

先确认 cgroup 版本;以下文件名只适用于 cgroup v2:

stat -fc %T /sys/fs/cgroup
CGROUP="$(systemctl show app.service -p ControlGroup --value)"
cat "/sys/fs/cgroup${CGROUP}/cpu.max"
cat "/sys/fs/cgroup${CGROUP}/cpu.stat"
cat "/sys/fs/cgroup${CGROUP}/cpu.pressure"

只有第一条显示 cgroup2fs 时才按此路径读取;单元名和 ControlGroup 必须来自目标服务。cpu.max 的第一列为 max 时表示没有该层 CPU 带宽上限;数值形式是 quota 与 period。cpu.statnr_throttledthrottled_usec 持续增加,才说明观察窗口内发生了节流。还要向父 cgroup 逐层核对:子服务自身没有限额,不代表上层 slice、Pod 或容器没有限额。

节流与宿主机饱和可以同时发生。修改 CPUQuota、容器 limit 或编排参数属于容量变更,应先确认业务负载、父层约束和调度策略,再在受控窗口处理。

4. 把热点定位到线程和代码路径

PID=<actual-pid>
pidstat -u -w -t -p "$PID" 1 10
top -H -p "$PID"

PID 必须来自目标实例,进程重启后要重新确认。pidstat -t 用于区分线程,-u 对照用户态与内核态 CPU,-w 观察自愿和非自愿上下文切换。命令行、线程名和符号可能含敏感信息,进入工单或社区前应脱敏。

需要调用栈时,优先使用应用运行时自带且已验证开销的 profiler。具备授权、性能事件权限并评估开销后,才考虑短时采样:

perf --version
sudo perf record -F 99 -g -p "$PID" -- sleep 30
sudo perf report --stdio

perf 可能受 kernel.perf_event_paranoid、符号文件、容器权限和编译选项限制;空符号或不完整调用栈不等于没有热点。不要在未知负载下长时间提高采样频率,也不要用 strace 全量跟踪来替代 CPU profiling。

5. 验证闭环

处置后用相同采样周期重复 vmstatmpstatpidstat 和 cgroup 计数,确认比较的是同一实例、同一 CPU 配额与相近业务负载。至少同时验证:热点线程 CPU 降低或吞吐按预期提高;运行队列、PSI 与请求延迟回落;nr_throttled 不再异常增长;虚拟机 %steal 或宿主机争用已由平台侧证据解释;错误率和尾延迟没有因降载或限流转移到其他组件。

只看到总 CPU 下降不算闭环。还应记录根因属于代码路径、并发模型、cgroup 配额、宿主机调度还是内核开销,并把容量阈值、回归压测和告警口径与该根因对应起来。

还可以在“单核热点”分支补一项 CPU 亲和性与 cpuset 检查。nproc --all 给出的是已安装 CPU 数,不能证明目标进程实际允许在哪些 CPU 上运行;线程长期集中在一个核,也可能是 systemd CPUAffinity=、容器 cpuset 或运行时绑核导致。

先以目标进程和实际 cgroup 为准读取:

PID=<actual-pid>
CGROUP="$(systemctl show app.service -p ControlGroup --value)"
grep -E '^(Cpus_allowed|Cpus_allowed_list):' "/proc/$PID/status"
taskset -pc "$PID"
cat "/sys/fs/cgroup${CGROUP}/cpuset.cpus"
cat "/sys/fs/cgroup${CGROUP}/cpuset.cpus.effective"

这些 cgroup 路径同样以 v2 为前提;读取前应按正文方法确认 cgroup2fscpuset.cpus 为空不代表未受约束,它可能继承父层设置,因此应以 cpuset.cpus.effective 为当前有效集合,并在必要时逐层检查父 cgroup。多线程进程还应抽查各 TID 的 /proc/$PID/task/$TID/status,因为线程级亲和性可以不同。

验证时把允许 CPU 集合与 mpstat -P ALL 1 10pidstat -t -p "$PID" 1 10 同窗口对照:若负载只压满允许集合而其他 CPU 空闲,应先确认绑核是否符合设计;只有允许集合足够宽、热点仍稳定落在单个线程时,再把结论收敛到单线程代码路径或忙等。taskset 来自 util-linux,查看或修改其他用户进程的亲和性需要相应权限;排查阶段只读,不要直接用 taskset -p 改写掩码。

正文的 cgroup v2 分支已经很清楚;考虑到版本基线包含 Linux 5.4 与 systemd 245,建议再补一个 cgroup v1 / hybrid 分支。此类主机上 stat -fc %T /sys/fs/cgroup 不是 cgroup2fs 并不代表没有 CPU 配额,控制器可能单独挂载,且实际路径应以目标热点进程的 /proc 记录为准。

先定位 CPU 控制器挂载点和该进程在控制器中的路径:

PID=<actual-hot-pid>
awk -F: '$2 ~ /(^|,)cpu(,|$)/ {print "controllers=" $2, "path=" $3}' "/proc/$PID/cgroup"
findmnt -rn -t cgroup -o TARGET,OPTIONS \
  | awk '$2 ~ /(^|,)cpu(,|$)/'

确认输出后再代入实际值读取,不能固定假设挂载目录一定是 /sys/fs/cgroup/cpu

CPU_MOUNT=<actual-cpu-controller-mount>
CPU_PATH=<actual-path-from-proc>
cat "${CPU_MOUNT}${CPU_PATH}/cpu.cfs_quota_us"
cat "${CPU_MOUNT}${CPU_PATH}/cpu.cfs_period_us"
cat "${CPU_MOUNT}${CPU_PATH}/cpu.stat"

v1 中 cpu.cfs_quota_us=-1 表示当前层不设 CFS 带宽上限;cpu.statnr_periodsnr_throttledthrottled_time 仍应做同一观察窗口的前后差分,其中 throttled_time 单位是纳秒,不要与 v2 的 throttled_usec 混用。也要沿 CPU_PATH 向父层核对,因为服务自身为 -1 时仍可能受上层 slice 或容器 cgroup 限制。若 /proc/$PID/cgroup 没有带 cpu 的 v1 条目,则先确认控制器是否挂载并启用,而不是把缺少文件解释成“没有发生节流”。

正文已提示 %system 与软中断,但排障时值得把它单列成一个分支,否则单核上的 ksoftirqd/N 很容易被误判成业务线程热点。sysstat 12.2+ 可先做同窗口采样:

mpstat -P ALL -I SCPU 1 10
sar -I XALL 1 10
ps -eLo pid,tid,psr,stat,pcpu,comm --sort=-pcpu | head -n 40

/proc/interrupts/proc/softirqs 都是启动以来累计值;需要保留采样前后两份并比较增量,不能按单次快照判断当前热点。若怀疑网络收包路径,再用实际接口名和前一步定位到的 IRQ 核对:

IFACE=<actual-interface>
ip -s link show dev "$IFACE"
ethtool -S "$IFACE"
grep -E 'CPU|eth|virtio|ena|mlx' /proc/interrupts
cat /proc/softirqs
cat /proc/net/softnet_stat
cat /proc/irq/<actual-irq>/smp_affinity_list
grep . /sys/class/net/"$IFACE"/queues/rx-*/rps_cpus

ethtool 为可选工具,统计项名称随驱动变化;softnet_stat 的字段也应按运行内核文档解释,不宜跨版本硬套列号。读取 IRQ、驱动统计或其他进程通常需要相应权限。

判断时可将 SCPU 中的 NET_RX/NET_TX、对应网卡 IRQ 增量、ksoftirqd/N 所在 CPU 与接口丢包放在同一时间轴:若它们集中在同一 CPU,而业务线程的 %usr 并不突出,更接近中断或收包队列分布问题;若软中断均匀且热点稳定落在业务线程,才继续按正文进入代码采样。RSS、RPS、XPS、IRQ affinity 与 irqbalance 会相互影响,排查阶段先读取现状,不要直接改掩码。

处置后的验证应保持相近流量,重复同样的 1 秒采样,确认每 CPU 的软中断不再异常集中,并同时检查接口丢包、请求尾延迟和吞吐;只看到某个 ksoftirqd CPU 降低还不构成闭环。

还可以补一个容易漏掉的分支:CPU 很忙,但有效算力因降频变小。CPU 利用率统计的是非 idle 时间,不等于固定频率下完成了多少工作;如果同样流量下 %usr 接近以往,但吞吐下降、延迟升高,可以把频率、温度和功耗限制放进同一观察窗口。

先只读当前 cpufreq 策略和内核告警:

grep . /sys/devices/system/cpu/cpufreq/policy*/scaling_{driver,governor,min_freq,max_freq,cur_freq} 2>/dev/null
journalctl -k --since '-15 min' --no-pager \
  | grep -Ei 'thermal|throttl|power limit|overheat'

这些 sysfs 文件取决于内核、驱动和硬件,缺失不能直接解释成“没有降频”;scaling_cur_freq 也可能是驱动估算值或瞬时值,需要连续采样,不能用一次读数定性。x86 主机若已安装与运行内核配套的 turbostat,可在评估开销和权限后短时观察 Busy_MHz、温度及 throttling 相关列;虚拟机里的频率字段则常由宿主机抽象,结论要回到平台侧指标核对。

判断时最好对照相近流量下的每请求 CPU 时间、吞吐和尾延迟:若 CPU 时间相近但实际频率持续降低,并伴随温度或功耗限制证据,更像散热或 power cap 问题;若频率正常而某线程 %usr 增长,再回到正文的代码热点分支。排查阶段先别改 governor、频率上限或功耗限制,处置后也要在相近负载下重复同样采样。

再补一个常见误判:监控里的 1 分钟 CPU 平均值低于配额,不代表请求高峰没有被 CFS 短周期节流。cpu.max 的 quota 是按 period 补充的,突发并行任务可能提前耗完当期额度,随后等到下一周期;长窗口利用率会把这段等待摊平。

在 cgroup v2 且已确认目标 cgroup 路径后,可以保留同一故障窗口的两份累计快照:

date -u --iso-8601=seconds
cat "/sys/fs/cgroup${CGROUP}/cpu.max"
cat "/sys/fs/cgroup${CGROUP}/cpu.stat"
sleep 10
date -u --iso-8601=seconds
cat "/sys/fs/cgroup${CGROUP}/cpu.stat"

重点看这 10 秒内 nr_periodsnr_throttledthrottled_usec 的增量,并和请求尾延迟、并发数放在同一时间轴。nr_throttled / nr_periods 的增量比值只能说明有多少调度周期发生过节流,不能直接当成“损失了多少 CPU”;throttled_usec 也不应和请求等待时间一比一换算。采样窗口应覆盖实际尖峰,避免只在事故后读一次累计值。

若平均 CPU 不高,但每次延迟尖峰都伴随节流计数快速增长,更像并发突发与 quota/period 不匹配;若计数不增长,再回到单线程热点、软中断或宿主机争用分支。调整配额或周期前先确认父 cgroup 约束,改后用相近流量和相同采样间隔复测。