文档目录

4.8 配套代码:容器节流诊断与环境一致性检查

对应小节:4.8 容器与编排的干扰 核心:一条命令确认「你的进程有没有被周期性掐停」。

一、节流诊断脚本(最重要的一份)

#!/usr/bin/env bash
# tools/check-throttling.sh [SAMPLE_SECONDS]
#
# 判断容器是否被 CPU 配额节流。
# 这是「本地好、上线慢」的头号原因的确认手段。
set -uo pipefail

DURATION="${1:-10}"

echo "═══ 容器 CPU 节流诊断 ═══"
echo

# ── cgroup v2(现代发行版)────────────────────────────────
if [ -f /sys/fs/cgroup/cpu.max ]; then
  read -r QUOTA PERIOD < /sys/fs/cgroup/cpu.max
  echo "cgroup 版本: v2"
  if [ "$QUOTA" = "max" ]; then
    echo "CPU 配额  : 无限制(未设置 CPU limit)"
    echo "结论      : 不存在节流问题"
    exit 0
  fi
  LIMIT_CORES=$(awk -v q="$QUOTA" -v p="$PERIOD" 'BEGIN{printf "%.2f", q/p}')
  echo "CPU 配额  : $QUOTA / $PERIOD = ${LIMIT_CORES} 核"
  echo

  echo "采样 ${DURATION} 秒..."
  P0=$(awk '/nr_periods/{print $2}' /sys/fs/cgroup/cpu.stat)
  T0=$(awk '/nr_throttled/{print $2}' /sys/fs/cgroup/cpu.stat)
  U0=$(awk '/throttled_usec/{print $2}' /sys/fs/cgroup/cpu.stat)
  sleep "$DURATION"
  P1=$(awk '/nr_periods/{print $2}' /sys/fs/cgroup/cpu.stat)
  T1=$(awk '/nr_throttled/{print $2}' /sys/fs/cgroup/cpu.stat)
  U1=$(awk '/throttled_usec/{print $2}' /sys/fs/cgroup/cpu.stat)

  awk -v p0="$P0" -v p1="$P1" -v t0="$T0" -v t1="$T1" -v u0="$U0" -v u1="$U1" -v d="$DURATION" 'BEGIN{
    dp = p1-p0; dt = t1-t0; du = u1-u0;
    printf "本窗口周期数      : %d\n", dp
    printf "本窗口被节流次数  : %d\n", dt
    if (dp > 0) {
      ratio = 100*dt/dp
      printf "节流比例          : %.2f%%\n", ratio
      printf "被节流总时长      : %.3f 秒 / %d 秒\n", du/1e6, d
      printf "时间被掐停占比    : %.2f%%\n", 100*(du/1e6)/d
      print ""
      if (ratio > 25)      print "结论: ❌ 严重节流 —— 这是 P99 抖动的很可能原因"
      else if (ratio > 5)  print "结论: ⚠️  存在节流,值得关注"
      else if (ratio > 1)  print "结论: ⚠️  轻微节流,通常可接受"
      else                 print "结论: ✅ 基本无节流"
    } else {
      print "结论: 本窗口内没有调度周期(应用可能空闲)"
    }
  }'

# ── cgroup v1(较老发行版)────────────────────────────────
elif [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ]; then
  QUOTA=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)
  PERIOD=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)
  echo "cgroup 版本: v1"
  if [ "$QUOTA" -lt 0 ] 2>/dev/null; then
    echo "CPU 配额: 无限制"
  else
    awk -v q="$QUOTA" -v p="$PERIOD" 'BEGIN{printf "CPU 配额: %d/%d = %.2f 核\n", q, p, q/p}'
    echo
    echo "cpu.stat 内容:"
    cat /sys/fs/cgroup/cpu/cpu.stat 2>/dev/null | head -5
    echo
    awk '/nr_periods/{p=$2} /nr_throttled/{t=$2} END{
      if (p>0) printf "累计节流比例: %.2f%%\n", 100*t/p
    }' /sys/fs/cgroup/cpu/cpu.stat
  fi
else
  echo "❌ 未检测到 cgroup CPU 限制"
  echo "   → 说明当前不在容器内,或容器未设置 CPU limit"
  echo "   → 本次实验【不能】用于推断生产环境(生产有 limit 时行为不同)"
fi

echo
echo "═══ 补充信息 ═══"
echo "容器内存 limit : $(cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || echo 'n/a')"
echo "可见 CPU 核数   : $(nproc)"
echo "  ⚠️ 注意:容器里 nproc 可能显示宿主机的核数,与 CPU limit 不一致"

二、JVM 与容器内存对齐检查

#!/usr/bin/env bash
# tools/check-jvm-memory.sh <PID>
#
# 检查 JVM 堆设置与容器内存 limit 是否匹配。
# 不匹配的典型后果:OOMKilled(进程被无声杀死)。
set -uo pipefail

