文档目录

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。
  • 快照是瞬时的:故障排查时应该连续采集多次(类似线程快照的思路),才能看出趋势。