版本都在,却没人知道改了什么
一句话总结
“用代码管理仪表板”这句话的核心,不在于把它放进文件,而在于让改动能被读懂、也能被回滚。
为什么需要它
故障的第二天,仪表板变了。不知道是谁改的,也不知道改了什么。Grafana 会保留版本,但把两个版本的 JSON 下载下来对比,会发现几百行整个都不一样,因为里面混着每次保存都会变的字段。所以“版本保留着”这个事实,与“知道改了什么”这个事实之间,有相当远的距离。
迁移时也一样。把做得很好的仪表板 JSON 原样放到另一个环境,面板会全部变空。界面上没有错误,就只是空的。这是因为仪表板引用数据源时用的是 uid 而不是名称,而自动生成的 uid 在每个环境中都不同。不知道这一点,就会白白花掉一天。
工作原理
Grafana 的仪表板每次保存都会生成新版本,保存请求中一并发送的消息会附在该版本上(仪表板 HTTP API)。如果把消息留空,列表里就只剩下时间。而且,首次保存的消息会被 Grafana 自己覆盖——不亲自确认的话,就会为“明明写了消息,为什么没留下”而困惑很久。
要让两个版本可读,需要规范化。每次保存都会变的是仪表板顶层的 version、id 这类字段。把它们去掉并对键排序后输出,从这时起,diff 才成为人能读的东西。规范化函数要具备两个性质:对相同输入总是产生相同输出(确定性),并且让只有易变字段不同的两个版本变成相同的东西。
| 做什么 | 为什么 |
|---|---|
去掉顶层的 version、id |
每次保存都会变,会淹没 diff |
| 对键排序 | 顺序一变,相同的内容看起来也不同 |
保留面板的 id |
它是指向面板的名称,有意义 |
迁移问题用数据源变量来解决。创建一个 datasource 类型的模板变量,让面板指向该变量,这样即使环境不同,需要修改的地方也只有一处(变量文档)。当查询带着不存在的 uid 发出时,Grafana 会返回 404,但这个响应只在浏览器开发者工具中才能看到,面板上只会显示为空图表。
到这里还需要另一个性质。必须能自己查询文件与界面是否一致。如果只是把仪表板放进仓库,却没有人确认它是否真的与界面一致,两边就会悄无声息地分道扬镳。规范化函数已经有了,所以这个检查很短——把从界面取得并规范化的结果,与规范化后的文件对比即可。而且这个检查器也要双向测试。如果不确认给它相同的东西会通过、给它不同的东西会失败,就会一直信任一个无论给什么都通过的检查器。
让仪表板能被找到,也是用代码管理的一部分。仪表板多到几十个时,该打开哪一个就成了问题,这时依靠的是文件夹和标签。两者都写在仪表板 JSON 和 provisioning 配置中,所以文件成为源头之后,就连添加一个标签,也必须修改文件来反映。这看起来很麻烦,但这份麻烦恰恰是“谁在什么时候出于什么原因”被留下的价值。
最后是把源头放在哪里。通过 provisioning 提供的仪表板,无论在界面上还是通过 API 都无法保存,尝试保存会被拒绝并返回 400(provisioning 文档)。这看起来不方便,但正是这种方式的核心——它从根本上消除了界面与文件分道扬镳的途径。从此,修改仪表板的唯一途径就是改文件并让它重新被读取,而这条路上附带代码评审。
在现场相遇的样子
有一个团队把仪表板放进了仓库,半年后界面和文件却已经完全不同。因为他们没有让文件来提供仪表板,只是把它当作备份放在那里。没有人有理由去改文件,也没有任何东西阻止人在界面上修改。改用 provisioning 之后,文件才成为源头。
另一个团队把仪表板迁移到 staging,结果为“没有数据”花了两天。查询是对的,Prometheus 也好好的。问题是面板的 datasource.uid 是生产环境中自动生成的值,而 staging 里没有这样的 uid。把它提取成数据源变量之后,迁移就只需要复制文件。
本环境能判定与不能判定的内容
这个 Pod 中的 Grafana 是真正在运行的,但没有图像渲染器,所以无法检查界面图形本身。不过,版本 API、provisioning 状态、保存被拒绝的响应、仪表板 JSON 模型都可以全部确认,本实验的判定就在这个范围内进行。仓库一侧(评审、CI)因为这个 Pod 中没有 git,所以不涉及——取而代之的是,做到把“要把什么放进仓库才算一整套”整理成清单为止。
下一项实验要做什么
先把仪表板上传,附上保存消息再保存一次,确认版本列表实际留下了什么。接着编写规范化脚本,把两个版本的差异提取成人能读懂的形式,再把数据源提取成变量,使它可以迁移。然后把仪表板改成由文件提供,亲自拿到保存被拒绝的响应。最后,通过修改文件来完成添加一个标签这件事,并制作能自己查看文件与界面是否一致的检查器。