LabHub
学习 学习路径 课程

Grafana — 仪表盘是一个问题

做一块只回答一个问题的看板

在 LabHub 中继续学习

目标

启动真实 Grafana 并连接真实 Prometheus,从零创建一个回答单一问题的 dashboard。完成后将其固化为 provisioning 文件,使这个 Grafana 即使被删除,也能重新构建相同画面。

这是第一个带界面的实验。Grafana 在 http://127.0.0.1:3000 启动后,可通过 terminal 上方的网页预览按钮打开真实 Grafana 画面。panel 既可以在界面中点击创建,也可以通过 API 上传;评分器不关心创建方式,只检查 Grafana 中最终呈现的结果。

为什么重要

dashboard 不是图片,而是回答问题的工具。因此,本实验不按绘制 panel 的顺序进行,而遵循问题 → query → panel → alert → 文件的顺序。

最后一步尤其重要。点击创建的 dashboard 只存在于 Grafana database 中;database 一旦消失,dashboard 也随之消失,并且不会留下谁在何时修改了什么。实际工作中找不到“当时那个 dashboard”,几乎总是因为这个原因。

此 Pod 中包含虚拟服务 shop-api 的 12 小时指标。http_requests_total 有四个 handler label(/api/orders/api/search/api/users/healthz),响应时间记录在 http_request_duration_seconds_bucket histogram 中。

步骤

  1. /root/graf/provisioning/datasources/prometheus.yml 中声明 data source,并在 http://127.0.0.1:3000 启动 Grafana。data source 的 uid 为 labprom,地址为 http://127.0.0.1:9090
  2. /root/graf/02-question.md 中用一句话写出此 dashboard 要回答的问题,并在 /root/graf/02-question.promql 中用一行写出回答该问题的 PromQL。
  3. 创建并上传一个仅含该 query 的单 panel dashboard,uid 为 shop-api
  4. 添加 p95 响应时间和当前请求率 panel。类型应符合问题的形态。
  5. 添加 handler template variable,让所有 panel 都按该变量筛选。
  6. /root/graf/runbook.md 中编写 runbook:哪里坏了/先看什么/如何回滚。
  7. /root/graf/provisioning/alerting/shop-api.yml 中创建 alert rule,并指向 runbook。
  8. 将 dashboard 导出为 JSON,放到 /root/graf/dashboards/shop-api.json,并通过 provider 文件让 Grafana 读取该目录。

参考

启动 Grafana,并通过文件连接 data source

/root/graf/provisioning/datasources/prometheus.yml 中声明 uid 为 labprom 的 prometheus data source,将该目录指定为 provisioning 路径,并在 http://127.0.0.1:3000 启动 Grafana。

如果在 UI 中点击添加 data source,它只会保留在 Grafana DB 中。评分器会检查 API 响应的 readOnly 是否为 true,这表示“来自文件”。

mkdir -p /root/graf/provisioning/datasources /root/graf/provisioning/dashboards \
         /root/graf/provisioning/alerting /root/graf/dashboards \
         /tmp/gf/data /tmp/gf/logs /tmp/gf/plugins

GF_PATHS_DATA=/tmp/gf/data GF_PATHS_LOGS=/tmp/gf/logs GF_PATHS_PLUGINS=/tmp/gf/plugins \
GF_PATHS_PROVISIONING=/root/graf/provisioning \
GF_SERVER_HTTP_PORT=3000 \
GF_AUTH_ANONYMOUS_ENABLED=true GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \
GF_PLUGINS_PREINSTALL_DISABLED=true \
  setsid nohup grafana server --homepath /opt/grafana >/var/log/grafana.log 2>&1 </dev/null &

curl -s http://127.0.0.1:3000/api/health          # "database": "ok" 가 나올 때까지 20~40초
curl -s http://127.0.0.1:3000/api/datasources/uid/labprom/health

这组环境变量各有原因:把 data、log、plugin 路径转到 /tmp,是因为此 Pod 没有 capability,可能无法写入默认路径;启用 anonymous access,是为了通过网页预览打开时不先遇到登录页;禁用 plugin 预安装,是因为 Grafana 11.4 每次启动都会尝试从互联网下载 grafana-lokiexplore-app,而此 Pod 无法访问外网。

先写下 dashboard 要回答的问题

/root/graf/02-question.md 中写一句以问号结尾、由此 dashboard 回答的问题,并在 /root/graf/02-question.promql 中写出回答它的 PromQL。问题是“shop-api 响应中有多少百分比是 5xx?”。

要计算的不是数量,而是比例。流量翻倍时 5xx 数量也会翻倍,但用户遭遇失败的概率不变。

用 5xx 请求的每秒速率除以全部请求的每秒速率。使用 status label 和 rate(),窗口设为 [5m]

