Skip to content

宿主机高 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 的残留(容器尚未完全回收),一般可忽略,持续存在时检查容器运行时。