主题
K8s Pod CPU Load 突刺排查实战
K8s 集群中部分节点出现 CPU Load 突刺,但不知道是节点上哪些 Pod 导致的。这是一个非常典型的运维痛点:节点级 node_load1 告警触发了,可登到机器上 top 一看,进程列表眼花缭乱,到底对应哪个 Pod?本文给出从指标采集 → 关联分析 → Grafana 可视化的完整方案,帮你做到"节点 Load 一突刺,立刻定位到 Pod"。
适用对象:运行 Kubernetes(含 Kubeadm / RKE2 / K3s 等发行版)的集群,监控栈以 Prometheus + Grafana 为准。
读完本文你将得到
- 理解 CPU Load 与 CPU 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 突发吃满 CPU | Usage 突增,无 Throttling | container_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 monitoring3.2 确认 cAdvisor 指标可抓取
kube-prometheus-stack 默认已创建 cAdvisor 的 ServiceMonitor。验证方式:
bash
# 在 Prometheus UI 中检查 target 状态
# 访问 http://<prometheus-url>/targets
# 搜索 "cadvisor",确认所有节点状态为 UP
# 或者用命令行验证
kubectl get servicemonitor -n monitoring | grep cadvisor3.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 5m4.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!=""}
) * 1004.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])
) * 1004.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 导入方式
- 登录 Grafana → Dashboards → Import
- 将下方 JSON 粘贴到 "Import via panel text" 输入框
- 选择 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: 15s8.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