TT Lab
开始
学习 学习路径 课程

Grafana — 仪表盘是一个问题

一条竖线省下三十分钟排查

在 TT Lab 中继续学习

目标

在只有一个错误率面板的仪表板上,把部署和故障作为标注叠加上去,让图表不仅回答“什么时候”,还能回答“那时发生了什么”。标注通过 API 留下并重新读取确认,同时还要制作一个由部署流水线自动留下记录的记录器。

为什么重要

指标只能回答数值变化的时间点。那个时间点上人做了什么,是指标之外的事件,不在同一个界面里,所以凌晨被呼叫的人要在部署记录、聊天和配置仓库之间来回切换去对时间。调查时间的相当一部分就耗在这种来回切换上。标注把该事件放到同一条时间轴上,消除来回切换。不过,标注是无法事后补建的记录,必须在当时留下,这就需要由流水线而不是人工来留下。写什么也要事先定好——要让下一个人当场就能回滚,标注里必须有版本、操作人和回滚命令。

步骤

  1. 用 lab-start-grafana 启动 Grafana,并创建 uid 为 gfd-annot 的仪表板。面板是绘制 shop-api 5xx 比率的一个 timeseries。然后在 /root/gfd-annotations/01-blind.txt 中写三行:question= 后写该面板回答的问题,unanswerable= 后写仅凭该面板无法回答的问题,reason= 后写无法回答的原因,不少于 40 个字符。
  2. 用 POST /api/annotations 为该仪表板(dashboardUID 为 gfd-annot)留下一条部署标注。标签中必须有 deploy,正文中必须包含 version=vX.Y.Z 形式的版本。时间设为距现在 30 分钟前(毫秒 epoch)。用响应中返回的 id 再次读取 GET /api/annotations 确认后,在 /root/gfd-annotations/02-annot.txt 中写两行 id=<그 id> 和 version=<적은 버전>(占位符依次为该 id 与所写的版本)。
  3. 留下一条带有开始和结束的区间标注。标签是 incident,开始是距现在 25 分钟前,结束是距现在 5 分钟前(因此长度为 20 分钟)。正文用一行写明发生了什么。然后在 /root/gfd-annotations/03-region.txt 中写两行 id=<그 id> 和 duration_min=<구간 길이(분)>(占位符依次为该 id 与区间长度,单位为分钟)。
  4. 在仪表板的 annotations.list 中声明两个标注查询。一个取回 deploy 标签,另一个取回 incident 标签。两个条目都必须处于开启状态(enable),颜色(iconColor)互不相同,目标是按标签过滤的形式(target.type 为 tags)。为避免只取回同时带有两个标签的标注,每个条目只放一个标签。
  5. 创建 /root/gfd-annotations/annotate-deploy.sh。它以第一个参数接收版本,在 gfd-annot 仪表板上留下标注,标签是 deploy 和 auto 两个,正文中包含 version=、by=、rollback= 三个值。如果给出环境变量 DRY_RUN=1,则不发送,只把要发送的 JSON 正文打印到标准输出后结束(此时输出只能是一个 JSON)。然后用该脚本实际记录两个不同的版本。
  6. 在 /root/gfd-annotations/annotation-fields.txt 中每行写一个部署标注中必须写的字段名——version、by、rollback 三项必须包含。然后在 gfd-annot 仪表板上留下一条遵守该格式的标注。标签是 deploy 和 runbook 两个,正文中必须以 필드이름=값 的形式(占位符依次为字段名与值)写入所记下的全部字段。rollback 的值必须是可以直接输入的命令,所以不少于 10 个字符。带有 runbook 标签的标注只能有这一条。
  7. 创建 /root/gfd-annotations/07-timeline.tsv。把 gfd-annot 仪表板上的所有标注,按时间升序(相同则按 id 升序)每行一条,用制表符分成三列 <id> <태그 하나> <요약>(占位符依次为 id、一个标签与摘要)。第二列必须是该标注上实际带有的某一个标签,第三列是不少于 4 个字符的摘要。
  8. 在 /root/gfd-annotations/08-finding.txt 中写四行——deploy_id= 是第 2 步留下的部署标注的 id,incident_id= 是第 3 步留下的区间标注的 id,gap_min= 是部署时间与故障开始时间之差按分钟四舍五入的整数(不小于 0),verdict= 是把这两个事件连起来得出的结论,是一句不少于 60 个字符的话。这两个时间请从 API 重新读取后计算。

