Skip to content

K8s Pod CPU Load 突刺排查实战

K8s 集群中部分节点出现 CPU Load 突刺,但不知道是节点上哪些 Pod 导致的。这是一个非常典型的运维痛点:节点级 node_load1 告警触发了,可登到机器上 top 一看,进程列表眼花缭乱,到底对应哪个 Pod?本文给出从指标采集 → 关联分析 → Grafana 可视化的完整方案,帮你做到"节点 Load 一突刺,立刻定位到 Pod"。

适用对象:运行 Kubernetes(含 Kubeadm / RKE2 / K3s 等发行版)的集群,监控栈以 Prometheus + Grafana 为准。

读完本文你将得到

  • 理解 CPU LoadCPU Usage 的本质区别,以及为什么 Pod 级 Load 采集比想象中复杂
  • 一套可直接部署的 Prometheus + Grafana 方案,实现节点 → Pod 的 CPU 突刺关联定位
  • 10+ 条核心 PromQL 查询语句,覆盖 CPU 使用率、CPU 节流、CFS 调度延迟等维度
  • 一个完整的 Grafana Dashboard JSON,导入即用
  • 进阶方案:eBPF (Tetragon/Cilium)自定义 DaemonSet 采集器的对比与选型

一、问题分析:CPU Load ≠ CPU Usage

1.1 概念澄清

概念定义采集层级能否归因到 Pod
CPU Usage容器实际消耗的 CPU 时间(秒/核)Pod/Container 级✅ cAdvisor 直接提供
CPU Load Average处于运行态(R) + 不可中断等待(D) 的进程/线程数均值Node 级❌ 无法直接拆分到 Pod
CPU Throttling容器因 CFS quota 限制被强制暂停的时间Pod/Container 级✅ cAdvisor 直接提供
Runnable Processes处于可运行状态的进程数Node 级⚠️ 需额外采集

1.2 为什么 Pod 级 Load 难以直接采集

Linux 的 Load Average 来自 /proc/loadavg,是节点级别的全局统计。cAdvisor / kubelet 并不提供"某个 Pod 贡献了多少 Load"这一指标。因此我们的策略是:

text
┌─────────────────────────────────────────────────────────────┐
│  思路:用多维 Pod 级 CPU 指标"拟合"出谁在制造 Load            │
│                                                              │
│  节点 Load 突刺 ──► 定位到具体节点                            │
│       │                                                      │
│       ▼                                                      │
│  该节点上所有 Pod 的:                                        │
│  ① CPU Usage 速率变化率(谁在突增?)                         │
│  ② CPU Throttling 时长(谁被限流了?)                        │
│  ③ CPU 使用占 Limit 比例(谁在打满 quota?)                  │
│  ④ CFS 调度周期数(谁在频繁抢占 CPU?)                       │
│                                                              │
│  交叉对比时间线 → 定位"元凶" Pod                              │
└─────────────────────────────────────────────────────────────┘

1.3 常见导致 Load 突刺的场景

场景特征关键指标
未设 CPU Limit 的 Pod 突发吃满 CPUUsage 突增,无 Throttlingcontainer_cpu_usage_seconds_total 速率飙升
设了 Limit 但请求量暴增Usage 打满 Limit,Throttling 严重container_cpu_cfs_throttled_seconds_total 飙升
GC / 编译 / 序列化等 CPU 密集操作周期性 Usage 尖峰Usage 速率呈规律性脉冲
IO Wait 导致 D 状态进程堆积Usage 不高但 Load 高node_cpu_seconds_total{mode="iowait"}
线程数暴增(Go goroutine 泄露等)线程数激增,Load 飙升需结合 process_threads 或自定义指标

二、方案架构

2.1 整体拓扑

