4.5 配套代码:系统级观测命令与快照脚本
对应小节:4.5 系统级观测 一条命令采集完整的系统快照——比逐个敲命令更不容易漏项。
一、一键系统快照脚本
#!/usr/bin/env bash
# tools/system-snapshot.sh <OUTPUT_FILE> [PID]
#
# 采集完整的系统级快照。适合在压测进行中或故障排查时运行。
set -uo pipefail
OUT="${1:?usage: system-snapshot.sh <output_file> [pid]}"
PID="${2:-}"
mkdir -p "$(dirname "$OUT")"
{
echo "═══ 系统快照 $(date -Iseconds) ═══"
echo
# ── CPU 总体与五段分解 ─────────────────────────────────
echo "───── CPU 总体(重点看 sy / wa / st)─────"
if command -v mpstat > /dev/null 2>&1; then
mpstat 1 3 | tail -4
else
top -b -n 1 | head -5
fi
echo
echo "───── CPU 频率(是否降频)─────"
if [ -f /proc/cpuinfo ]; then
grep -m4 "MHz" /proc/cpuinfo
echo "governor: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo n/a)"
else
echo "(非 Linux,跳过)"
fi
echo
# ── 上下文切换 ────────────────────────────────────────
echo "───── 上下文切换(cswch=自愿/锁竞争, nvcswch=被抢占/线程过多)─────"
if [ -n "$PID" ]; then
pidstat -w -p "$PID" 1 3 2>/dev/null | tail -4
else
echo "(未指定 PID,显示系统级)"
fi
vmstat 1 3 | tail -3
echo
# ── 进程级 CPU ────────────────────────────────────────
if [ -n "$PID" ]; then
echo "───── 进程 CPU(PID=$PID)─────"
pidstat -u -p "$PID" 1 3 2>/dev/null | tail -4
echo
fi
# ── 内存 ──────────────────────────────────────────────
echo "───── 内存 ─────"
free -h 2>/dev/null || vm_stat 2>/dev/null | head -6
echo
# ── 磁盘 IO(看 await,不是 %util)────────────────────
echo "───── 磁盘 IO(关键:await)─────"
if command -v iostat > /dev/null 2>&1; then
iostat -x 1 2 | grep -E "Device|^sd|^nvme|^dm-" | head -10
else
echo "(iostat 不可用,请安装 sysstat)"
fi
echo
# ── 网络 ──────────────────────────────────────────────
echo "───── 网络连接状态 ─────"
if command -v ss > /dev/null 2>&1; then
ss -s
echo
echo "连接状态分布:"
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn | head -8
echo
echo "TIME_WAIT 数量: $(ss -tan | grep -c TIME-WAIT || echo 0)"
fi
echo
echo "───── 网络重传与错误 ─────"
if [ -f /proc/net/snmp ]; then
grep -A1 "^Tcp:" /proc/net/snmp | tail -1 | awk '{
printf "重传段数=%s 错误段数=%s\n", $13, $14
}' 2>/dev/null || echo "(解析失败,请手工查看 /proc/net/snmp)"
else
netstat -s 2>/dev/null | grep -E "retransmit|error|drop" | head -5
fi
echo
# ── 容器限制 ──────────────────────────────────────────
echo "───── 容器资源限制 ⭐ ─────"
if [ -f /sys/fs/cgroup/cpu.max ]; then
echo "cpu.max: $(cat /sys/fs/cgroup/cpu.max)"
echo "--- cpu.stat(重点看 nr_throttled)---"
head -5 /sys/fs/cgroup/cpu.stat
# 计算节流比例
awk '/nr_periods/{p=$2} /nr_throttled/{t=$2} END{
if (p>0) printf "节流比例: %.1f%%%s\n", 100*t/p, (t/p>0.25 ? " ⚠️ 严重" : "")
}' /sys/fs/cgroup/cpu.stat
elif [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ]; then
echo "cgroup v1:"
echo " quota: $(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)"
echo " period: $(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)"
else
echo "(非容器环境或 cgroup 不可见)"
fi
echo "memory.max: $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo n/a)"
echo
# ── 文件描述符 ────────────────────────────────────────
echo "───── 文件描述符 ─────"
echo "ulimit -n: $(ulimit -n)"
if [ -n "$PID" ] && [ -d "/proc/$PID/fd" ]; then
echo "当前进程 FD 数: $(ls /proc/"$PID"/fd | wc -l)"
fi
echo
# ── NUMA 与超线程 ─────────────────────────────────────
echo "───── 硬件拓扑 ─────"
if command -v lscpu > /dev/null 2>&1; then
lscpu | grep -E "^CPU\(s\)|Thread|Core|Socket|NUMA|Model name"
fi
} > "$OUT" 2>&1
echo "✅ 系统快照 → $OUT"
echo
echo "快速判读入口:"
echo " grep -A3 'CPU 总体' '$OUT' # 看 sy / st 是否偏高"
echo " grep -A2 '上下文切换' '$OUT' # 看切换次数"
echo " grep -A3 '容器资源限制' '$OUT' # 看节流比例"
echo " grep -A2 '磁盘 IO' '$OUT' # 看 await"
二、关键指标的判读标准
| 指标 | 正常范围 | 偏高意味着 | 下一步 |
|---|---|---|---|
us |
视业务 | 应用真在算 | 采 CPU 火焰图 |
sy |
< 15% | 系统调用/锁竞争/页错误 | 采火焰图看内核栈;查锁 |
wa |
< 5% | 等磁盘 | 看 iostat -x 的 await |
st |
< 1% | 被邻居抢 CPU | 换环境,优化代码无效 |
cswch/s |
几千–几万 | 锁竞争或 IO 等待 | 线程快照 + lock 火焰图 |
nvcswch/s |
几百–几千 | 线程数远超核数 | 减少线程/改协程 |
iostat await |
< 10 ms | 磁盘慢或 IO 密集 | 看具体设备与卷类型 |
TIME_WAIT |
< 2 万 | 连接复用不足 | 开启 keep-alive |
| 节流比例 | < 1% | CPU limit 太小 | 见第 4.8 节 |
三个最容易被忽略的(加粗项):sy 高、st 高、nvcswch/s 高。
三、两个指标的深入用法
3.1 st(steal)——判断"优化是否有意义"
# 采集 10 次 st 值
for i in $(seq 1 10); do
vmstat 1 1 | tail -1 | awk '{print "st=" $17 "%"}'
sleep 1
done
判读:
st持续 < 1%:环境没问题,继续优化代码。st持续 > 3%:你的优化努力有很大一部分会被环境吃掉——先解决环境问题(换独占实例、调整部署)。
3.2 nvcswch/s —— 验证"线程数是否过多"
# 观察 5 秒
pidstat -w -p "$PID" 1 5 | tail -6
计算方法:
如果 nvcswch/s ≈ 每秒几十万,而 CPU 核数是 8
→ 说明每个核上有很多线程在轮转
→ 应该减少线程数或改用协程
四、off-CPU 分析(进阶)
# 方法一:async-profiler 的 wall 模式(推荐,最简单)
asprof -d 60 -e wall -f wall.html "$PID"
# 方法二:perf 的调度事件(需要权限)
perf record -e sched:sched_stat_sleep -e sched:sched_switch -a -g -- sleep 10
perf report --stdio | head -50
# 方法三:bpftrace 一行统计阻塞时长
bpftrace -e '
tracepoint:sched:sched_switch /args.prev_state == 2/ {
@[kstack] = sum(nsecs - args.prev_state_start);
}
'
判读:off-CPU 分析回答「线程不在 CPU 上时去哪了」。这与 wall 火焰图的目标一致,但 perf 能看到内核态的阻塞原因(等锁、等 IO、等调度)。
实践建议:优先用 async-profiler 的
wall(更安全、无需权限),只有需要区分"等锁 vs 等 IO"时才用 perf。
五、动手改造
| 改动 | 观察什么 |
|---|---|
| 在压测进行中运行快照脚本 | 能看到负载下的真实系统状态(而不是空闲时的) |
在容器里运行并对比宿主机的 nproc |
容器看到的核数可能远超 CPU limit——这就是"以为有 8 核其实只有 1 核"的陷阱 |
| 运行快照两次(压测前/压测中) | 对比哪些指标发生了变化——这就是瓶颈线索 |
用 -XX:ActiveProcessorCount=1 限制 JVM 看到的核数 |
观察 Dispatchers.Default 并行度、线程池大小的变化 |
加 taskset -c 0-1 绑核运行 |
对比不绑核时的波动——绑核能降低噪声但可能降低吞吐 |
六、这段代码的局限
st在部分云环境上报不准(宿主机可能不上报 steal 时间),此时要结合多台实例的延迟差异来判断。iostat的%util对 NVMe 意义有限,只看await和队列深度。- 容器里看到的
/proc/cpuinfo是宿主机的(除非用了lxcfs),所以要以 cgroup 的cpu.max为准,而不是nproc。 - 快照是瞬时的:故障排查时应该连续采集多次(类似线程快照的思路),才能看出趋势。