文档目录

4.5 系统级观测:sys、steal 与上下文切换

上一节:4.4 JFR 与 jcmd | 下一节:4.6 应用可观测性 配套代码:04-toolchain/05-system-observability


一句话结论

JVM 指标告诉你「应用怎么了」,系统指标告诉你「环境怎么了」。 三个最容易被忽略、信息量最大的系统指标是:sys CPU(内核在忙什么)、steal(被虚拟机邻居抢走多少)、上下文切换次数(锁竞争与线程过多的信号)。


一、用「体检的血检」理解系统观测

去医院看病,医生不会只看你说「哪不舒服」(应用指标),还会让你做血检、尿检(系统指标)——因为身体大环境的问题会表现为各种局部症状。

血检项 对应系统指标 提示
白细胞 sys CPU 有"炎症"(系统调用/锁/页错误)
血糖 steal 被外部因素影响(虚拟机邻居)
心率 上下文切换 系统在频繁"换人"
肾功能 磁盘 await 排毒能力(IO 能力)

关键:应用层的所有指标都建立在系统资源之上。系统层的瓶颈会伪装成应用层的问题。


二、CPU 的五个组成部分(不要只看总数)

# 最常用的三个命令
top -b -n 1 | head -5
pidstat -u -p <pid> 1
mpstat -P ALL 1

top 那一行 %Cpu(s) 的解读:

%Cpu(s): 12.3 us, 34.5 sy,  0.0 ni, 51.2 id,  1.2 wa,  0.0 hi,  0.8 si,  0.0 st
          │        │          │        │        │        │        │        └─ steal(被邻居抢走)
          │        │          │        │        │        │        └─ 软中断
          │        │          │        │        │        └─ 硬中断
          │        │          │        │        └─ IO 等待
          │        │          │        └─ 空闲
          │        │          └─ nice(低优先级)
          │        └─ sys(内核态)
          └─ user(用户态)
指标 高意味着 常见原因
us(user) 应用代码在算 正常;如果过高说明 CPU 是瓶颈
sy(sys) ⭐ 内核在忙 频繁系统调用、锁竞争、页错误、网络栈处理
wa(iowait) 等磁盘 磁盘慢或 IO 密集
si/hi(中断) 网络/设备中断 高流量网络、中断没做多队列
st(steal) ⭐ 被虚拟机邻居抢走 CPU 云主机超卖;你的优化再多也没用

两个最容易被忽略的指标

sy 高(比如 35%) 是一个强烈信号:

  • 你的进程在做大量系统调用(每次调用都要陷入内核);
  • 或者锁竞争(synchronized 的竞争在内核里排队);
  • 或者内存问题(页错误、TLB 失效)。

sy 高时,优化用户态代码收益有限——要去查上面这三种原因。用 async-profiler 采 CPU 火焰图,能看到内核栈(__pthread_mutex_lock、sys_... 之类)。

st 高 意味着:

你的容器/虚拟机想用 CPU,但宿主机的其他租户占用了。
表现为:你的应用什么都没做,延迟就是高。

这是「性能优化完全无效」的一类原因,必须先确认环境(换独占型实例、或者调整部署位置)。


三、上下文切换:锁竞争的信号

# 进程级的上下文切换
pidstat -w -p <pid> 1

# 系统级
vmstat 1     # 看 cs(context switches)和 in(interrupts)列

输出示例:

Average:      UID       PID   cswch/s nvcswch/s  Command
Average:     1000     12345  45000.00   12000.00  java
指标 含义
cswch/s 自愿切换(主动让出,如等 IO、等锁)
nvcswch/s 非自愿切换(被抢占,通常因为线程数 > 核数)

判读:

现象 含义
cswch/s 很高 锁竞争或大量 IO 等待(线程主动让出)
nvcswch/s 很高 线程数远超核数(CPU 时间片被耗尽)
两者都高,且吞吐不升 线程/协程模型有问题

经验值:正常服务 cs 通常在几千到几万/秒。超过 10 万/秒 就值得查(除非是极高并发的 IO 服务)。

上下文切换是「加线程反而变慢」的直接证据(第 2 章 2.6 节)。


四、磁盘与网络

磁盘

iostat -x 1
指标 含义 判读
%util 设备忙碌比例 100% 不等于慢(SSD 并行度高)
await 平均 IO 等待时间 这才是关键:超过 10 ms 就值得关注
r/s, w/s IOPS 与容量对比
aqu-sz 平均队列深度 持续 > 1 说明排队

注意:%util 是历史遗留指标,对 SSD/NVMe 意义有限。看 await。

网络

# 连接状态分布(TIME_WAIT 堆积是压测客户端的常见瓶颈)
ss -s

# 详细连接
ss -tnp state established | head -20

# 网络错误与重传
netstat -s | grep -E "retransmit|error|drop"
现象 含义
TIME_WAIT 数量巨大 连接复用不足(压测客户端常见)
重传率高 网络质量问题
socket 用尽 文件描述符或端口耗尽

五、perf 与 eBPF:深入到内核