text
┌────────────── 数据采集层 ──────────────────────────────────┐
│                                                             │
│  kubelet/cAdvisor ──────┐                                   │
│  (每节点内置,10250端口)  │   /metrics/cadvisor               │
│                         │                                   │
│  node-exporter ─────────┤   /metrics (9100端口)              │
│  (DaemonSet)            │                                   │
│                         │                                   │
│  kube-state-metrics ────┘   /metrics (8080端口)              │
│  (Deployment)               Pod/Deployment 对象状态           │
│                                                             │
└──────────────────┬──────────────────────────────────────────┘
                   │ Prometheus 定时 scrape (15s 间隔)

┌─── Prometheus ─────────────────────────────────────────────┐
│  存储 + 查询引擎                                            │
│  ServiceMonitor / PodMonitor 声明抓取目标                    │
│  Recording Rules 预计算高频查询                              │
│  Alert Rules 突刺告警                                       │
└──────────────────┬─────────────────────────────────────────┘


┌─── Grafana ────────────────────────────────────────────────┐
│  Dashboard: "Node → Pod CPU Load 关联分析"                  │
│  - Row 1: 节点 Load 总览(找出异常节点)                     │
│  - Row 2: 该节点上各 Pod CPU Usage 排名                     │
│  - Row 3: 该节点上各 Pod CPU Throttling 排名                │
│  - Row 4: 时间线叠加对比(Load vs Pod CPU)                  │
└────────────────────────────────────────────────────────────┘

2.2 前置条件检查

在开始之前,确认以下组件已就绪:

bash
# 1. 检查 kubelet cAdvisor 是否正常(所有节点应有数据)
kubectl get --raw /api/v1/nodes/<node-name>/proxy/metrics/cadvisor | head -5

# 2. 检查 node-exporter 是否以 DaemonSet 运行
kubectl get ds -A | grep node-exporter

# 3. 检查 kube-state-metrics 是否运行
kubectl get deploy -A | grep kube-state-metrics

# 4. 检查 Prometheus 是否可访问
kubectl get svc -n monitoring prometheus-kube-prometheus-prometheus

# 5. 检查 Grafana 是否可访问
kubectl get svc -n monitoring prometheus-grafana

如果集群使用 kube-prometheus-stack Helm chart 安装,上述组件(除 cAdvisor 内置外)通常已自动部署,跳过第三章的部署步骤,直接从第四章开始配置 Dashboard。

三、组件部署(如已有监控栈可跳过)

3.1 安装 kube-prometheus-stack

bash
# 添加 Helm repo
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# 安装(生产环境建议指定 values 文件)
helm install kube-prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  --set prometheus.prometheusSpec.retention=30d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=local-path \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi \
  --set grafana.adminPassword=<your-password>

# 验证所有组件就绪
kubectl get pods -n monitoring

3.2 确认 cAdvisor 指标可抓取

kube-prometheus-stack 默认已创建 cAdvisor 的 ServiceMonitor。验证方式:

bash
# 在 Prometheus UI 中检查 target 状态
# 访问 http://<prometheus-url>/targets
# 搜索 "cadvisor",确认所有节点状态为 UP

# 或者用命令行验证
kubectl get servicemonitor -n monitoring | grep cadvisor

3.3 调整采集间隔(可选但推荐)

默认 scrape 间隔 30s,对于突刺检测可能太粗。建议调为 15s:

yaml
# 编辑 cAdvisor 的 ServiceMonitor
kubectl edit servicemonitor -n monitoring kube-prometheus-kubelet

# 修改 cadvisor 端点的 interval
# spec.endpoints:
#   - interval: 15s       # 改为 15s
#     path: /metrics/cadvisor
#     port: https-metrics

四、核心 PromQL 查询

以下 PromQL 是 Dashboard 的核心,也可在 Prometheus UI 的 Graph 页面直接调试。

4.1 节点级:找出 Load 突刺的节点

promql
# 节点 1 分钟 Load Average(所有节点)
node_load1

# 找出 Load 超过 CPU 核数的节点(过载指标)
node_load1 / count without(cpu) (node_cpu_seconds_total{mode="idle"}) > 1

# Load 的 5 分钟变化率(突刺检测)
node_load1 - node_load1 offset 5m

4.2 Pod 级:CPU Usage(使用率)