promq 'sum(rate(http_requests_total[5m]))'
promq 'sum by (status) (rate(http_requests_total[5m]))'
promq "$(cat /root/graf/02-question.promql)"

评分器会把你写的 query 通过 Grafana data source 实际执行并检查结果。写法不同也没关系,只要数值正确即可。

创建回答问题的 panel

创建 uid 为 shop-api 的 dashboard,并加入一个绘制第 2 步 query 的 timeseries panel。dashboard 标题应能看出它回答的问题。

可以通过网页预览打开 Grafana,新建 dashboard 并添加 panel(此时在 dashboard 设置中将 uid 指定为 shop-api),也可以像下面这样通过 API 上传。

curl -s -XPOST -H 'Content-Type: application/json' \
  -d @/tmp/dash.json http://127.0.0.1:3000/api/dashboards/db

/tmp/dash.json 的结构是 {"overwrite": true, "dashboard": { ... }}

类型必须为 timeseries。incident 中需要的答案不是“现在是百分之几”,而是“从何时开始上升”;没有时间轴的类型无法回答。

可按如下方式检查上传结果。

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.dashboard.panels'

让 panel 类型符合问题形态

在同一 dashboard 中添加 p95 响应时间 panel 和当前请求率 panel,使 panel 总数变为三个。p95 使用 timeseries,当前请求率使用 stat

p95 从 histogram 计算。le 是 bucket 边界,应只保留它并聚合其余 label。

promq 'histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))'
promq 'sum(rate(http_requests_total[5m]))'

当前请求率不要使用 gauge。gauge 用来显示数值在 0 与最大值之间的位置,但每秒请求数没有最大值。当前时刻的单个数字应使用 stat

反之,若 p95 使用 statgauge,就无法看到“从何时开始变慢”。quantile 随时间如何变化才是关键。

让一个 dashboard 查看四个 handler

添加名为 handler 的 query 类型 template variable,并让三个 panel 的 query 都按该变量筛选。

手工列值的 custom variable 会在新增 handler 的那天过时。请从数据中读取。

label_values(http_requests_total, handler)

而且只声明 variable 不会产生任何效果。panel query 必须像这样筛选。

sum(rate(http_requests_total{handler="$handler"}[5m]))

评分器会把 variable 替换为真实值后执行 query。label 名称或引号错误会返回空结果并立即失败。

先写 runbook,再创建 alert

/root/graf/runbook.md 中编写 runbook。必须包含 ## 무엇이 깨졌나## 먼저 볼 것## 되돌리는 법 三节,并且“先看什么”中要有可实际执行的 http_requests_total query。

顺序如此安排是有原因的。若先创建 alert,就会变成“响了再想”,文档最终不会被写。先写 runbook,便能阻止那些无法回答“它响起时人现在该做什么”的 alert 被创建。

“先看什么”必须是命令,而不是句子。“检查状态”在凌晨 3 点毫无帮助。请直接写下统计哪个 handler 正在失败的 query。

创建 alert 并指向 runbook

/root/graf/provisioning/alerting/shop-api.yml 中创建 5xx 比例 alert rule。必须有 threshold 条件,for 至少 5 分钟,annotations.runbook_url 必须指向第 6 步的 runbook。写完文件后重新启动 Grafana。

for 是本步骤的核心。只要一个请求失败,5xx 就可能短暂升高;若每次都叫醒人,对方以后会忽略 alert。alert 不是因为关闭而死,而是因为被忽略而死。

alert query 不能使用 $handler 等 dashboard variable。alert 在没有画面的情况下求值,没有 dropdown 来展开 variable。

provisioning 配置在启动时读取。只写文件不会发生任何变化。

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)
curl -s http://127.0.0.1:3000/api/v1/provisioning/alert-rules | jq '.[].title'

也可使用管理员账户重新加载(anonymous access 会返回 403)。

curl -XPOST -u admin:admin http://127.0.0.1:3000/api/admin/provisioning/alerting/reload

将 dashboard 固化为文件

把当前画面中的 dashboard 导出为 JSON,保存到 /root/graf/dashboards/shop-api.json;通过 /root/graf/provisioning/dashboards/lab.yml 让 Grafana 读取该目录,然后重新启动。评分器会检查 meta.provisioned 是否为 true

dashboard JSON 与 provider 文件并不相同。provider 文件不包含 dashboard,而是指示应该读取哪个目录

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api \
  | jq '.dashboard' > /root/graf/dashboards/shop-api.json

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.meta.provisioned'

meta.provisionedtrue,表示当前画面来自文件。此后即使尝试用 API 覆盖,Grafana 也会以 Cannot save provisioned dashboard 拒绝,从而阻止画面与文件发生偏差。