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
七、本节小结
- CPU 要看五个组成部分,其中两个最容易被忽略:
sy(内核在忙)和st(被邻居抢走)。 sy高 通常指向系统调用多、锁竞争、或页错误——此时优化用户态代码收益有限。st高 意味着环境问题,任何应用层优化都无效——先换环境。- 上下文切换:
cswch/s高 → 锁竞争;nvcswch/s高 → 线程数超过核数。 - 磁盘看
await而不是%util;网络看TIME_WAIT与重传。 perf/bpftrace能深入内核,但优先用 async-profiler 的wall或 JFR 来回答「等在哪」。
八、自测
- 一个服务的
top显示us=8%, sy=42%, wa=2%。这说明什么?你会先查哪三件事? - 你的容器 CPU 利用率只有 30%,但延迟抖动很大;
vmstat显示st是 8%。请解释这两件事的关系,并说明你的优化努力是否会有回报。 - 一个服务加了线程池大小后,
nvcswch/s从 3000 涨到 45000,QPS 反而下降。请解释原因,并说出你会怎么调整。
- 说明瓶颈在内核态(
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)与上下文切换次数辅助判断。 st=8%意味着你的容器/虚拟机有 8% 的 CPU 时间被宿主机的其他租户抢走了。这解释了「利用率只有 30% 但抖动大」——因为你的进程在该运行的时候被强制暂停,恢复后延迟自然出现尖刺。优化是否会有回报:① 对于平均吞吐类问题,优化仍有部分回报;② 对于尾延迟类问题,回报很有限——因为抖动源在环境层,不在你的代码里。正确做法:先确认st是否持续偏高(多次采样),如果确认,应优先解决环境问题(换独占型实例、调整部署位置、避开高负载时段),而不是继续优化代码。- 原因:
nvcswch/s从 3000 涨到 45000 说明大量线程被强制抢占——线程数远超 CPU 核数,操作系统必须不断切换它们。每次切换的代价包括:保存/恢复上下文(1–5 微秒)+ CPU 缓存失效(新线程要重新加载工作集)。当切换频率高到一定程度,花在切换上的 CPU 时间超过了并行带来的收益,QPS 自然下降(第 2 章 2.6 节)。怎么调整:① 把线程数降到接近 CPU 核数(对于 CPU 密集型任务)——先用压测找到「吞吐拐点」对应的线程数;② 如果是 IO 密集型任务,改用协程(降低切换成本)而不是继续加线程;③ 检查是否有锁竞争(cswch/s也高的话),因为锁竞争会同时造成自愿切换与非自愿切换;④ 用pidstat -w与pidstat -u一起观察,确认瓶颈是 CPU 还是锁。