promql
# 某节点上各 Pod 的 CPU 使用速率(核数)
sum by (namespace, pod) (
  rate(container_cpu_usage_seconds_total{
    node="<node-name>",
    container!="",
    container!="POD"
  }[5m])
)

# 按 CPU 使用量降序排列 Top 10 Pod
topk(10,
  sum by (namespace, pod) (
    rate(container_cpu_usage_seconds_total{
      node="<node-name>",
      container!="",
      container!="POD"
    }[5m])
  )
)

# Pod CPU 使用占 Limit 的百分比
sum by (namespace, pod) (
  rate(container_cpu_usage_seconds_total{node="<node-name>", container!=""}[5m])
)
/
sum by (namespace, pod) (
  container_spec_cpu_quota{node="<node-name>", container!=""}
  / container_spec_cpu_period{node="<node-name>", container!=""}
) * 100

4.3 Pod 级:CPU Throttling(节流)

promql
# Pod 被 CFS 节流的时间比例(0~1,1 表示完全被限流)
sum by (namespace, pod) (
  rate(container_cpu_cfs_throttled_seconds_total{
    node="<node-name>",
    container!=""
  }[5m])
)

# 被节流的调度周期占比(更直观)
sum by (namespace, pod) (
  rate(container_cpu_cfs_throttled_periods_total{
    node="<node-name>",
    container!=""
  }[5m])
)
/
sum by (namespace, pod) (
  rate(container_cpu_cfs_periods_total{
    node="<node-name>",
    container!=""
  }[5m])
) * 100

4.4 Pod 级:CPU Usage 突刺检测

promql
# Pod CPU 使用率的 5 分钟变化量(突刺检测核心查询)
sum by (namespace, pod) (
  rate(container_cpu_usage_seconds_total{node="<node-name>", container!=""}[2m])
)
-
sum by (namespace, pod) (
  rate(container_cpu_usage_seconds_total{node="<node-name>", container!=""}[2m]) offset 5m
)

# 使用 predict_linear 预测未来 10 分钟的 CPU 使用趋势
sum by (namespace, pod) (
  predict_linear(
    rate(container_cpu_usage_seconds_total{node="<node-name>", container!=""}[5m])[10m:1m],
    600
  )
)

4.5 IO Wait 排查(Load 高但 CPU Usage 不高时)

promql
# 节点 IO Wait 占比
avg by (node) (
  rate(node_cpu_seconds_total{mode="iowait"}[5m])
) * 100

# 节点上各 Pod 的磁盘 IO 写入速率(排查 IO 大户)
sum by (namespace, pod) (
  rate(container_fs_writes_bytes_total{node="<node-name>", container!=""}[5m])
)

五、Recording Rules 预计算(推荐)

对于大型集群,上述查询在 Dashboard 中实时计算可能较慢。建议添加 Recording Rules:

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pod-cpu-load-rules
  namespace: monitoring
  labels:
    prometheus: kube-prometheus
    role: alert-rules
spec:
  groups:
    - name: pod_cpu_load_recording
      interval: 15s
      rules:
        # 每 Pod CPU 使用速率(核数)
        - record: pod:container_cpu_usage_seconds_total:rate5m
          expr: |
            sum by (namespace, pod, node) (
              rate(container_cpu_usage_seconds_total{container!="", container!="POD"}[5m])
            )

        # 每 Pod CPU Throttling 比例
        - record: pod:container_cpu_cfs_throttled_ratio:rate5m
          expr: |
            sum by (namespace, pod, node) (
              rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m])
            )
            /
            sum by (namespace, pod, node) (
              rate(container_cpu_cfs_periods_total{container!=""}[5m])
            )

        # 节点 CPU 核数
        - record: node:cpu_cores:count
          expr: |
            count without(cpu) (node_cpu_seconds_total{mode="idle"})

        # 节点 Load / CPU 核数比(过载指数)
        - record: node:load1_per_core:ratio
          expr: |
            node_load1 / node:cpu_cores:count

