【集群】云原生批调度实战:Volcano 指标采集与可视化

【集群】云原生批调度实战:Volcano 指标采集与可视化

Fre5h1nd Lv6

本系列《云原生批调度实战:Volcano 监控与性能测试》计划分为以下几篇,点击查看其它内容。

  1. 云原生批调度实战:调度器测试与监控工具 kube-scheduling-perf
  2. 云原生批调度实战:调度器测试与监控工具 kube-scheduling-perf 实操注意事项说明
  3. 云原生批调度实战:调度器测试监控结果
  4. 云原生批调度实战:本地环境测试结果与视频对比分析
  5. 监控与测试环境解析:测试流程拆解篇
  6. 监控与测试环境解析:指标采集与可视化篇
  7. 监控与测试环境解析:自定义镜像性能回归测试
  8. 监控与测试环境解析:数据收集方法深度解析与Prometheus Histogram误差问题
  9. 云原生批调度实战:Volcano调度器enqueue功能禁用与性能测试
  10. 云原生批调度实战:Volcano Pod创建数量不足问题排查与Webhook超时修复
  11. 云原生批调度实战:Volcano版本修改与性能测试优化
  12. 云原生批调度实战:Volcano Webhook禁用与性能瓶颈分析

上一篇我们从 Makefile → Kind → 测试代码 串起了一次最小性能测试的全链路。本篇将回答另一个常见问题:

TestBatchJob 跑完后,Grafana 面板上的 CREATED / SCHEDULED / RUNNING 曲线是怎么来的?」

下图给出了核心组件与数据流,阅读完本文,希望能够帮你快速实现:

  1. 理解 审计日志 → Exporter → Prometheus+Grafana → 截图归档 的端到端链路;
  2. 自定义审计策略 & 面板查询 & 截图归档。
graph LR;
  subgraph Control-Plane 审计日志
    APIServer["Kube-APIServer(开启审计)"] -->|/var/log/kubernetes/kube-apiserver-audit.log| NodeDisk[(control-plane 节点磁盘)]
  end

  NodeDisk --> Exporter["Audit-Exporter(Deployment)"]
  Exporter -->|/metrics| Prometheus((Prometheus))
  Prometheus --> Grafana[(Grafana Dashboard)]
  Grafana --> Script[save-result-images.sh]

1️⃣ 审计日志:audit-policy.yaml 决定记录什么

对应前文流程图中的 Control-Plane 审计日志 部分,在 Kubernetes 中,每个请求在不同执行阶段都会生成审计事件;这些审计事件会根据特定策略被预处理并写入后端。[3]

在此过程中,Kubernetes 审计子系统需要一份 Policy 文件来声明规则(指明需记录的事件范围)。而本项目根目录的 audit-policy.yaml 中就声明了一套规则,重点拦截了衡量调度器吞吐量的关键对象 Pod / Job 的 CRUD

audit-policy.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: audit.k8s.io/v1
kind: Policy
omitManagedFields: True
omitStages:
- RequestReceived
- ResponseStarted
rules:
- level: RequestResponse
resources:
- group: ""
resources:
- pods
- pods/binding
- pods/status
- group: batch
resources:
- jobs
- jobs/status
- group: batch.volcano.sh
resources:
- jobs
- jobs/status
verbs:
- create
- patch
- update
- delete
- level: Metadata
  • omitStages 指明每个请求可无须记录相关的阶段(stage)。Kubernetes 中已定义的阶段有:
    • RequestReceived - 此阶段对应审计处理器接收到请求后, 并且在委托给其余处理器之前生成的事件。
    • ResponseStarted - 在响应消息的头部发送后,响应消息体发送前生成的事件。 只有长时间运行的请求(例如 watch)才会生成这个阶段。
    • ResponseComplete - 当响应消息体完成并且没有更多数据需要传输的时候。
    • Panic - 当 panic 发生时生成。
  • level: RequestResponse 既保留请求头,也包含响应体,方便后续解析出 Result 及耗时
    • verbs 指明此规则所适用的操作(verb)列表。将 CREATE / PATCH / UPDATE / DELETE 四类操作一次性覆盖;
    • resources 指明此规则所适用的资源类别列表,包含 batchbatch.volcano.sh/jobs 等 CRD,兼顾不同调度器对象。字段 group 给出包含资源的 API 组的名称,空字符串代表 core API 组。
  • level: Metadata 则仅记录请求的元数据(请求的用户、时间戳、资源、动词等等), 但是不记录请求或者响应的消息体。
    • resources 为空列表意味着适用于 API 组中的所有资源类别。

