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

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

Fre5h1nd Lv6

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

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

上一篇我们从 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 下可见:以apiRequests(api_requests_total)、podSchedulingLatency(pod_scheduling_latency_seconds)为例
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    // Metric definitions
    var (
    registry = prometheus.NewRegistry()

    apiRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
    Name: "api_requests_total",
    Help: "Total number of API requests to the scheduler",
    }, []string{"cluster", "namespace", "user", "verb", "resource", "code"})
    // 核心为:apiRequests = prometheus.NewCounterVec(...)

    ...

    podSchedulingLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
    Name: "pod_scheduling_latency_seconds",
    Help: "Duration from pod creation to scheduled on node in seconds",
    Buckets: prometheus.ExponentialBuckets(0.001, 2, 20),
    }, []string{"cluster", "namespace", "user"})
    // 核心为:batchJobCompleteLatency = prometheus.NewCounterVec(...)

    ...

    )

    func init() {
    registry.MustRegister(
    apiRequests,
    podSchedulingLatency,
    ...
    )
    }

    // updateMetrics processes audit event and updates metrics
    func (p *Exporter) updateMetrics(clusterLabel string, event auditv1.Event) {
    // ... 根据需求,自定义规则将 verb/resource 填充指标 ...
    if event.Stage == auditv1.StageResponseComplete {
    labels := []string{
    clusterLabel,
    ns,
    extractUserAgent(event.UserAgent),
    event.Verb,
    extractResourceName(event),
    strconv.Itoa(int(event.ResponseStatus.Code)),
    }
    apiRequests.WithLabelValues(labels...).Inc()
    // 核心为:apiRequests.WithLabelValues(labels...).Inc()
    }
    ...
    if event.ObjectRef != nil {
    switch event.ObjectRef.Resource {
    case "pods":
    if event.ObjectRef.Subresource == "binding" && event.Verb == "create" {
    target := buildTarget(event.ObjectRef)
    createTime, exists := p.podCreationTimes[target]
    if !exists {
    // Kueue's audit events may create pod/binding events before pod creation events
    user := extractUserAgent(event.UserAgent)
    podSchedulingLatency.WithLabelValues(
    clusterLabel,
    ns,
    user,
    ).Observe(0)
    // 核心为:podSchedulingLatency.WithLabelValues(...).Observe()
    p.podCreationTimes[target] = nil
    return
    }

    if createTime == nil {
    return
    }
    latency := event.StageTimestamp.Sub(*createTime).Seconds()

    user := extractUserAgent(event.UserAgent)
    podSchedulingLatency.WithLabelValues(
    clusterLabel,
    ns,
    user,
    ).Observe(latency)
    // 核心为:podSchedulingLatency.WithLabelValues(...).Observe()
    p.podCreationTimes[target] = nil

    }
    ...
    }
    }
    }
  • 扩展步骤
    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 官方审计文档。

📊 Panel 一览速查表

面板 IDGrafana 标题输出文件主要查询典型用途
panel-1Pod Scheduling Latency Group By UserAgentpanel-1.pnghistogram_quantile(0.99, pod_scheduling_latency_seconds_bucket) 等分位线调度延迟长尾监控
panel-2Total API Calls Group By (UserAgent, Verb, Resource)panel-2.pngapi_requests_total 累积全维度 API 调用计数
panel-3API Calls Rate Group By (UserAgent, Verb, Resource)panel-3.pngrate(api_requests_total[$rate_interval])API 吞吐趋势(含资源/动词维度)
panel-4BatchJob Completion Latency Group By UserAgentpanel-4.pnghistogram_quantile(... batchjob_completion_latency_seconds_bucket)关注 Job 完成延迟
panel-5Total Pod Scheduled Group By UserAgentpanel-5.pngsum(pod_scheduling_latency_seconds_count) vs sum(api_requests_total{verb="create",resource="pods"})对比已调度与已创建 Pod 总量
panel-6Total BatchJob Completed Group By UserAgentpanel-6.pngsum(batchjob_completion_latency_seconds_count)Job 完成总量统计
panel-7Total API Calls Group By UserAgentpanel-7.pngsum(api_requests_total) by (cluster,user)按 UserAgent 维度累计 API 调用
panel-8API Calls Rate Group By UserAgentpanel-8.pngrate(api_requests_total[$rate_interval])按 UserAgent 维度 API 吞吐

以上所有查询皆可在 base/kube-prometheus-stack/audit-exporter.json 中找到对应 panel.idexpr 字段。

🏷️ CREATED / SCHEDULED 指标与 Panel-5 解读

Grafana 的 panel-5.pngTotal Pod Scheduled Group By UserAgent)同时叠加了 累计已调度累计已创建 两条时间序列,用来快速判断“调度器吞吐量是否跟得上工作负载产生速度”。

两条序列的 PromQL 如下:

1
2
3
4
5
# 已调度总量(Series-A)
sum(pod_scheduling_latency_seconds_count{cluster=~"$cluster",namespace=~"$namespace",user=~"$user"}) by (cluster,user)

# 已创建总量(Series-B)
sum(api_requests_total{cluster=~"$cluster",namespace=~"$namespace",user=~"$user",resource="pods",verb="create"}) by (cluster,user)

💡 资源类型

  • 两条序列均针对 Pod 资源:
    • Series-B(CREATED) 捕获 verb=create, resource=pods 的审计事件;
    • Series-A(SCHEDULED) 统计 pod_scheduling_latency_seconds_count 直方图计数,同样基于 Pod UID 聚合。
  • Volcano 中的 PodGroup 仅在 Gang 场景下辅助调度,不会出现在上述指标中,因其创建频次远低于 Pod,且非调度器吞吐主瓶颈。

ℹ️ 为什么 Panel-5 采用 pod_scheduling_latency_seconds_count?可以改用 api_requests_total 吗?

  1. 数据源差异
    • api_requests_total{verb="create",resource="pods/binding"} 也能反映调度动作,但 Exporter 默认并未将 pods/binding 事件打入此 Counter,而是交由 pod_scheduling_latency_seconds Histogram 统一处理(具体实现可见本文“🔍 内部实现:Exporter 如何 tail + 解析?”章节代码示例),以便同时统计累积延迟。
    • Histogram 自带 _count 系列,天然表示 成功调度次数;其 Bucket 仍能计算 P99 等延迟 —— 一举两得。
  2. 统计精度
    • Exporter 对同一 Pod 仅在 首次绑定成功Observe 一次,所以 _count 与实际调度 Pod 数量一一对应,不会多计。
    • 若直接使用 api_requests_total 方案,需要确保:
      1. Exporter 也把 pods/binding 计入 Counter;
      2. Retry 或失败重试场景会导致多计,需要额外 status="Success" 过滤。
  3. 替代方案
    • 若你更习惯 Counter,可在 metrics.go 中追加:
      1
      2
      podBindRequests := prometheus.NewCounterVec(... name="pod_bind_requests_total" ...)
      // 在 event.ObjectRef.Subresource=="binding" 时 Inc()
    • 然后在 Dashboard 将 Panel-5 的 Series-A 改为 sum(pod_bind_requests_total)

⚖️ 结论:默认 _count 与 Counter 效果一致且无需新指标,不会导致调度数量误差;若需要可根据上述方法自定义。


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-09-10 08:39:42
  • 链接: https://freshwlnd.github.io/2025/08/07/k8s/k8s-scheduler-performance-volcano-metrics/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论