# CPU 热点(用户态 + 内核态)
perf top -p <pid>
perf record -F 99 -p <pid> -g -- sleep 30
perf report

# off-CPU 分析(线程不在 CPU 上时去哪了)
perf record -e sched:sched_stat_sleep -e sched:sched_switch -a -g -- sleep 10

# eBPF(低开销定点观测)
bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[comm] = count(); }'
需求 工具
看内核态热点 perf top / perf record
看 off-CPU(等待) perf 的 sched 事件,或 async-profiler 的 wall
定时观测某个行为 bpftrace
不想装工具 async-profiler 的 wall + JFR

注意:perf 需要权限(perf_event_paranoid),且在生产环境采样本身有开销。优先用 JFR 的 wall 或 async-profiler 来回答「等在哪」,因为它们更安全。


六、环境相关的三个陷阱

陷阱 表现 怎么确认
CPU 降频 后跑的基准比先跑慢 grep MHz /proc/cpuinfo、cpupower frequency-info
NUMA 跨节点访问 大量核时扩展性差 numastat、numactl --hardware
超线程 「16 核」实际性能不等同 16 物理核 lscpu;或 grep siblings /proc/cpuinfo
邻居噪声(steal) 应用无异常但延迟抖动 top 看 st、vmstat

实验时的建议:

# 固定频率(避免降频干扰测量)
sudo cpupower frequency-set -g performance

# 绑定到指定核(避免调度抖动)
taskset -c 2-5 java -jar app.jar

# 查看 NUMA 拓扑
numactl --hardware

七、本节小结

  1. CPU 要看五个组成部分,其中两个最容易被忽略:sy(内核在忙)和 st(被邻居抢走)。
  2. sy 高 通常指向系统调用多、锁竞争、或页错误——此时优化用户态代码收益有限。
  3. st 高 意味着环境问题,任何应用层优化都无效——先换环境。
  4. 上下文切换:cswch/s 高 → 锁竞争;nvcswch/s 高 → 线程数超过核数。
  5. 磁盘看 await 而不是 %util;网络看 TIME_WAIT 与重传。
  6. perf/bpftrace 能深入内核,但优先用 async-profiler 的 wall 或 JFR 来回答「等在哪」。

八、自测

  1. 一个服务的 top 显示 us=8%, sy=42%, wa=2%。这说明什么?你会先查哪三件事?
  2. 你的容器 CPU 利用率只有 30%,但延迟抖动很大;vmstat 显示 st 是 8%。请解释这两件事的关系,并说明你的优化努力是否会有回报。
  3. 一个服务加了线程池大小后,nvcswch/s 从 3000 涨到 45000,QPS 反而下降。请解释原因,并说出你会怎么调整。
  1. 说明瓶颈在内核态(sy=42% 远高于 us=8%)——应用代码本身没在忙,但系统调用/锁竞争/页错误消耗了大量 CPU。先查三件事:① 锁竞争——用 async-profiler 采 lock 事件和 cpu 火焰图看内核栈(__pthread_mutex_lock、futex),或看线程快照里 BLOCKED 线程数;② 系统调用频率——是否有大量小 IO、频繁 write 日志、或 Recv/Send 调用(用 JFR 的 FileWrite/SocketWrite 事件,或 bpftrace 统计 syscall);③ 内存/页错误——是否频繁分配大对象导致缺页,或堆太小导致频繁 GC(GC 线程的扫描也是内核/内存活动)。另外可以看一下网络软中断(si)与上下文切换次数辅助判断。
  2. st=8% 意味着你的容器/虚拟机有 8% 的 CPU 时间被宿主机的其他租户抢走了。这解释了「利用率只有 30% 但抖动大」——因为你的进程在该运行的时候被强制暂停,恢复后延迟自然出现尖刺。优化是否会有回报:① 对于平均吞吐类问题,优化仍有部分回报;② 对于尾延迟类问题,回报很有限——因为抖动源在环境层,不在你的代码里。正确做法:先确认 st 是否持续偏高(多次采样),如果确认,应优先解决环境问题(换独占型实例、调整部署位置、避开高负载时段),而不是继续优化代码。
  3. 原因:nvcswch/s 从 3000 涨到 45000 说明大量线程被强制抢占——线程数远超 CPU 核数,操作系统必须不断切换它们。每次切换的代价包括:保存/恢复上下文(1–5 微秒)+ CPU 缓存失效(新线程要重新加载工作集)。当切换频率高到一定程度,花在切换上的 CPU 时间超过了并行带来的收益,QPS 自然下降(第 2 章 2.6 节)。怎么调整:① 把线程数降到接近 CPU 核数(对于 CPU 密集型任务)——先用压测找到「吞吐拐点」对应的线程数;② 如果是 IO 密集型任务,改用协程(降低切换成本)而不是继续加线程;③ 检查是否有锁竞争(cswch/s 也高的话),因为锁竞争会同时造成自愿切换与非自愿切换;④ 用 pidstat -w 与 pidstat -u 一起观察,确认瓶颈是 CPU 还是锁。