主题
宿主机高 CPU 进程定位 K8s 副本排查脚本(k8s-node-cpu-process-to-pod.sh)
从节点 OS 层自下而上排查:高 CPU 进程 →
/proc/<pid>/cgroup反解 → 容器 ID/Pod UID → 精确归属到 Pod 副本。与集群视角的 K8s Worker 节点高 CPU 副本排查脚本 互补,组成完整的「节点 CPU 高」排查方案。
为什么需要宿主机视角(方案优化点)
集群视角(kubectl top)有三个盲区,本脚本全部覆盖:
| 盲区 | 集群视角 | 宿主机视角(本脚本) |
|---|---|---|
| 宿主机守护进程(systemd 服务、监控 agent、失控脚本)吃 CPU | ❌ 看不到 | ✅ 标记「宿主机进程」并提示 systemctl status <pid> |
| 非 K8s 管理的普通容器(docker run 手动起的) | ❌ 看不到 | ✅ 标记「普通容器」并给出容器 ID |
| metrics-server 故障/数据延迟时定位 | ❌ 依赖它 | ✅ 不依赖,直接读 /proc + crictl |
| 精确到容器 ID 级反查 | 间接(按节点过滤) | ✅ cgroup 直接解析,兼容 systemd/cgroupfs 两种命名 |
推荐排查动线:
kubectl top nodes 发现节点 CPU 高
│
├─ ① 集群视角(在任意有 kubectl 的机器):
│ k8s-worker-cpu-top.sh -n <node> → 哪个 Pod/副本、是否超 limit
│
└─ ② 宿主机视角(ssh 到该节点, 本脚本):
k8s-node-cpu-process-to-pod.sh → 进程级确认 + 揪出非容器元凶
│
├─ K8s-Pod → kubectl top/exec/describe 深入(见文末)
├─ 普通容器 → docker/crictl inspect 该容器
└─ 宿主机进程 → systemctl status <pid>核心原理
K8s 为每个 Pod/容器创建独立 cgroup,进程的 /proc/<pid>/cgroup 中直接包含 Pod UID 和容器 ID,两种命名风格均可解析:
# systemd cgroup 驱动(RKE2 默认): uid 中的 - 被转义为 _
/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid_下划线>.slice/cri-containerd-<64位容器ID>.scope
# cgroupfs 驱动:
/kubepods/burstable/pod<uid-中划线>/<64位容器ID>反查优先级:crictl inspect <容器ID>(精确到容器名,自动探测 RKE2 自带的 /var/lib/rancher/rke2/bin/crictl 和 containerd/crio/dockershim socket)→ kubectl get pods -A 按 UID 匹配(兜底)。同时从 cgroup 路径提取 QoS 等级(Guaranteed/Burstable/BestEffort),辅助判断 throttle 风险。
前置条件
- 在目标 worker 节点上执行,需 root(或能读其他进程的
/proc) - 反查通道(二选一,自动探测):
crictl(RKE2 节点自带)或kubectl+ kubeconfig
使用方法
bash
chmod +x k8s-node-cpu-process-to-pod.sh
./k8s-node-cpu-process-to-pod.sh # CPU TOP15 进程并定位 Pod 归属
./k8s-node-cpu-process-to-pod.sh -t 30 # 只看 CPU >= 30% 的进程
./k8s-node-cpu-process-to-pod.sh --top 30 # 列出 TOP30
./k8s-node-cpu-process-to-pod.sh -w 5 # 每 5 秒刷新(观测模式)输出示例
===================================================================
[1/3] 节点负载 (worker-02 2026-08-04 10:30:15)
===================================================================
10:30:15 up 12 days, load average: 5.82, 4.10, 2.55
CPU 核数: 4
===================================================================
[2/3] 高 CPU 进程 -> Pod 副本定位 (TOP 15, 阈值 >= 0%)
===================================================================
(反查通道: crictl @ unix:///run/k3s/containerd/containerd.sock)
PID CPU% MEM% COMMAND 类型 K8s归属(ns/pod) 容器/QoS
------------------------------------------------------------------------
18432 98.5 12.1 vllm K8s-Pod ai-inference/vllm-7b9f4c5d6-x2k9p cnt=vllm qos=Burstable
20115 45.2 3.4 node_exporter K8s-Pod monitoring/node-exporter-abcde cnt=node-exporter qos=Guaranteed
15230 22.0 1.1 backup.sh 宿主机进程 - 查: systemctl status 15230
9801 15.6 8.0 java 普通容器 - cid=3f8a1b2c9d4e (docker)
===================================================================
[3/3] 节点上 Pod 级 CPU 汇总 (crictl stats, 近似实时)
===================================================================
NAMESPACE POD CONTAINER CPU(m)
ai-inference vllm-7b9f4c5d6-x2k9p vllm 980
monitoring node-exporter-abcde node-exporter 210解读要点:vllm 是 Pod 高负载元凶;backup.sh 是宿主机进程(集群视角的盲区);java 是手动 docker run 的容器,不受 K8s 管控。
完整脚本
bash
#!/usr/bin/env bash
#
# k8s-node-cpu-process-to-pod.sh
# 宿主机视角排查: 高 CPU 进程 -> cgroup -> 容器 -> K8s Pod 副本
#
# 用法(在目标 worker 节点上执行, 需要 root 或能读 /proc):
# ./k8s-node-cpu-process-to-pod.sh # 列出 CPU TOP15 进程并定位 Pod 归属
# ./k8s-node-cpu-process-to-pod.sh -t 30 # 只看 CPU >= 30% 的进程
# ./k8s-node-cpu-process-to-pod.sh --top 30 # 列出 TOP30
# ./k8s-node-cpu-process-to-pod.sh -w 5 # 每 5 秒刷新(观测模式)
#
set -euo pipefail
THRESHOLD=0 # CPU% 阈值
TOP_N=15
WATCH=0
usage() { sed -n '5,20p' "$0"; exit 0; }
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--threshold) THRESHOLD="$2"; shift 2 ;;
--top) TOP_N="$2"; shift 2 ;;
-w|--watch) WATCH="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "未知参数: $1"; usage ;;
esac
done
# ---------- 依赖与运行环境探测 ----------
CRICTL=""
find_crictl() {
if command -v crictl >/dev/null 2>&1; then CRICTL="crictl"; return 0; fi
# RKE2/K3s 自带 crictl 但不在 PATH
for p in /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl; do
[[ -x "$p" ]] && { CRICTL="$p"; return 0; }
done
return 1
}
RUNTIME_EP=""
detect_runtime_endpoint() {
for sock in /run/k3s/containerd/containerd.sock \
/run/containerd/containerd.sock \
/var/run/crio/crio.sock \
/run/dockershim.sock; do
[[ -S "$sock" ]] && { RUNTIME_EP="unix://$sock"; return 0; }
done
return 1
}
HAVE_CRICTL=0; HAVE_KUBECTL=0
find_crictl && detect_runtime_endpoint && HAVE_CRICTL=1
command -v kubectl >/dev/null 2>&1 && HAVE_KUBECTL=1
# ---------- cgroup 解析: pid -> pod_uid, container_id, runtime, qos ----------
# 兼容两种 cgroup 命名:
# systemd 驱动: .../kubepods-burstable-pod<uid_下划线>.slice/cri-containerd-<64hex>.scope
# cgroupfs : .../kubepods/burstable/pod<uid-中划线>/<64hex>
parse_cgroup() {
local pid="$1" cg="" uid="" cid="" runtime="" qos="Guaranteed"
cg=$(grep -m1 -E 'kubepods|docker-|libpod|crio-' "/proc/$pid/cgroup" 2>/dev/null | cut -d: -f3 || true)
[[ -z "$cg" ]] && return 1
[[ "$cg" == *burstable* ]] && qos="Burstable"
[[ "$cg" == *besteffort* ]] && qos="BestEffort"
if [[ "$cg" =~ (cri-containerd|crio|docker)-([0-9a-f]{64})\.scope ]]; then
runtime="${BASH_REMATCH[1]}"; cid="${BASH_REMATCH[2]}"
elif [[ "$cg" =~ /([0-9a-f]{64})(\.scope)?$ ]]; then
cid="${BASH_REMATCH[1]}"
[[ "$cg" == *docker* ]] && runtime="docker" || runtime="containerd"
fi
# systemd 把 uid 中的 - 转义为 _
if [[ "$cg" =~ pod([0-9a-f]{8}(_[0-9a-f]{4}){3}_[0-9a-f]{12}) ]]; then
uid="${BASH_REMATCH[1]//_/-}"
elif [[ "$cg" =~ pod([0-9a-f-]{36}) ]]; then
uid="${BASH_REMATCH[1]}"
fi
# 不在 kubepods 下的容器 = 普通容器(非 K8s 管理)
local in_k8s=0
[[ "$cg" == *kubepods* ]] && in_k8s=1
echo "$in_k8s $uid $cid $runtime $qos"
return 0
}
# ---------- 归属反查 ----------
declare -A UID_CACHE # pod_uid -> "ns name"
declare -A CID_CACHE # container_id -> "ns pod container"
build_uid_map() {
[[ $HAVE_KUBECTL -eq 1 ]] || return 1
local rows
rows=$(kubectl get pods -A -o json 2>/dev/null | jq -r \
'.items[] | [.metadata.uid, .metadata.namespace, .metadata.name] | @tsv') || return 1
while IFS=$'\t' read -r uid ns name; do
UID_CACHE["$uid"]="$ns $name"
done <<< "$rows"
}
resolve_by_crictl() {
local cid="$1"
[[ $HAVE_CRICTL -eq 1 && -n "$cid" ]] || return 1
[[ -n "${CID_CACHE[$cid]:-}" ]] && { echo "${CID_CACHE[$cid]}"; return 0; }
local out
out=$("$CRICTL" --runtime-endpoint "$RUNTIME_EP" inspect "$cid" 2>/dev/null | jq -r \
'[.status.labels["io.kubernetes.pod.namespace"] // "?",
.status.labels["io.kubernetes.pod.name"] // "?",
.status.labels["io.kubernetes.container.name"] // "?"] | @tsv') || return 1
[[ "$out" == $'?\t?\t?' || -z "$out" ]] && return 1
CID_CACHE["$cid"]="$out"
echo "$out"
}
resolve_by_uid() {
local uid="$1"
[[ -n "$uid" && -n "${UID_CACHE[$uid]:-}" ]] || return 1
echo "${UID_CACHE[$uid]}"
}
# ---------- 主流程 ----------
run_once() {
echo "==================================================================="
echo "[1/3] 节点负载 ($(hostname) $(date '+%F %T'))"
echo "==================================================================="
uptime
nproc_val=$(nproc)
echo "CPU 核数: $nproc_val"
echo
echo "==================================================================="
echo "[2/3] 高 CPU 进程 -> Pod 副本定位 (TOP $TOP_N, 阈值 >= ${THRESHOLD}%)"
echo "==================================================================="
[[ $HAVE_CRICTL -eq 1 ]] && echo "(反查通道: crictl @ $RUNTIME_EP)" || echo "(反查通道: kubectl UID 匹配)"
printf "%-8s %6s %5s %-18s %-10s %-40s %s\n" \
"PID" "CPU%" "MEM%" "COMMAND" "类型" "K8s归属(ns/pod)" "容器/QoS"
printf -- "-%.0s" {1..120}; echo
shown=0
# 按 CPU 降序遍历进程
while read -r pid pcpu pmem comm; do
[[ $shown -ge $TOP_N ]] && break
# 阈值过滤(整数比较, pcpu 可能是小数)
pcpu_int=${pcpu%.*}
[[ "$pcpu_int" -lt "$THRESHOLD" ]] && continue
info=$(parse_cgroup "$pid" || echo "")
if [[ -z "$info" ]]; then
# 无容器 cgroup -> 宿主机进程
printf "%-8s %6s %5s %-18s %-10s %-40s %s\n" \
"$pid" "$pcpu" "$pmem" "${comm:0:18}" "宿主机进程" "-" "查: systemctl status $pid"
shown=$((shown+1)); continue
fi
read -r in_k8s uid cid runtime qos <<< "$info"
if [[ "$in_k8s" -eq 0 ]]; then
printf "%-8s %6s %5s %-18s %-10s %-40s %s\n" \
"$pid" "$pcpu" "$pmem" "${comm:0:18}" "普通容器" "-" "cid=${cid:0:12} ($runtime)"
shown=$((shown+1)); continue
fi
# K8s Pod: 优先 crictl 按容器 ID 精确反查, 失败则按 UID 匹配
resolved=""
if r=$(resolve_by_crictl "$cid" 2>/dev/null); then
resolved="$r"
elif r=$(resolve_by_uid "$uid" 2>/dev/null); then
resolved="$r"
fi
if [[ -n "$resolved" ]]; then
read -r rns rpod rcnt <<< "$resolved"
printf "%-8s %6s %5s %-18s %-10s %-40s %s\n" \
"$pid" "$pcpu" "$pmem" "${comm:0:18}" "K8s-Pod" "$rns/$rpod" "cnt=$rcnt qos=$qos"
else
printf "%-8s %6s %5s %-18s %-10s %-40s %s\n" \
"$pid" "$pcpu" "$pmem" "${comm:0:18}" "K8s-Pod" "(uid=${uid:-?} 未匹配,可能Pod已删)" "cid=${cid:0:12} qos=$qos"
fi
shown=$((shown+1))
done < <(ps -eo pid=,pcpu=,pmem=,comm= --sort=-pcpu | awk 'NR>0 {print}')
echo
echo "==================================================================="
echo "[3/3] 节点上 Pod 级 CPU 汇总 (crictl stats, 近似实时)"
echo "==================================================================="
if [[ $HAVE_CRICTL -eq 1 ]]; then
"$CRICTL" --runtime-endpoint "$RUNTIME_EP" stats -o json 2>/dev/null | jq -r \
'.stats[] | [.attributes.labels["io.kubernetes.pod.namespace"] // "-",
.attributes.labels["io.kubernetes.pod.name"] // "-",
.attributes.labels["io.kubernetes.container.name"] // "-",
((.cpu.usageCoreNanoSeconds.value // "0") | tonumber / 10000000 | floor | tostring)] | @tsv' 2>/dev/null \
| sort -t$'\t' -k4 -rn | head -10 \
| awk -F'\t' 'BEGIN{printf "%-18s %-42s %-22s %8s\n","NAMESPACE","POD","CONTAINER","CPU(m)"} {printf "%-18s %-42s %-22s %8s\n",$1,$2,$3,$4}' \
|| echo "(crictl stats 不可用, 可跳过)"
else
echo "(无 crictl, 跳过; 可在有 kubectl 的机器上用 k8s-worker-cpu-top.sh 补充)"
fi
cat <<'EOF'
--------------------------------------------------------------------
定位到 Pod 后的后续动作:
kubectl top pod <pod> -n <ns> --containers # 确认容器级用量
kubectl exec -it <pod> -n <ns> -- top -H # 线程级定位
kubectl describe pod <pod> -n <ns> | grep -A4 Limits # 是否超限被 throttle
kubectl delete pod <pod> -n <ns> # 应急: 让控制器重建副本
宿主机进程吃 CPU(非 K8s):
systemctl status <pid> 或 cat /proc/<pid>/cmdline 确认来源服务
EOF
}
build_uid_map || true
if [[ "$WATCH" -gt 0 ]]; then
while true; do clear; run_once; sleep "$WATCH"; done
else
run_once
fi注意事项
ps的 CPU% 是进程生命周期平均值,瞬时排查建议配合-w 5观测模式或直接看top;进程运行越久,平均值越被稀释,刚启动就飙高的进程更需关注。- cgroup 解析同时兼容 systemd 驱动(RKE2/Rocky Linux 默认)与 cgroupfs 驱动;v1/v2 层级均可(v2 下路径同样在
/proc/<pid>/cgroup单行中)。 - 输出中
qos=BestEffort的 Pod 无 requests/limits,是节点 CPU 争抢时的首要牺牲对象,建议参照 K8s Worker 节点高 CPU 副本排查脚本 中的建议补齐资源约束。 - 「uid 未匹配」通常说明进程是已删除 Pod 的残留(容器尚未完全回收),一般可忽略,持续存在时检查容器运行时。