通常情况下,可以使用 --audit-policy-file 标志将包含策略的文件传递给 kube-apiserver

▶️ FAQ:策略细节常见疑问

💡 以下内容专门回应在阅读源码时最常见的 3 个疑惑。

level: Metadatalevel: RequestResponse 有何区别?为何都要保留?

  • 作用域不同:
    • RequestResponse 规则匹配我们关心的调度相关资源(pods / jobs / jobs.batch.volcano.sh 等),并且显式列举了 verbs=create|patch|update|delete。它会把 请求头 + 响应体 全量落盘,方便后续 Exporter 解析出 Result (Success/Failure) 与延迟直方图
    • Metadata 规则的 resources: [] 表示「兜底规则」——凡是不在前一条命中列表内的 任何 资源,统一只记录元数据(谁、何时、做了什么),不包含请求/响应体。这样既能保留审计合规性,又避免为海量无关对象写大文件。
  • 优先级:Kubernetes 会按照 YAML 中的 先后顺序 匹配规则,一旦命中即停止继续匹配。因此本项目先写精确匹配、再写兜底规则,二者不会冲突。
  • 同时编写两条规则的目的:平衡指标精度与日志体积RequestResponse 为核心对象提供高粒度延迟直方图与成功率计算;Metadata 兜底满足审计留痕合规,又避免为成百上千个与调度无关的对象写入冗余响应体,从而显著降低磁盘占用与解析成本。

② 为什么 omitStages 要排除 RequestReceivedResponseStarted?最终会记录哪些 Stage?

  • 背景:一次 API 请求最多可生成四个 Stage 事件(RequestReceivedResponseStartedResponseCompletePanic)。其中 RequestReceivedResponseStarted 体量大且价值有限
    • RequestReceived 只表明「请求到达了 APIServer」,但拿不到任何时长信息;
    • ResponseStarted 仅对 长连接 watch 场景才会生成,对我们的批量 CRUD 测试用例几乎恒为空;
  • 因此在策略里把这两阶段排除,既减少日志体积,也避免 Exporter 做无意义解析。
  • 与规则无冲突:omitStages 作用于 全局,告诉 APIServer 在生成审计事件时忽略指定阶段;后面的 rules 只决定「对哪些请求生成事件以及生成到什么 level」。二者工作维度不同,不会互相覆盖。
  • 在本项目的批量 Job / Pod 测试中,最终实际落盘的 Stage 主要是:
    • ResponseComplete — 绝大多数正常请求;
    • Panic — 只有当 APIServer panic 才会出现(理论上极少)。
  • 如何拿到「创建 / 调度 / 运行」等关键时间点?Exporter 仅需关注 stage="ResponseComplete" 的事件:
    • 创建时间:匹配 verb=createresource=pods|jobs 的完成时间戳;
    • 调度时间:匹配 resource=pods/binding 的完成时间戳(kube-scheduler 向 APIServer 发起 bind 请求);
    • 运行时间:匹配 resource=pods/statusverb=updatestatus.phase=Running 的完成时间戳;
      Exporter 在内存中以同名 Pod UID 关联多条事件,计算时间差即可,无需 RequestReceived/Started 阶段即可还原完整链路。
    • Panic 含义:当 APIServer 在处理请求过程中发生运行时崩溃并捕获到 panic 时才会生成,用于事后问题排查,正常测试流程极罕见。

audit-policy.yaml 是如何交给 Kind 中的 kube-apiserver 的?

  • 每个调度器对应的 Kind 集群(位于 clusters/<scheduler>/kind.yaml)都做了如下三种操作:
    1. extraMounts 把根目录下的 audit-policy.yaml 挂载到控制平面节点的 /etc/kubernetes/policies/audit-policy.yaml
    2. apiServer.extraVolumes 定义名为 audit-policies 的 HostPath 卷,并将其挂载到同一路径,确保文件在 Pod 内可读;
    3. apiServer.extraArgs 增加
      1
      2
      3
      audit-policy-file: /etc/kubernetes/policies/audit-policy.yaml
      audit-log-path: /var/log/kubernetes/kube-apiserver-audit.<scheduler>.log
      audit-log-maxsize: "10240"
      这样 APIServer 一启动就按照我们自定义的策略把审计事件写入宿主机 /var/log/kubernetes/,后续再被 Exporter Tail。

