首页
/ Istio Grafana 官方仪表盘:Jsonnet 生成流程与 UID 链接规范的工程实践

Istio Grafana 官方仪表盘:Jsonnet 生成流程与 UID 链接规范的工程实践

2026-09-05 10:07:23作者:牧宁李

本文围绕 Istio 仓库中 manifests/addons/dashboards 目录的官方开发指南展开,讲解 Istio 官方 Grafana 仪表盘“Jsonnet 生成 + JSON 手工导出”双轨维护模式、基于 MD5 哈希的 UID 链接规范,以及如何通过 gen.sh 一键生成并校验全部仪表盘产物。读完后,你可以独立理解 Istio 监控面板的代码化生成流程、新增一个 Jsonnet 仪表盘、并保证跨 Grafana 版本链接稳定。

1. 仪表盘目录的定位与发布链路

manifests/addons/dashboards 目录存放 Istio 的官方 Grafana 仪表盘。该目录承担两个角色:

  1. 发布源:在版本发布期间,这些仪表盘会被发布到 Grafana 官方仪表盘库(Istio 组织名下);
  2. 样例捆绑:仪表盘 JSON 会被打进 Istio 的 Grafana 样例部署 samples/addons/grafana.yaml,供用户在本地演示环境(demo profile 等)直接 kubectl apply 使用。

目录中的产物分为两类,这一“双轨制”是整个开发工作流的基础:

类型 文件特征 维护方式
新生成的仪表盘 *.libsonnet(源码)+ *.gen.json(生成物) 用 Jsonnet 代码化生成,是新增仪表盘的首选方式
遗留仪表盘 *.json 直接在 Grafana UI 中手工编辑后导出、提交入库

当前仓库中 Jsonnet 化的仪表盘有三个源码文件,对应三个控制面/环境组件:

  • pilot.libsonnetpilot-dashboard.gen.json(Istio 控制面 Pilot 仪表盘)
  • ztunnel.libsonnetztunnel-dashboard.gen.json(Ambient 模式 ztunnel 节点代理仪表盘)
  • istio-mesh.libsonnetistio-mesh-dashboard.gen.json(网格整体仪表盘)

遗留的手工 JSON 仪表盘则包括 istio-performance-dashboard.jsonistio-workload-dashboard.jsonistio-service-dashboard.jsonistio-extension-dashboard.json

2. Jsonnet + Grafonnet:仪表盘代码化工作流

README 明确指出:较新的仪表盘使用 [Jsonnet] 配合 Grafonnet 库生成,并且“任何新仪表盘都应优先采用这种方式”。

2.1 依赖声明:jsonnetfile.json

jsonnetfile.json 声明了唯一的第三方依赖——Grafonnet:

{
  "version": 1,
  "dependencies": [
    {
      "source": {
        "git": {
          "remote": "https://github.com/grafana/grafonnet.git",
          "subdir": "gen/grafonnet-latest"
        }
      },
      "version": "main"
    }
  ],
  "legacyImports": true
}

要点:

  • 依赖锁定在 gen/grafonnet-latest 子目录,版本取 main 分支;
  • 配套存在 jsonnetfile.lock.json 用于锁定解析结果;
  • "legacyImports": true 允许不带包前缀的裸导入(如 import 'g.libsonnet');
  • 运行期通过 jb install 将依赖拉取到本地 vendor/ 目录,jsonnet -J vendor -J lib 即可编译。

2.2 Grafonnet 的入口封装:g.libsonnet

lib/g.libsonnet 只有一行,它把 Grafonnet 的最新主库暴露为本地符号 g

import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet'

所有 .libsonnet 源文件的第一行都是 local g = import 'g.libsonnet';,之后统一使用 g.dashboard.*g.panel.* 等 Grafonnet API 构建仪表盘对象。

2.3 公共库(lib/):仪表盘骨架、网格、面板与查询