PID="${1:?usage: check-jvm-memory.sh <pid>}"

echo "═══ JVM 与容器内存对齐检查 ═══"
echo

# 容器内存 limit
if [ -f /sys/fs/cgroup/memory.max ]; then
  MEM_LIMIT_BYTES=$(cat /sys/fs/cgroup/memory.max)
elif [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
  MEM_LIMIT_BYTES=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
else
  MEM_LIMIT_BYTES=0
fi

# JVM 堆上限
MAX_HEAP=$(jcmd "$PID" VM.flags 2>/dev/null | tr ' ' '\n' | grep -iE "^-XX:MaxHeapSize=" | head -1)
MAX_HEAP_MB=$(jcmd "$PID" VM.flags 2>/dev/null | tr ' ' '\n' | grep -iE "^-XX:MaxHeapSize=" | head -1 | sed 's/.*=//' | awk '{printf "%.0f", $1/1048576}')
[ -z "$MAX_HEAP_MB" ] && MAX_HEAP_MB=0

# 直接内存与元空间
MAX_DIRECT=$(jcmd "$PID" VM.flags 2>/dev/null | tr ' ' '\n' | grep -iE "MaxDirectMemorySize" | head -1 | sed 's/.*=//')
MAX_META=$(jcmd "$PID" VM.flags 2>/dev/null | tr ' ' '\n' | grep -iE "MaxMetaspaceSize" | head -1 | sed 's/.*=//')

if [ "$MEM_LIMIT_BYTES" != "0" ] && [ "$MEM_LIMIT_BYTES" != "max" ]; then
  MEM_LIMIT_MB=$(( MEM_LIMIT_BYTES / 1048576 ))
  echo "容器内存 limit : ${MEM_LIMIT_MB} MB"
  echo "JVM 堆上限     : ${MAX_HEAP_MB} MB"
  echo "堆外预留       : $(( MEM_LIMIT_MB - MAX_HEAP_MB )) MB"
  echo "  MaxDirectMemorySize: ${MAX_DIRECT:-未设置(默认≈堆大小,需注意!)}"
  echo "  MaxMetaspaceSize   : ${MAX_META:-未设置(默认无上限)}"
  echo

  if [ "$MAX_HEAP_MB" -gt 0 ]; then
    RATIO=$(awk -v h="$MAX_HEAP_MB" -v m="$MEM_LIMIT_MB" 'BEGIN{printf "%.0f", 100*h/m}')
    echo "堆占容器内存比例: ${RATIO}%"
    if [ "$RATIO" -gt 85 ]; then
      echo "结论: ❌ 堆占比过高,几乎必然 OOMKilled"
      echo "      建议: 堆 = 容器 limit 的 50%~75%(例如 ${MEM_LIMIT_MB}MB → $(( MEM_LIMIT_MB * 60 / 100 ))MB)"
    elif [ "$RATIO" -gt 75 ]; then
      echo "结论: ⚠️  偏紧,需确认堆外内存的实际使用量"
    else
      echo "结论: ✅ 比例合理"
    fi
  fi
else
  echo "容器内存 limit : 无限制或非容器环境"
  echo "JVM 堆上限     : ${MAX_HEAP_MB} MB"
  echo "  ⚠️ 本次实验不能推断生产环境(生产通常设置了内存 limit)"
fi

echo
echo "═══ 堆外内存实际使用(需 -XX:NativeMemoryTracking=summary)═══"
jcmd "$PID" VM.native_memory summary 2>/dev/null | head -25 || \
  echo "(未开启 NativeMemoryTracking,无法查看。启动时加 -XX:NativeMemoryTracking=summary)"

三、压测环境与生产一致性检查

#!/usr/bin/env bash
# tools/check-env-parity.sh <EXPECTED_CPU_CORES> <EXPECTED_MEM_MB>
#
# 对比压测环境与生产的关键配置。不一致则数据不可迁移。
set -uo pipefail

EXP_CPU="${1:?usage: check-env-parity.sh <expected_cpu_cores> <expected_mem_mb>}"
EXP_MEM="${2:?}"

FAILED=0
echo "═══ 环境一致性检查(压测环境 vs 生产)═══"
echo

# ① CPU limit
if [ -f /sys/fs/cgroup/cpu.max ]; then
  read -r Q P < /sys/fs/cgroup/cpu.max
  if [ "$Q" = "max" ]; then
    ACT_CPU="无限"
  else
    ACT_CPU=$(awk -v q="$Q" -v p="$P" 'BEGIN{printf "%.2f", q/p}')
  fi
  echo "CPU limit : 生产 [${EXP_CPU} 核]  压测 [${ACT_CPU} 核]"
  if [ "$Q" = "max" ]; then
    echo "   ❌ 压测环境未设置 CPU limit —— 数据与生产不可比"
    FAILED=1
  fi
else
  echo "CPU limit : 压测环境非容器或未设限制 ❌"
  FAILED=1
fi
echo

# ② 内存 limit
if [ -f /sys/fs/cgroup/memory.max ]; then
  M=$(cat /sys/fs/cgroup/memory.max)
  if [ "$M" = "max" ]; then
    ACT_MEM="无限"
  else
    ACT_MEM=$(( M / 1048576 ))
  fi
  echo "内存 limit: 生产 [${EXP_MEM} MB]  压测 [${ACT_MEM} MB]"
  [ "$ACT_MEM" != "$EXP_MEM" ] && { echo "   ❌ 不一致,数据不可迁移"; FAILED=1; }
else
  echo "内存 limit: 压测环境未设限制 ❌"
  FAILED=1
fi
echo

# ③ JVM 参数
echo "JVM 参数(必须完全一致):"
echo "  ${JVM_ARGS:-(未设置 JVM_ARGS 环境变量,无法自动对比)}"
echo "  → 请手工核对:-Xmx / -Xms / GC 选型 / 其他 -XX"
echo

# ④ 节点规格
echo "节点信息:"
echo "  可见核数: $(nproc)(注意:容器里可能显示宿主机的核数)"
echo "  内存总量: $(free -h 2>/dev/null | awk '/Mem:/{print $2}' || echo n/a)"
echo

# ⑤ sidecar 检查
echo "同 Pod 内的其他容器(sidecar 会分走资源):"
echo "  → 在 K8s 里执行: kubectl get pod <pod> -o jsonpath='{.spec.containers[*].name}'"
echo

if [ "$FAILED" -eq 0 ]; then
  echo "✅ 关键项一致(仍需人工核对 JVM 参数与 sidecar)"
else
  echo "❌ 存在不一致项 —— 本次压测数据【不可用于容量规划】"
  echo "   可以用于「相对比较」(优化前后),但必须在报告里声明"
fi

四、K8s 侧的资源与节流检查

#!/usr/bin/env bash
# 在 K8s 环境里使用(需要 kubectl)
POD="${1:?usage: check-k8s-resources.sh <pod>}"

echo "═══ Pod 资源配置 ═══"
kubectl get pod "$POD" -o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}'
echo