2️⃣ Exporter:kube-apiserver-audit-exporter 把日志变成指标

前文 Policy 决定了「记录什么」,Exporter 则决定了「怎么提炼指标」。
部署清单位于:base/kube-apiserver-audit-exporter/kube-apiserver-audit-exporter/deployment.yaml

base/kube-apiserver-audit-exporter/kube-apiserver-audit-exporter/deployment.yaml:24:41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
containers:
- args:
- --audit-log-path
- /var/log/kubernetes/kube-apiserver-audit.log
image: kind-registry:5000/ghcr.io/wzshiming/kube-apiserver-audit-exporter/kube-apiserver-audit-exporter:v0.0.25
imagePullPolicy: IfNotPresent
name: exporter
ports:
- containerPort: 8080
name: http
protocol: TCP
resources:
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- mountPath: /var/log/kubernetes
name: audit-logs
...

关键参数说明:

字段含义示例值
--audit-log-path审计日志所在宿主机路径/var/log/kubernetes/kube-apiserver-audit.log
image可独立升级的 Exporter 镜像…/kube-apiserver-audit-exporter:v0.0.25
VolumeMount将宿主机日志目录挂载进 PodmountPath: /var/log/kubernetes

📌 组件何时被部署?—— Makefile 触发点

在本仓库最常用的入口 make default 会连续执行多轮 serial-test。理解一次 serial-test 的执行序列即可明白监控组件的真实部署时机:

步骤触发目标关键动作
1prepare-<scheduler>make up-<scheduler> 创建 单个调度器集群,但此时 没有 监控栈
2start-<scheduler>运行性能测试 (TestBatchJob 等) 并 写入 audit-log
3end-<scheduler>make down-<scheduler> 销毁该集群,日志仍留在宿主机 /var/log/kubernetes/
⬇(循环)(依次换下一个调度器)
4prepare-overviewmake up-overview 创建 独立的 overview 集群
5start-overviewclusters/overview/Makefile:start-export 部署 Exporter + PromStack,并把 所有 kube-apiserver-audit.*.log HostPath 挂载到 Pod
6save-result$(RESULT_RECENT_DURATION_SECONDS) 秒等待指标就绪→执行 hack/save-result-images.sh 截图
7end-overview销毁 overview 集群,聚合循环结束

也就是说:Export­er 和 Prometheus 直到 所有 调度器测试跑完后才被一次性拉起,随后一次性重放/解析先前留下的多份 audit-log。

Exporter 会 tail 文件并实时解析,输出如下两类 Prometheus 指标(简化):

1
2
3
4
5
6
7
# HELP kube_audit_event_total Total number of audit events
# TYPE kube_audit_event_total counter
kube_audit_event_total{verb="create",resource="pods",status="Success"} 1280

# HELP kube_audit_event_latency_seconds Histogram of audit event latency
# TYPE kube_audit_event_latency_seconds histogram
kube_audit_event_latency_seconds_bucket{resource="pods",le="0.1"} 240

其中 status="Success" 字段让我们能够在 Grafana 中分别绘制 CREATED / SCHEDULED / RUNNING 三条曲线。

🔍 内部实现:Exporter 如何 tail + 解析?

该部分比较复杂,涉及另一个项目。简单理解后,将该部分分为以下三步:

  • 跟踪文件:Exporter 使用 Go 语言实现,入口位于 <base/kube-apiserver-audit-exporter>,源仓库位于https://github.com/wzshiming/kube-apiserver-audit-exporter。其核心依赖 tail(或 OS inotify)持续读取宿主机 /var/log/kubernetes/…audit.log
  • JSON 解析:每行审计日志都是合法 JSON,Exporter 利用 encoding/json 反序列化为 auditinternal.Event 结构体,随后按 verb / resource / stage / status 维度进行 map 聚合;
  • 指标暴露:聚合结果通过 prometheus/client_golang 转为 counterhistogram 两类 kube_audit_* 指标;