lib/ 目录把可复用的构件拆分为独立模块,这是 Jsonnet 方式优于手工 JSON 的核心原因——面板布局、时间范围、数据源变量都不必在每个仪表盘里重复:

  • lib/dashboard.libsonnet:仪表盘骨架工厂,统一注入默认行为——

    {
      new(name):
        g.dashboard.new(name)
        + g.dashboard.graphTooltip.withSharedCrosshair()
        + g.dashboard.withRefresh('15s')
        + g.dashboard.time.withFrom('now-30m')
        + g.dashboard.time.withTo('now')
        + g.dashboard.withVariables([variables.datasource]),
    }
    

    即每个新仪表盘自动获得 15 秒自动刷新、默认时间窗 now-30m ~ now、共享十字光标提示以及数据源变量。

  • lib/lib-grid.libsonnet:面板网格布局工具(grid.makeGrid([...], panelHeight=..., startY=...));

  • lib/panels.libsonnet:面板工厂(timeSeriesheatmap 等,区分 bytes 格式化、速率格式化等变体);

  • lib/queries.libsonnet:Prometheus 查询构造器,按 container/pod/component/app 标签参数化生成一组查询;

  • lib/variables.libsonnet:仪表盘变量(如 datasource 变量)。

2.4 一个真实的仪表盘源码:pilot.libsonnet

pilot.libsonnet 展示了完整组装过程:先通过 queries 模块按 pilot 的标签(pod: 'istiod-.*'app: 'istiod')取回查询集合,再用 grid.makeGrid 按行(row)拼装面板,最后一行为整个仪表盘写入 UID:

dashboard.new('Istio Control Plane Dashboard')
+ g.dashboard.withPanels(
  grid.makeGrid([
    row.new('Deployed Versions')
    + row.withPanels([
      panels.timeSeries.simple('Pilot Versions', queries.istioBuild, 'Version number of each running instance'),
    ]),
  ], panelHeight=5)
  + grid.makeGrid([
    row.new('Resource Usage')
    + row.withPanels([
      panels.timeSeries.bytes('Memory Usage', queries.goMemoryUsage, 'Memory usage of each running instance'),
      panels.timeSeries.allocations('Memory Allocations', queries.goAllocations, 'Details about memory allocations'),
      panels.timeSeries.base('CPU Usage', queries.cpuUsage, 'CPU usage of each running instance'),
      panels.timeSeries.base('Goroutines', queries.goroutines, 'Goroutine count for each running instance'),
    ]),
  ], panelHeight=10, startY=1)
  + ...  // Push Information、Webhooks 等行
)
+ g.dashboard.withUid(std.md5('pilot-dashboard.json'))

从源码结构看,pilot 仪表盘覆盖四大分区:Deployed Versions(各运行实例版本)、Resource Usage(内存/分配/CPU/goroutine)、Push Information(xDS 推送速率、事件、连接数、推送错误、推送时延/体积热力图)、Webhooks(验证与注入速率)。ztunnel.libsonnet 遵循完全相同的组装模式,只是查询标签换成 pod: "ztunnel-.*"app: "ztunnel",并按 Process / Network / Operations 三行组织面板。

3. 仪表盘链接规范:强制 UID 链接

这是 README 中约束最严格的一节:所有仪表盘必须使用 UID 链接,禁止路径(path-based)链接。原因有三:

  1. 路径链接在新版 Grafana 中已被废弃;
  2. UID 链接在 Grafana 版本升级间更稳定;
  3. UID 链接在自建 Grafana 与 Grafana Cloud 上都能正常工作。

3.1 用文件名 MD5 作为 UID

Istio 的约定是:仪表盘的 UID 取其仪表盘文件名(JSON 文件名)的 MD5 哈希,例如:

g.dashboard.withUid(std.md5('istio-mesh.json'))

仓库中现有三处实例验证了该约定:

