一条竖线省下三十分钟排查
目标
在只有一个错误率面板的仪表板上,把部署和故障作为标注叠加上去,让图表不仅回答“什么时候”,还能回答“那时发生了什么”。标注通过 API 留下并重新读取确认,同时还要制作一个由部署流水线自动留下记录的记录器。
为什么重要
指标只能回答数值变化的时间点。那个时间点上人做了什么,是指标之外的事件,不在同一个界面里,所以凌晨被呼叫的人要在部署记录、聊天和配置仓库之间来回切换去对时间。调查时间的相当一部分就耗在这种来回切换上。标注把该事件放到同一条时间轴上,消除来回切换。不过,标注是无法事后补建的记录,必须在当时留下,这就需要由流水线而不是人工来留下。写什么也要事先定好——要让下一个人当场就能回滚,标注里必须有版本、操作人和回滚命令。
步骤
- 用
lab-start-grafana启动 Grafana,并创建 uid 为gfd-annot的仪表板。面板是绘制 shop-api 5xx 比率的一个timeseries。然后在/root/gfd-annotations/01-blind.txt中写三行:question=后写该面板回答的问题,unanswerable=后写仅凭该面板无法回答的问题,reason=后写无法回答的原因,不少于 40 个字符。 - 用
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 与所写的版本)。 - 留下一条带有开始和结束的区间标注。标签是
incident,开始是距现在 25 分钟前,结束是距现在 5 分钟前(因此长度为 20 分钟)。正文用一行写明发生了什么。然后在/root/gfd-annotations/03-region.txt中写两行id=<그 id>和duration_min=<구간 길이(분)>(占位符依次为该 id 与区间长度,单位为分钟)。 - 在仪表板的
annotations.list中声明两个标注查询。一个取回deploy标签,另一个取回incident标签。两个条目都必须处于开启状态(enable),颜色(iconColor)互不相同,目标是按标签过滤的形式(target.type为tags)。为避免只取回同时带有两个标签的标注,每个条目只放一个标签。 - 创建
/root/gfd-annotations/annotate-deploy.sh。它以第一个参数接收版本,在gfd-annot仪表板上留下标注,标签是deploy和auto两个,正文中包含version=、by=、rollback=三个值。如果给出环境变量DRY_RUN=1,则不发送,只把要发送的 JSON 正文打印到标准输出后结束(此时输出只能是一个 JSON)。然后用该脚本实际记录两个不同的版本。 - 在
/root/gfd-annotations/annotation-fields.txt中每行写一个部署标注中必须写的字段名——version、by、rollback三项必须包含。然后在gfd-annot仪表板上留下一条遵守该格式的标注。标签是deploy和runbook两个,正文中必须以필드이름=값的形式(占位符依次为字段名与值)写入所记下的全部字段。rollback的值必须是可以直接输入的命令,所以不少于 10 个字符。带有runbook标签的标注只能有这一条。 - 创建
/root/gfd-annotations/07-timeline.tsv。把gfd-annot仪表板上的所有标注,按时间升序(相同则按 id 升序)每行一条,用制表符分成三列<id> <태그 하나> <요약>(占位符依次为 id、一个标签与摘要)。第二列必须是该标注上实际带有的某一个标签,第三列是不少于 4 个字符的摘要。 - 在
/root/gfd-annotations/08-finding.txt中写四行——deploy_id=是第 2 步留下的部署标注的 id,incident_id=是第 3 步留下的区间标注的 id,gap_min=是部署时间与故障开始时间之差按分钟四舍五入的整数(不小于 0),verdict=是把这两个事件连起来得出的结论,是一句不少于 60 个字符的话。这两个时间请从 API 重新读取后计算。
参考
- 工作目录是
/root/gfd-annotations。部署历史的素材是/opt/lab/gfd/gfd-annotations/releases.tsv,生成该文件的脚本是/opt/lab/gfd/gfd-annotations/make.sh。 - 用
lab-start-grafana启动 Grafana(需要 20–40 秒)。它是匿名 Admin,无需令牌即可使用 API,并且可以通过终端上方网页预览的 3000 端口打开界面。 - 标注的时间是毫秒级的 epoch 整数。如果按秒填写,会被标在 1970 年代的某处,从界面上消失。
- 不能在这个环境中判定的内容:标注是否真的在界面上画成了竖线。面板由浏览器绘制,而这里没有图像渲染器插件。评分完全只依据 API 响应和仪表板模型——查询已经声明、并且能用该标签查到标注,说明绘制的条件已经具备,但这与“已经画出来”不是一回事。
- 常见错误:没有
dashboardUID就创建,结果成了组织级标注。创建后立即用GET重新读取确认的习惯,可以当场抓住这个错误。 - Annotate visualizations · Annotations HTTP API · Dashboard HTTP API · Dashboard JSON model
图表只能回答“什么时候”
用 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}'
结论不必断定“就是部署导致的”。间隔很短,是值得怀疑的依据,而不是证据。如果连下一个人应该先确认什么都写出来,这条标注就成了调查记录。