六、告警规则

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pod-cpu-load-alerts
  namespace: monitoring
  labels:
    prometheus: kube-prometheus
    role: alert-rules
spec:
  groups:
    - name: pod_cpu_load_alerts
      rules:
        # 节点 Load 超过 CPU 核数持续 2 分钟
        - alert: NodeLoadHigh
          expr: node:load1_per_core:ratio > 1.5
          for: 2m
          labels:
            severity: warning
          annotations:
            summary: "节点 {{ $labels.node }} Load 过高"
            description: "节点 {{ $labels.node }} 的 Load1/CPU核数 = {{ $value | printf \"%.2f\" }},持续超过 2 分钟"
            dashboard: "https://grafana.example.com/d/pod-cpu-load-analysis"

        # Pod CPU 使用率突刺(2分钟内增长超过 1 核)
        - alert: PodCpuUsageSpike
          expr: |
            pod:container_cpu_usage_seconds_total:rate5m
            - pod:container_cpu_usage_seconds_total:rate5m offset 2m
            > 1.0
          for: 1m
          labels:
            severity: warning
          annotations:
            summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} CPU 使用突刺"
            description: "Pod {{ $labels.pod }} 在节点 {{ $labels.node }} 上 CPU 使用率 2 分钟内增长 {{ $value | printf \"%.2f\" }} 核"

        # Pod CPU Throttling 严重(超过 50% 的调度周期被节流)
        - alert: PodCpuThrottled
          expr: pod:container_cpu_cfs_throttled_ratio:rate5m > 0.5
          for: 5m
          labels:
            severity: info
          annotations:
            summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} CPU 被严重节流"
            description: "Pod {{ $labels.pod }} 有 {{ $value | printf \"%.0f\" }}% 的 CFS 调度周期被节流,可能需要调高 CPU Limit"

七、Grafana Dashboard

7.1 导入方式

  1. 登录 Grafana → Dashboards → Import
  2. 将下方 JSON 粘贴到 "Import via panel text" 输入框
  3. 选择 Prometheus 数据源 → Import

7.2 Dashboard JSON