源文件 UID 表达式
pilot.libsonnet#L86 g.dashboard.withUid(std.md5('pilot-dashboard.json'))
ztunnel.libsonnet#L56 g.dashboard.withUid(std.md5('ztunnel.json'))
istio-mesh.libsonnet#L60 g.dashboard.withUid(std.md5('istio-mesh.json'))

注意一个细节:pilot 与 ztunnel 的 MD5 输入文件名与生成产物名不一致(产物是 pilot-dashboard.gen.jsonztunnel-dashboard.gen.json,而哈希输入分别是 pilot-dashboard.jsonztunnel.json)。这并非笔误,而是刻意为之的兼容性选择——保证 UID 与历史版本仪表盘在 Grafana 库/集群中已有记录保持一致,避免升级后旧书签失效。

3.2 跨仪表盘链接的两个 helper 库

仪表盘之间互相跳转时,统一使用两个 helper 库,而不是各自硬编码 UID:

两者结构对称,以 Service 为例:

{
  // Service 仪表盘的 UID(MD5 of istio-service-dashboard.json)
  uid:: std.md5('istio-service-dashboard.json'),

  // 数据链接:面板内数据点跳到 Service 仪表盘并带上当前变量
  dataLink(title='View Service')::
    g.panel.link.new(title)
    + g.panel.link.withUrl('/d/' + $.uid + '?${vars}')
    + g.panel.link.withTargetBlank(true),

  // 顶部导航链接:跳转到 Service 仪表盘
  dashboardLink::
    g.dashboard.link.dashboards.new('Service Dashboard', [$.uid])
    + g.dashboard.link.dashboards.options.withAsDropdown(false)
    + g.dashboard.link.dashboards.options.withIncludeVars(true)
    + g.dashboard.link.dashboards.options.withKeepTime(true)
}

三类链接语义清晰:

  • uid:单一真源,所有地方引用这个 UID 而不重复写 MD5 字符串;
  • dataLink:面板数据链接,URL 形如 /d/<uid>?${vars}——/d/ 前缀正是 Grafana 的 UID 路由,${vars} 会把当前仪表盘的变量值透传给目标页,并在新标签页打开;
  • dashboardLink:仪表盘顶部导航链接,配置为非下拉(withAsDropdown(false))、携带变量(withIncludeVars(true))、保持时间区间(withKeepTime(true))。

README 给出的使用示例:

local serviceDashboard = import 'istio-service.libsonnet';

dashboard.new('My Dashboard')
+ g.dashboard.withLinks([
  serviceDashboard.dashboardLink
])

即新仪表盘只要导入 helper 库并把 dashboardLink 挂进 withLinks,就自动获得“跳转 Service 仪表盘”导航项,UID 一致性由 helper 库保证。

4. 生成流程:gen.sh 一键产出全部 addon 部署

README 的“Generation”一节指出:无论哪种类型的仪表盘,统一执行 ./manifests/addons/gen.sh 生成全部所需产物。阅读 gen.sh 可以看到完整流水线:

./manifests/addons/gen.sh

脚本(set -eux 严格模式)依次完成:

  1. Kiali / Prometheus / Loki:分别用 helm template(固定 chart 版本:Kiali 2.31.0、Prometheus 28.13.0、Loki LOKI_VERSION 默认 7.2.0)加 values-kiali.yamlvalues-prometheus.yamlvalues-loki.yaml 渲染出 samples/addons/ 下对应的 YAML;

  2. Jsonnet 仪表盘生成:进入 dashboards/ 目录执行 jb install 安装 Grafonnet 依赖,然后对每个 *.libsonnet 执行:

    for file in *.libsonnet; do
      dashboard="${file%.*}"
      jsonnet -J vendor -J lib "${file}" > "${dashboard}-dashboard.gen.json"
    done
    

    命名规则固定:pilot.libsonnetpilot-dashboard.gen.json,以此类推;-J vendor -J lib 指定 import 搜索路径;

  3. Grafana 部署helm template grafanaGRAFANA_VERSION 默认 9.2.2)叠加 values-grafana.yaml 渲染;

  4. ConfigMap 拆分与压缩:为避免 Kubernetes 单对象尺寸上限,compressDashboard 函数先用 jq -c 把每个 JSON 压成单行,再分两个 ConfigMap 产出:

    • istio-grafana-dashboards:pilot、ztunnel、performance;
    • istio-services-grafana-dashboards:workload、service、mesh、extension;
  5. 自动校验:若存在 test_dashboard_links.sh,脚本末尾自动执行(详见第 5 节)。