若要增加更多指标(如自定义 label、增加 summary 等):

  • 定位代码:仓库中路径 exporter/metrics.go 下可见:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var (
    apiRequests = prometheus.NewCounterVec(...)
    podSchedulingLatency = prometheus.NewHistogramVec(...)
    )
    func (p *Exporter) updateMetrics(clusterLabel string, event auditv1.Event) {
    // ... 根据 verb/resource 填充指标 ...
    apiRequests.WithLabelValues(...).Inc()
    podSchedulingLatency.WithLabelValues(...).Observe(latency)
    }
  • 扩展步骤
    1. 复制:复制上述变量块,替换 Namekube_audit_pod_latency_seconds(示例),同时调整 BucketsHelp
    2. 修改:在 updateMetrics 中增加条件:
      1
      2
      3
      if evt.ObjectRef.Resource == "pods" {
      podLatency.WithLabelValues(evt.Verb, evt.Stage).Observe(cost)
      }
    3. 注册:确保在 init()NewCollector()registry.MustRegister(podLatency)
    4. 换镜像:docker build -t <registry>/audit-exporter:dev . && docker push …,然后在 base/kube-apiserver-audit-exporter/.../deployment.yaml 更新 imagekubectl apply -k

完整示例可参考项目 exporter/metrics.go 源码


3️⃣ Prometheus 抓取:Kustomize 一条龙

base/kube-prometheus-stack 目录通过 Kustomize 把 Exporter、Prometheus Operator 与多个 ServiceMonitor 组合在一起,无需额外手动配置抓取目标。

  • Prometheus 会自动发现 Exporter 的 metrics 端口;
  • Grafana 面板 JSON audit-exporter.json 已预置在同目录,标签切片(Scheduler 类型、Namespace、Verb)均可动态选择。

若要自定义阈值或颜色,只需 kubectl edit cm grafana-dashboards 后刷新浏览器即可即时生效。

其中用到了 Kustomize 工具,较为复杂,在此仅简单介绍。

✨ Kustomize 简介

Kustomize 是 Kubernetes 官方提供的 原生资源定制工具,核心理念是“声明式 Patch 与组合”。相比 helm,它无需模板语言,也不引入额外 CRD:

  • 基础资源(Base):每个目录下的 kustomization.yaml 列出若干 resources,可按文件或目录引用;
  • 叠加层(Overlay):上层可以通过 patches, images, replicas 等声明式字段覆写或追加配置;
  • 生成器configMapGenerator, secretGenerator 快速为应用生成引用。

在本项目中:

1
2
3
4
5
base/kube-prometheus-stack/           # 监控基础组件 Base
├── crd/ # CRD 资源
├── grafana/ # Dashboard JSON 及账号
├── ...
└── kustomization.yaml # 声明所有组件

clusters/overview/Makefile 里的 kubectl kustomize <dir> | hack/local-registry-with-load-images.sh 两步做了:

  1. kubectl kustomize渲染:把以上 Base + Patch 解析成纯 YAML 清单;
  2. local-registry-with-load-images.sh镜像处理:重写鏡像地址到本地 Kind Registry 并预先 docker pull
  3. kubectl create -k应用:批量创建 Exporter、Prometheus Operator、Alertmanager、ServiceMonitor 等所有资源,一次到位。

因此我们才能“一键 make”拿到完整的监控栈。


4️⃣ 截图归档:save-result-images.sh 归档面板截图

运行 make save-result 后,hack/save-result-images.sh 会在本地循环调用 Grafana render API,按面板 ID 生成 output/panel-*.png

hack/save-result-images.sh
1
2
3
4
5
6
7
8
9
10
11
RECENT_DURATION=${RECENT_DURATION:-5min}

FROM=$(date -u -Iseconds -d "- ${RECENT_DURATION}" | sed 's/+00:00/.000Z/')
TO=$(date -u -Iseconds | sed 's/+00:00/.000Z/')

OUTPUT="${ROOT_DIR}/output"
mkdir -p "${OUTPUT}"