参考

图表只能回答“什么时候”

用 lab-start-grafana 启动 Grafana,并创建 uid 为 gfd-annot 的仪表板。面板是绘制 shop-api 5xx 比率的一个 timeseries。然后在 /root/gfd-annotations/01-blind.txt 中写三行:question= 后写该面板回答的问题,unanswerable= 后写仅凭该面板无法回答的问题,reason= 后写无法回答的原因,不少于 40 个字符。

启动 Grafana 需要 20–40 秒。请等到 curl -s http://127.0.0.1:3000/api/health 返回 "database": "ok" 为止。

5xx 比率不是个数,而是比率。用 5xx 请求的每秒次数除以全部请求的每秒次数。先用 promq "<PromQL>" 查询一下,确认有数字返回。

面板类型必须是 timeseries 的原因,会在下一步显现。官方文档写明,支持标注的可视化是 Time series、State timeline 和 Candlestick——只显示一个数字的面板没有地方放事件。

把部署记为标注,并重新读取确认

用 POST /api/annotations 为该仪表板(dashboardUID 为 gfd-annot)留下一条部署标注。标签中必须有 deploy,正文中必须包含 version=vX.Y.Z 形式的版本。时间设为距现在 30 分钟前(毫秒 epoch)。用响应中返回的 id 再次读取 GET /api/annotations 确认后,在 /root/gfd-annotations/02-annot.txt 中写两行 id=<그 id> 和 version=<적은 버전>(占位符依次为该 id 与所写的版本)。

必填字段只有 text 一个。如果不填 dashboardUID,就会成为组织级标注,无法按这个仪表板过滤。

curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"dashboardUID":"gfd-annot","time":1700000000000,"tags":["deploy"],"text":"..."}' \
  http://127.0.0.1:3000/api/annotations
curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' | jq

最常见的错误是搞错时间单位。如果按秒填写,会被标在 1970 年代的某处,从界面上消失。30 分钟前是 $(( ($(date +%s) - 1800) * 1000 ))。

后面的步骤也会用到这个版本。部署历史在 /opt/lab/gfd/gfd-annotations/releases.tsv 中。

用区间标注标出故障时间

留下一条带有开始和结束的区间标注。标签是 incident,开始是距现在 25 分钟前,结束是距现在 5 分钟前(因此长度为 20 分钟)。正文用一行写明发生了什么。然后在 /root/gfd-annotations/03-region.txt 中写两行 id=<그 id> 和 duration_min=<구간 길이(분)>(占位符依次为该 id 与区间长度,单位为分钟)。

区间标注与点标注在同一个位置创建。不同之处只是要同时发送 timeEnd。官方文档写明,从 Grafana 6.4 起,区间由带有 time 和 timeEnd 的一个条目表示。

如果用 date 分别计算两个时间,秒数会错位,长度就会偏离 20 分钟。请只取一次基准时间,然后从它往前减。

NOW=$(date +%s)
echo $(( (NOW - 1500) * 1000 )) $(( (NOW - 300) * 1000 ))

评分器不会直接相信 duration_min,而是根据标注的两个时间重新计算后对照。

用标签区分,让仪表板取回

在仪表板的 annotations.list 中声明两个标注查询。一个取回 deploy 标签,另一个取回 incident 标签。两个条目都必须处于开启状态(enable),颜色(iconColor)互不相同,目标是按标签过滤的形式(target.type 为 tags)。为避免只取回同时带有两个标签的标注,每个条目只放一个标签。

仪表板 JSON 的 annotations.list 是条目数组。一个条目就是界面上方的一个开关,名称就是该开关的名称。

要指向内置的标注数据源,把 datasource 设为 {"type": "grafana", "uid": "-- Grafana --"}。