values-grafana.yaml 揭示了这些 ConfigMap 如何被 Grafana 消费:两个 dashboardProvidersistioistio-services)把上述两个 ConfigMap 分别挂载到 /var/lib/grafana/dashboards/istio.../istio-services,并都归入 istio 文件夹;同时定义了默认 Prometheus 数据源(http://prometheus:9090,15s 采样间隔)与 Loki 数据源(http://loki:3100,5s 采样间隔)。此外该样例做了面向演示的简化(匿名访问、admin/admin、关闭 RBAC),生产环境使用时应按需收紧。

5. 校验脚本:test_dashboard_links.sh 确保没有回退到路径链接

生成后,可用 test_dashboard_links.sh 验证所有仪表盘都采用 UID 链接:

./manifests/addons/dashboards/test_dashboard_links.sh

脚本逻辑对每个 *.json / *.gen.json 文件做两类 grep 检查:

  • 禁止项/dashboard/db/ 出现即判定为废弃的路径链接,打印前 5 处命中位置并令整个检查失败(set -eu + error_count 计数,最终非零退出);
  • 期望项:存在 /d/(UID 路由)或 "dashboards"(仪表盘导航链接结构)即认为合格;两者都没有也不报错,仅输出 INFO: No dashboard links found in <file> 提示——即“无链接”合法,“错误格式”非法。

全部通过时输出:All dashboards use the proper UID-based linking format!。如前所述,gen.sh 末尾会自动调用该脚本,因此在正常的生成流水线中该校验是内建的一步,不需要手动补跑;手动运行的场景是只改了 JSON 而跳过完整生成时做快速自检。

6. 实践小结:新增一个仪表盘的操作步骤

综合 README 与仓库现状,在 Istio 中新增一个 Jsonnet 仪表盘的标准做法:

  1. manifests/addons/dashboards 下新建 <name>.libsonnet,复用 lib/dashboard.libsonnetdashboard.new('<名称>') 骨架,配合 lib/panels.libsonnetlib/queries.libsonnet 组装面板;
  2. 结尾调用 g.dashboard.withUid(std.md5('<目标JSON文件名>')) 固定 UID(注意与既有产物/历史 UID 保持一致,避免破坏用户已有书签);
  3. 如需从其他面板跳转到 Service/Workload 仪表盘,导入 istio-service.libsonnet / istio-workload.libsonnet 使用其 dataLink / dashboardLink
  4. 运行 ./manifests/addons/gen.sh,确认 <name>-dashboard.gen.json 生成、test_dashboard_links.sh 通过;
  5. 如需把新仪表盘加入样例部署,还要在 gen.shcompressDashboard 清单与对应 kubectl create configmap --from-file 列表中登记(该脚本按文件名显式列举,不是通配收集——从源码结构看,漏登记会导致仪表盘不会出现在 samples/addons/grafana.yaml 中)。

这套“Jsonnet 源码为真源、MD5 文件名定 UID、helper 库统一跳转、生成脚本内建链接校验”的流水线,使 Istio 官方仪表盘在代码化可维护与跨 Grafana 版本/环境链接稳定性之间取得了平衡;而遗留的手工 JSON 仪表盘则保持原样继续随版本演进,两者通过同一个 gen.sh 入口统一产出。

登录后查看全文
热门项目推荐
相关项目推荐