json
{
  "annotations": { "list": [{ "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" }] },
  "editable": true,
  "graphTooltip": 1,
  "panels": [
    {
      "title": "🔴 节点 CPU Load 总览(找出异常节点)",
      "type": "row",
      "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
      "collapsed": false
    },
    {
      "title": "节点 Load1 / CPU 核数(过载指数)",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 },
      "fieldConfig": {
        "defaults": {
          "color": { "mode": "palette-classic" },
          "custom": { "lineWidth": 2, "fillOpacity": 10, "showPoints": "auto" },
          "unit": "percentunit",
          "thresholds": { "mode": "absolute", "steps": [
            { "color": "green", "value": null },
            { "color": "yellow", "value": 0.8 },
            { "color": "red", "value": 1.0 }
          ]}
        }
      },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [{ "expr": "node_load1 / count without(cpu) (node_cpu_seconds_total{mode=\"idle\"})", "legendFormat": "{{ instance }}" }]
    },
    {
      "title": "节点 Load1/5/15 绝对值",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 },
      "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "short" } },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [
        { "expr": "node_load1{instance=~\"$node.*\"}", "legendFormat": "{{ instance }} load1" },
        { "expr": "node_load5{instance=~\"$node.*\"}", "legendFormat": "{{ instance }} load5" },
        { "expr": "node_load15{instance=~\"$node.*\"}", "legendFormat": "{{ instance }} load15" }
      ]
    },
    {
      "title": "🔍 节点上 Pod CPU Usage 排名(定位元凶)",
      "type": "row",
      "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 },
      "collapsed": false
    },
    {
      "title": "节点 $node 上各 Pod CPU 使用速率 Top 20",
      "description": "按 CPU 使用核数降序排列,找出 CPU 消耗最大的 Pod",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 10, "w": 12, "x": 0, "y": 10 },
      "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 15 }, "unit": "short" } },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"], "sortBy": "max", "sortDesc": true }
      },
      "targets": [{
        "expr": "topk(20, sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{node=~\"$node.*\", container!=\"\", container!=\"POD\"}[5m])))",
        "legendFormat": "{{ namespace }}/{{ pod }}"
      }]
    },
    {
      "title": "节点 $node 上各 Pod CPU Usage 突刺检测",
      "description": "CPU 使用率 2 分钟变化量,正值表示突增,负值表示突降",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 10, "w": 12, "x": 12, "y": 10 },
      "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "short" } },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "min", "last"] }
      },
      "targets": [{
        "expr": "topk(10, sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{node=~\"$node.*\", container!=\"\", container!=\"POD\"}[2m])) - sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{node=~\"$node.*\", container!=\"\", container!=\"POD\"}[2m]) offset 2m))",
        "legendFormat": "Δ {{ namespace }}/{{ pod }}"
      }]
    },
    {
      "title": "⚡ CPU Throttling 分析(限流 Pod 排查)",
      "type": "row",
      "gridPos": { "h": 1, "w": 24, "x": 0, "y": 20 },
      "collapsed": false
    },
    {
      "title": "节点 $node 上各 Pod CPU Throttling 比例 Top 15",
      "description": "被 CFS 节流的调度周期占比,>50% 说明 Pod 被严重限流",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 10, "w": 12, "x": 0, "y": 21 },
      "fieldConfig": {
        "defaults": {
          "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "percent", "min": 0, "max": 100,
          "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 25 }, { "color": "red", "value": 50 }] }
        }
      },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"], "sortBy": "max", "sortDesc": true }
      },
      "targets": [{
        "expr": "topk(15, sum by (namespace, pod) (rate(container_cpu_cfs_throttled_periods_total{node=~\"$node.*\", container!=\"\"}[5m])) / sum by (namespace, pod) (rate(container_cpu_cfs_periods_total{node=~\"$node.*\", container!=\"\"}[5m])) * 100)",
        "legendFormat": "{{ namespace }}/{{ pod }}"
      }]
    },
    {
      "title": "节点 $node 上各 Pod CPU Throttling 时长",
      "description": "每秒被节流的秒数",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 10, "w": 12, "x": 12, "y": 21 },
      "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "s" } },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [{
        "expr": "topk(15, sum by (namespace, pod) (rate(container_cpu_cfs_throttled_seconds_total{node=~\"$node.*\", container!=\"\"}[5m])))",
        "legendFormat": "{{ namespace }}/{{ pod }}"
      }]
    },
    {
      "title": "📊 时间线叠加对比(Load vs Pod CPU 关联)",
      "type": "row",
      "gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 },
      "collapsed": false
    },
    {
      "title": "叠加对比:节点 Load vs Top Pod CPU Usage",
      "description": "将节点 Load 和各 Pod CPU 使用率放在同一时间线上,观察时间相关性",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 12, "w": 24, "x": 0, "y": 32 },
      "fieldConfig": {
        "defaults": { "custom": { "lineWidth": 2, "fillOpacity": 5 }, "unit": "short" },
        "overrides": [{
          "matcher": { "id": "byRegexp", "options": "Load.*" },
          "properties": [
            { "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } },
            { "id": "custom.lineWidth", "value": 3 },
            { "id": "custom.axisPlacement", "value": "right" }
          ]
        }]
      },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [
        { "expr": "node_load1{instance=~\"$node.*\"}", "legendFormat": "Load1 (右轴)" },
        { "expr": "topk(10, sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{node=~\"$node.*\", container!=\"\", container!=\"POD\"}[5m])))", "legendFormat": "CPU: {{ namespace }}/{{ pod }}" }
      ]
    },
    {
      "title": "🔧 辅助诊断:IO Wait 与磁盘写入",
      "type": "row",
      "gridPos": { "h": 1, "w": 24, "x": 0, "y": 44 },
      "collapsed": false
    },
    {
      "title": "节点 $node IO Wait 占比",
      "description": "IO Wait 高时,D 状态进程增多,也会导致 Load 飙升",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 45 },
      "fieldConfig": {
        "defaults": {
          "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 20 }, "unit": "percent",
          "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 30 }] }
        }
      },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [{ "expr": "avg by (instance) (rate(node_cpu_seconds_total{mode=\"iowait\", instance=~\"$node.*\"}[5m])) * 100", "legendFormat": "{{ instance }} iowait%" }]
    },
    {
      "title": "节点 $node 上各 Pod 磁盘写入速率",
      "description": "IO 密集 Pod 可能导致 IO Wait,间接推高 Load",
      "type": "timeseries",
      "datasource": "${datasource}",
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 45 },
      "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "Bps" } },
      "options": {
        "tooltip": { "mode": "multi", "sort": "desc" },
        "legend": { "displayMode": "table", "placement": "right", "calcs": ["max", "mean", "last"] }
      },
      "targets": [{
        "expr": "topk(10, sum by (namespace, pod) (rate(container_fs_writes_bytes_total{node=~\"$node.*\", container!=\"\"}[5m])))",
        "legendFormat": "{{ namespace }}/{{ pod }}"
      }]
    }
  ],
  "schemaVersion": 30,
  "style": "dark",
  "tags": ["kubernetes", "cpu", "load", "troubleshooting"],
  "templating": {
    "list": [
      { "name": "datasource", "type": "datasource", "query": "prometheus", "current": {}, "hide": 0 },
      { "name": "node", "type": "query", "datasource": "${datasource}", "query": "label_values(node_load1, instance)", "regex": "/(.*?)(:\\d+)?$/", "sort": 1, "refresh": 2, "current": {}, "hide": 0, "includeAll": false, "multi": false }
    ]
  },
  "time": { "from": "now-1h", "to": "now" },
  "timepicker": {},
  "timezone": "",
  "title": "K8s Node → Pod CPU Load 关联分析",
  "uid": "pod-cpu-load-analysis",
  "version": 1
}

