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 的历史曲线。