for i in {1..8}; do
wget -O "${OUTPUT}/panel-${i}.png" "http://127.0.0.1:8080/grafana/render/d-solo/perf?var-rate_interval=5s&orgId=1&from=${FROM}&to=${TO}&timezone=browser&var-datasource=prometheus&var-resource=\$__all&var-user=\$__all&var-verb=create&var-verb=delete&var-verb=patch&var-verb=update&var-namespace=default&var-cluster=\$__all&refresh=5s&theme=dark&panelId=panel-${i}&__feature.dashboardSceneSolo&width=900&height=500&scale=10"
done
  • RECENT_DURATION 环境变量控制 截图时间窗口(默认 5 分钟);
  • FROM/TO 时间戳使用 ISO-8601 UTC 毫秒格式,避免时区混淆;
  • panelId 与面板 JSON 中的 id 一一对应,可根据需要扩展。

示例输出目录结构:

1
2
3
4
5
output/
├── panel-1.png # CREATED 速率
├── panel-2.png # SCHEDULED 速率
├── panel-3.png # RUNNING 速率
└── …

⏱️ 为什么三套调度器曲线对齐到同一起始时间?

serial-test 模式下,Exporter 直到第 4 步才启动,它会从头开始顺序扫描所有 audit-log。Prometheus 采集时将「第一次 scrape 该指标的时刻」视为样本时间戳,而不是事件发生时间。因此:

  • 当 Exporter 第一次读取 三份 日志文件时(约 T0),所有指标都会带上 T0 的统一时间戳;
  • 读取完第一份文件后继续第二、第三份——对于 Prometheus 来说也仍是 “T0~T0+Δ” 的时间窗口;

结果就是:Grafana 图上三条曲线似乎“同一时刻起跑”。它们并非并发,而是 日志回放造成的时间折叠 —— 先跑的调度器其实更早完成,但其事件被延后才被采集。

如果希望曲线按真实事件时间展开,可以:

  1. 修改 Exporter,让它把 evt.StageTimestamp 用作 Prometheus histogramObserveWithTimestamp
  2. 或者在测试流程中提前启动 overview 集群,使 Exporter 按实时模式持续采集。

如果想亲眼查看 audit-log,有两种方法:

  1. 直接读宿主机文件(Kind 节点实际上是 Docker 容器):
    1
    2
    docker exec kueue-control-plane \
    cat /var/log/kubernetes/kube-apiserver-audit.kueue.log | head
    三个文件名称分别为 kube-apiserver-audit.{kueue|volcano|yunikorn}.log
  2. 查看 Exporter 容器日志(overview 集群):
    1
    kubectl -n monitoring logs deploy/kube-apiserver-audit-exporter | head
    启动时它会打印 starting from offset=0 file=...,表明正在从头重放。

日志文件很大,可用 grep '"verb":"create"' 等命令过滤感兴趣事件,字段含义详见 Kubernetes 官方审计文档。


5️⃣ 本地验证:三步走

  1. 最小规模跑一次
1
2
make prepare-volcano start-volcano end-volcano \
NODES_SIZE=1 JOBS_SIZE_PER_QUEUE=1 PODS_SIZE_PER_JOB=1
  1. 打开 Grafana

浏览器访问 http://127.0.0.1:8080/grafana,Dashboards → perf,即可看到实时曲线。

  1. 查看截图

测试结束后,output/ 将出现自动截好的图片,确认时间轴与曲线一致。



  • 希望这篇博客对你有帮助!如果你有任何问题或需要进一步的帮助,请随时提问。
  • 如果你喜欢这篇文章,欢迎动动小手给我一个follow或star。

🗺参考文献

[1] Github - kube-scheduling-perf

[2] A Comparative Analysis of Kueue, Volcano, and YuniKorn - Wei Huang, Apple & Shiming Zhang, DaoCloud

[3] Kubernetes官方文档 - 审计

[4] Kubernetes官方文档 - 审计Policy配置参考

  • 标题: 【集群】云原生批调度实战:Volcano 指标采集与可视化
  • 作者: Fre5h1nd
  • 创建于 : 2025-08-07 22:59:25
  • 更新于 : 2025-08-21 19:57:58
  • 链接: https://freshwlnd.github.io/2025/08/07/k8s/k8s-scheduler-performance-volcano-metrics/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论