echo "═══ 容器实际使用 vs Limit ═══"
kubectl top pod "$POD" --containers 2>/dev/null || echo "(需要 metrics-server)"
echo

echo "═══ 节流指标 ═══"
kubectl exec "$POD" -- cat /sys/fs/cgroup/cpu.stat 2>/dev/null | head -5 || \
  echo "(无权限 exec,请通过 Prometheus 查询 container_cpu_cfs_throttled_periods_total)"
echo

echo "═══ OOMKilled 历史(内存 limit 过小会静默杀进程)═══"
kubectl get pod "$POD" -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}'
kubectl describe pod "$POD" | grep -A3 -E "Last State|Reason" | head -10

Prometheus 侧的对应查询:

-- 节流比例(最灵敏的节流指标)
rate(container_cpu_cfs_throttled_periods_total{container="app"}[5m])
  / rate(container_cpu_cfs_periods_total{container="app"}[5m])

-- CPU 使用率(对比 limit)
rate(container_cpu_usage_seconds_total{container="app"}[5m])
  / on(pod) kube_pod_container_resource_limits{resource="cpu"}

-- 被 OOMKilled 的次数
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}

五、动手改造

改动 观察什么
用 docker run --cpus=0.5 跑服务,然后压测 节流比例会很高,P99 抖动明显——最直观的节流实验
把 --cpus 从 0.5 改成 4 节流消失,P99 稳定——对比两次的火焰图,看"被掐停"的影响
在容器里运行 nproc 会显示宿主机的核数,与 --cpus=0.5 严重不符——这是"以为有 8 核其实只有 0.5 核"的陷阱
用 -XX:ActiveProcessorCount=1 配合 --cpus=1 让 JVM 的线程池/调度器与配额匹配
把 -Xmx 设成容器内存 limit 的 100% 触发 OOMKilled,kubectl describe pod 里能看到原因
把 check-env-parity.sh 接进实验驱动器 从流程上保证"数据可迁移"

六、这段代码的局限

  • /proc/cpuinfo 与 nproc 在容器里显示的是宿主机的信息(除非挂了 lxcfs)。必须以 cgroup 的 cpu.max 为准。
  • st(steal)在部分虚拟化环境中不可靠,此时节流比例(cgroup 层)比 steal 更可信。
  • 节流比例高不等于「一定要加 CPU」——如果 CPU 需求本身不合理(比如日志占 20%),应该先优化代码(第 4.8 节)。
  • 一次性采样可能错过节流:如果应用是间歇性繁忙的,短窗口采样可能落在空闲期。建议连续采样多次,或直接看 Prometheus 的历史曲线。