7.3 Dashboard 使用流程

text
Step 1: 打开 Dashboard,在顶部下拉框选择出现 Load 突刺的节点


Step 2: 查看 Row 1 "节点 Load 总览"
        ├── Load1/CPU核数 > 1.0 → 该节点确实过载
        └── 确认突刺发生的时间点(比如 14:30)


Step 3: 查看 Row 2 "Pod CPU Usage 排名"
        ├── 在突刺时间点,哪个 Pod 的 CPU 使用率同步飙升?
        └── "突刺检测"面板直接显示 CPU 变化量最大的 Pod


Step 4: 查看 Row 3 "CPU Throttling 分析"
        ├── 哪些 Pod 被严重节流?说明它的 Limit 设置过低
        └── 被节流的 Pod 实际需要的 CPU 比 Limit 更高


Step 5: 查看 Row 4 "叠加对比"
        ├── 将节点 Load 和 Pod CPU 放在同一时间线上
        └── 时间线吻合 → 确认该 Pod 就是"元凶"


Step 6: 如果 CPU Usage 不高但 Load 仍然高
        ├── 查看 Row 5 "IO Wait" → IO 密集型导致 D 状态进程堆积
        └── 查看磁盘写入面板 → 找到 IO 大户 Pod

八、进阶方案:自定义 DaemonSet 采集 Pod 级进程数

当 cAdvisor 指标不足以定位问题(比如线程数暴增导致 Load 高),可以部署自定义 DaemonSet 采集更细粒度的数据。

8.1 采集器原理

text
DaemonSet Pod(每节点一个)

    ├── 读取 /proc/loadavg → 节点 Load
    ├── 遍历 /proc/[pid]/cgroup → 映射 PID 到容器/Pod
    ├── 统计每 Pod 的 R/D 状态进程数 → Pod 级 Load 贡献
    └── 暴露 /metrics 端口 → Prometheus 抓取

8.2 采集脚本(Python 示例)

python
#!/usr/bin/env python3
"""pod_load_collector.py - 采集每个 Pod 的进程状态数,近似 Pod 级 Load 贡献"""