curl -s http://127.0.0.1:3000/api/dashboards/uid/gfd-annot \
  | jq '.dashboard.annotations.list'

类型一混在一起,开关就没用了。想只看部署时,如果故障区间也一起打开,最终就没有人再用开关了。

让流水线自动留下

创建 /root/gfd-annotations/annotate-deploy.sh。它以第一个参数接收版本,在 gfd-annot 仪表板上留下标注,标签是 deploy 和 auto 两个,正文中包含 version=、by=、rollback= 三个值。如果给出环境变量 DRY_RUN=1,则不发送,只把要发送的 JSON 正文打印到标准输出后结束(此时输出只能是一个 JSON)。然后用该脚本实际记录两个不同的版本。

自动记录器的全部意义在于忙的日子也不会漏掉。因此要固定格式,并杜绝由人手工调用。

设置打印模式有两个原因:只在流水线内部运行的脚本出了故障很难确认,而且测试必须能在没有副作用的情况下运行。评分器也用这个模式检查正文——并且还会同时检查前后标注数量是否没有增加。

构造正文时,如果手工拼接字符串,遇到引号就会出错。请用 jq -n --arg 来构造。要把时间作为数字放入,则用 --argjson。

部署历史在 /opt/lab/gfd/gfd-annotations/releases.tsv 中。

下一个人能否当场采取行动

在 /root/gfd-annotations/annotation-fields.txt 中每行写一个部署标注中必须写的字段名——version、by、rollback 三项必须包含。然后在 gfd-annot 仪表板上留下一条遵守该格式的标注。标签是 deploy 和 runbook 两个,正文中必须以 필드이름=값 的形式(占位符依次为字段名与值)写入所记下的全部字段。rollback 的值必须是可以直接输入的命令,所以不少于 10 个字符。带有 runbook 标签的标注只能有这一条。

标注的内容不是喜好,而是契约。凌晨三点看到这条标注的人,必须能不打开别的窗口就采取下一步行动。只写了一个提交哈希的标注,会把这个人送去仓库。

写下回滚命令尤其重要。即使部署的人在睡觉,别人也必须能回滚,而这就要求命令在标注里。实际命令在 /opt/lab/gfd/gfd-annotations/releases.tsv 中。

评分器会读取你记下的字段列表,并对照标注是否按该列表填写。

按时间顺序重新读取标注,制作调查记录

创建 /root/gfd-annotations/07-timeline.tsv。把 gfd-annot 仪表板上的所有标注,按时间升序(相同则按 id 升序)每行一条,用制表符分成三列 <id> <태그 하나> <요약>(占位符依次为 id、一个标签与摘要)。第二列必须是该标注上实际带有的某一个标签,第三列是不少于 4 个字符的摘要。

排序标准必须与评分器一致。使用 jq 的 sort_by(.time, .id),即使是同一毫秒打上的标注,顺序也不会摇摆。

curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' \
  | jq -r 'sort_by(.time, .id)[] | [(.id|tostring), .tags[0], .text] | @tsv'

这张表就是调查记录的骨架。按时间顺序排开后,“部署 → 错误 → 回滚”这样的顺序就会映入眼帘,而这个顺序就是原因假设。

把部署和故障连起来,写下结论

在 /root/gfd-annotations/08-finding.txt 中写四行——deploy_id= 是第 2 步留下的部署标注的 id,incident_id= 是第 3 步留下的区间标注的 id,gap_min= 是部署时间与故障开始时间之差按分钟四舍五入的整数(不小于 0),verdict= 是把这两个事件连起来得出的结论,是一句不少于 60 个字符的话。这两个时间请从 API 重新读取后计算。

评分器会通过 API 重新读取这两个 id,自己计算间隔,并与你写的值对照。所以凭手工估算的数字无法通过。

curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' \
  | jq -c '.[] | {id, time, timeEnd, tags}'

结论不必断定“就是部署导致的”。间隔很短,是值得怀疑的依据,而不是证据。如果连下一个人应该先确认什么都写出来,这条标注就成了调查记录。