import os
import re
import glob
from http.server import HTTPServer, BaseHTTPRequestHandler
from collections import defaultdict

CONTAINER_ID_RE = re.compile(r'[a-f0-9]{64}')

def get_container_id_from_cgroup(pid):
    """从进程的 cgroup 信息中提取 container ID"""
    cgroup_path = f"/proc/{pid}/cgroup"
    try:
        with open(cgroup_path) as f:
            for line in f:
                match = CONTAINER_ID_RE.search(line)
                if match:
                    return match.group(0)
    except (FileNotFoundError, PermissionError):
        pass
    return None

def get_process_state(pid):
    """获取进程状态(R=运行, D=不可中断等待, S=睡眠...)"""
    stat_path = f"/proc/{pid}/stat"
    try:
        with open(stat_path) as f:
            parts = f.read().split()
            return parts[2]
    except (FileNotFoundError, PermissionError, IndexError):
        return None

def collect_pod_load():
    """收集每个容器的 R+D 状态进程数"""
    container_load = defaultdict(lambda: {"R": 0, "D": 0, "S": 0, "total": 0})
    for pid_dir in glob.glob("/proc/[0-9]*"):
        pid = pid_dir.split("/")[-1]
        container_id = get_container_id_from_cgroup(pid)
        if not container_id:
            continue
        state = get_process_state(pid)
        if not state:
            continue
        container_load[container_id]["total"] += 1
        if state in ("R", "D"):
            container_load[container_id][state] += 1
    return container_load

def format_metrics(container_load):
    """格式化为 Prometheus metrics"""
    lines = ["# HELP pod_runnable_processes R state processes per container",
             "# TYPE pod_runnable_processes gauge"]
    for cid, c in container_load.items():
        lines.append(f'pod_runnable_processes{{container_id="{cid[:12]}"}} {c["R"]}')
    lines += ["# HELP pod_d_state_processes D state processes per container",
              "# TYPE pod_d_state_processes gauge"]
    for cid, c in container_load.items():
        lines.append(f'pod_d_state_processes{{container_id="{cid[:12]}"}} {c["D"]}')
    lines += ["# HELP pod_total_processes Total processes per container",
              "# TYPE pod_total_processes gauge"]
    for cid, c in container_load.items():
        lines.append(f'pod_total_processes{{container_id="{cid[:12]}"}} {c["total"]}')
    return "\n".join(lines) + "\n"

class MetricsHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/metrics":
            load = collect_pod_load()
            metrics = format_metrics(load)
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(metrics.encode())
        else:
            self.send_response(404)
            self.end_headers()
    def log_message(self, format, *args):
        pass

if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", 9110), MetricsHandler)
    print("Pod Load Collector listening on :9110")
    server.serve_forever()

8.3 DaemonSet 部署清单

yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: pod-load-collector
  namespace: monitoring
  labels:
    app: pod-load-collector
spec:
  selector:
    matchLabels:
      app: pod-load-collector
  template:
    metadata:
      labels:
        app: pod-load-collector
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9110"
        prometheus.io/path: "/metrics"
    spec:
      hostPID: true           # 关键:需要看到宿主机所有进程
      containers:
        - name: collector
          image: python:3.11-slim
          command: ["python3", "/scripts/pod_load_collector.py"]
          ports:
            - containerPort: 9110
              name: metrics
          volumeMounts:
            - name: scripts
              mountPath: /scripts
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
          securityContext:
            readOnlyRootFilesystem: true
      volumes:
        - name: scripts
          configMap:
            name: pod-load-collector-script
---
apiVersion: v1
kind: Service
metadata:
  name: pod-load-collector
  namespace: monitoring
  labels:
    app: pod-load-collector
spec:
  selector:
    app: pod-load-collector
  ports:
    - port: 9110
      targetPort: 9110
      name: metrics
  clusterIP: None              # Headless Service,让 Prometheus 发现所有 Pod IP
---
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: pod-load-collector
  namespace: monitoring
  labels:
    prometheus: kube-prometheus
spec:
  selector:
    matchLabels:
      app: pod-load-collector
  podMetricsEndpoints:
    - port: metrics
      interval: 15s

8.4 使用自定义指标关联 Pod

promql
# 查看某节点上哪些容器的 R 状态进程数最多
topk(10, pod_runnable_processes)

# 关联 container_id 与 Pod 名称
# 通过 kube_pod_container_info 做 join
topk(10,
  pod_runnable_processes
  * on(container_id) group_left(namespace, pod)
  label_replace(
    kube_pod_container_info,
    "container_id",
    "$1",
    "container_image_id",
    ".*://(.*)"
  )
)

九、方案对比与选型建议

维度方案 A:纯 Prometheus + Grafana方案 B:自定义 DaemonSet 采集方案 C:eBPF (Tetragon)
部署复杂度⭐ 最低(如已有监控栈则零部署)⭐⭐ 中等(需部署 DaemonSet)⭐⭐⭐ 较高(需内核 ≥4.15)
CPU Usage 精度✅ cAdvisor 原生支持❌ 不采集✅ 内核级精确
Pod 级 Load 归因⚠️ 间接推断(时间线关联)✅ 直接采集 R/D 进程数✅ 内核级精确追踪
线程数暴增检测❌ 不支持✅ 支持✅ 支持
IO Wait 归因到 Pod⚠️ 仅磁盘 IO 字节数❌ 不支持✅ 精确到进程的 IO 追踪
资源开销低(cAdvisor 已内置)中(每节点 ~50m CPU, 64Mi)中(eBPF 程序本身开销小)
推荐场景日常运维、大多数场景够用需精确到 Pod 级 Load 贡献需深度排查内核级性能问题

推荐:先用方案 A(纯 Prometheus + Grafana),90% 的 CPU 突刺问题都会定位。如果仍有疑难杂症(如线程泄露、IO Wait 归因),再叠加方案 B方案 C

十、排障 Checklist

当收到节点 CPU Load 突刺告警时,按以下步骤排查:

text
□ Step 1: 确认告警节点
  └── Grafana → "节点 Load 总览" → 选择异常节点

□ Step 2: 判断 Load 类型
  ├── Load > CPU 核数 → CPU 过载(继续 Step 3)
  ├── Load 高但 CPU Usage 不高 → IO Wait 问题(跳 Step 5)
  └── Load 高但无明显 Pod 突增 → 线程泄露嫌疑(跳 Step 6)

□ Step 3: 找出 CPU 使用突刺的 Pod
  └── "Pod CPU Usage 排名" + "突刺检测" → 记录 Top Pod

□ Step 4: 检查 CPU Throttling
  ├── 该 Pod Throttling > 50% → 需要调高 CPU Limit
  └── 该 Pod 无 Throttling 但 Usage 飙升 → 需排查应用逻辑(GC?死循环?)

□ Step 5: 检查 IO Wait
  └── "IO Wait 占比" + "Pod 磁盘写入速率" → 找出 IO 大户 Pod

□ Step 6: 检查线程/进程数(需方案 B 自定义采集器)
  └── pod_total_processes 排序 → 找出线程泄露的 Pod

□ Step 7: 处置
  ├── 临时:kubectl top pods -n <ns> --sort-by=cpu 确认
  ├── 短期:调整 CPU Request/Limit、HPA 配置
  └── 长期:优化应用代码、增加资源预算、完善告警

附:常用 kubectl 命令

bash
# 实时查看节点上各 Pod 的 CPU 使用
kubectl top pods -A --sort-by=cpu | head -20

# 查看某 Pod 的资源配置
kubectl get pod <pod-name> -n <ns> -o jsonpath='{.spec.containers[*].resources}'

# 查看节点上所有 Pod 列表
kubectl get pods -A -o wide --field-selector spec.nodeName=<node-name>

# 查看某 Pod 的 CPU Throttling(需要 metrics-server v0.6+)
kubectl top pod <pod-name> -n <ns> --containers