监控面板设计:让 Dashboard 说人话的 7 个原则 原创
本文是 SRE 实战系列第 17 篇,讲可观测性里最被低估的一环:Dashboard 的设计。全文约 9000 字,从"为什么大多数面板看一眼就关"开始,拆出 7 条能直接执行的原则,每条都给出反面例子、正确画法、可直接抄的 PromQL 和 Grafana JSON 片段,最后给出一套面板分层方案、命名与目录规范、评审 Checklist,以及一次把 27 块面板删到 6 块的真实改造记录。
这一篇接的是第 7、12、14 篇留下的尾巴:数据采上来了、三支柱也打通了,但数据多不等于看得懂。面板是把指标变成判断力的最后一道工序,也是唯一一道天天影响 on-call 手速的工序。
一、先说一个所有人都在经历的场景
凌晨一点半,告警来了。你打开 Grafana,首页收藏夹里躺着 27 块面板。
第一块叫 K8s Cluster Overview,60 个面板排在 5 屏里,CPU、内存、磁盘、网络、Pod 数、API Server、etcd、CoreDNS 全在上面。你要找的是 payment-api 的 P99 延迟,于是你在几十个图里用鼠标滚轮上下找,用浏览器搜索功能搜 payment,找到一块叫 All Services Latency 的图,上面同时画着 40 条曲线,图例挤在右边,字体 10px,全是 -。
你点开图例想隐藏几条,结果隐藏完自己也忘了隐藏了哪几条。两分钟后,你放弃了这块面板,直接 kubectl logs 去了。
这块价值十几万的面板,在最需要它的那两分钟里,输给了 kubectl logs。
这不是个例。它几乎一定会发生,原因也很明确:
- 面板是"加"出来的,不是"设计"出来的。 谁有新需求就新建一块,三年下来只增不减,没有人为"这块该不该存在"负责
- 面板的消费者和生产者不是同一个人。 生产面板的人(做监控的)在白天、有上下文、知道指标含义;消费面板的人(on-call)在半夜、没上下文、只想赶紧判断"是不是我的事"
- 没人对面板的质量负责。 YAML 有 lint,告警有评审,PromQL 有单测,唯独面板没有门禁。它能渲染,就算通过
所以问题不在工具。Grafana 本身能画得很好——问题在于我们把面板当成"数据展示",而它其实是"决策界面"。
决策界面只有一个验收标准:它能不能让一个刚被叫醒、且不熟悉这个服务的人,在 30 秒内做出正确动作。
后面 7 条原则,都是这句话的展开。
二、原则 0:先承认一块面板只能回答一个问题
7 条原则之前,得先立一条底板原则,否则后面全是技巧没有抓手。
一块面板(dashboard)回答一个具体问题;一个问题需要多块面板协同时,用行(row)和链接组织,而不是摊平。
判断方法很简单,问三个问题:
- 这块面板,谁在看?(on-call / 服务 owner / 容量规划 / 老板)
- 他在什么场景下看?(告警后 1 分钟 / 每周复盘 / 季末汇报)
- 看完他要做什么决定?(回滚 or 扩容 or 转交 or 什么都不用做)
三个问题里任何一个答不上来,这块面板不该存在。
这条原则之所以写成"原则 0",是因为它是删面板的唯一依据。存量治理时你会遇到大量"这图好像有人用过"的面板——按这三问过滤,80% 会当场失去存在理由。
三、原则 1:一块面板,一个主角,一条时间线
反面
Panel: Service Health
├── 40 条 P99 延迟曲线(所有服务所有 Pod)
├── 旁边叠着错误率
└── 下面叠着 CPU 使用率图例默认展示全部,纵轴一个单位的图里混了三类量纲。
正确
一块面板 = 一个主体 + 一个指标 + 一个时间窗口。
- 主体:
payment-api(不是"所有服务") - 指标:P99(不是"延迟、错误、CPU")
- 窗口:最近 6 小时(和告警的时间范围对齐,不是默认的 24 小时)
# 主角:payment-api 的 P99(按 status_code 分组,最多 2 条线)
histogram_quantile(
0.99,
sum by (le, status_code) (
rate(http_server_request_duration_seconds_bucket{service="payment-api"}[5m])
)
)为什么是 P99 而不是平均值:平均值会把一次持续 30 秒的尖刺稀释成一条平线,而用户感知到的正是那个尖刺。同样,一块图里"平均响应时间"这种指标基本可以一次性全部删掉。
要不要分 Pod:99% 的场景不要。分 Pod 只在"怀疑单实例异常"时才有价值,那属于下钻视图,不属于总览视图。默认按 Pod 分组的图,本质上是把"找出异常实例"的活丢给了人眼。
一句话验收:把这块面板截图发给一个没上下文的人,他能用一句话说出"这是哪个服务、在变好还是变坏"吗?说不出就重画。
四、原则 2:图是用来比较的,没有基线的曲线等于没有信息
反面
一块只有一条 P99 曲线、纵轴自适应(Grafana 默认 auto)的面板。曲线在 180ms 到 210ms 之间小幅波动,但因为纵轴被自动缩放到 175–215ms,看起来像悬崖。你截图发群里:"延迟炸了。" 对方看了一眼说:"这不一直这样吗?"
正确
每条曲线至少要有两个参照物,缺一个就少一半判断力:
| 参照物 | 怎么加 | 回答什么问题 |
|---|---|---|
| 目标线(SLO 阈值) | Thresholds 画一条红线 300ms | 现在算不算坏? |
| 历史同期基线 | 叠加 7 天前的同一曲线,或用 offset 7d | 是今天坏了,还是本来就长这样? |
| 上游/下游同指标 | 同一块图叠加依赖服务曲线 | 是我坏了还是它坏了? |
# 主曲线:当前 P99
histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{service="payment-api"}[5m])))
# 基线:7 天前同一时刻(虚线,颜色调淡)
histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{service="payment-api"}[5m] offset 7d)))Grafana 里把基线那条改成虚线 + 40% 透明度,并在图例里重命名成 7d ago。一眼就能看出"今天是常态还是异常"。
纵轴纪律(这条最容易被忽略,但收益极高):
- 延迟、错误率、饱和度这类有明确语义边界的指标,纵轴必须锁死,不用
auto。P99 面板固定0 – 1s,错误率固定0 – 100%(或0 – 5%,取决于 SLO) - 只有吞吐量这类没有自然上限的指标才允许自适应,且自适应时要在面板标题里写清单位
- 不锁纵轴的后果不是"图难看",而是同样 200ms 的抖动,在不同时间被渲染成完全不同严重程度的图形,直接误导判断
这条原则可以浓缩成一句:没有基线的曲线不是监控,是装饰。 一个不看也知道会波动的数字,看完之后你不比之前知道得更多。
五、原则 3:红黄绿只用于"要不要叫人",其余一律交给数字
反面
一块面板上 30 个 Stat 大数字,全部染成红色。因为所有人都在配 Thresholds: 80 表示"超过 80% 就红",于是内存 82% 是红的、磁盘 81% 是红的、CPU 83% 是红的、Pod 数 85% 也是红的(单位都不一样,阈值却一样)。
结果是红色失去信息量。on-call 学会的第一件事就是"红色是常态,不用管"。
正确
颜色的语义必须唯一且全局统一。推荐这套四色规则,写进团队规范:
| 颜色 | 含义 | 触发后果 | 谁决定 |
|---|---|---|---|
| 🔴 红 | 当前已在伤害用户(对应一条 P1/P2 告警) | 叫人 | SLO 错误预算 |
| 🟡 黄 | 正在烧预算,但还没到叫人阈值 | 值班记录、工作时间处理 | 燃烧率告警规则 |
| 🔵 蓝 | 容量趋势,未来 2–4 周会到限 | 进扩容队列 | 容量规划 |
| ⚪ 灰/绿 | 一切正常 | 无事 | — |
关键点:颜色必须和告警规则同源。图上红,就必须有一条对应的告警在响;图上是黄色,就必须能在告警规则里找到对应的阈值。反过来,没有对应告警的红色,一律降级为无色。
这条的工程做法很简单:Grafana 的 threshold 值和告警规则里的 expr 阈值,从同一个 ConfigMap 生成。哪怕只是手工维护,也要在面板描述里写清"这个红色对应告警规则 SLOBurnRateHigh"。
"红黄绿"之外的信息,用数字和单位说人话:
❌ 0.9234 (什么单位?越大越好还是越小越好?)
✅ 92.3% (目标 99.9%,差 7.6pp)
❌ 1843
✅ 1.8k req/s (峰值 2.6k,容量 4k)Grafana 的 Unit 和 Decimals 是必填项,不是可选项。另外,Stat 面板右下角那个 Since / 上次变化时间 字段建议保留——它能单独回答"这个数是稳的还是刚变的"。
六、原则 4:从"症状"导航到"原因",最多三次点击
反面
on-call 的典型路径:从告警链接点进 K8s Cluster Overview(第 1 块),发现网络有问题,去收藏夹找 Node Detail(第 2 块),发现是某台机器,再去搜 Pod Metrics(第 3 块),发现是某个 Pod,最后去 Kibana 搜日志(第 4 个系统)。每次跳转都要重新选变量、重新选时间范围,其中两次还忘了改时间范围所以图是空的。
这段路上的每一分钟都不产生判断,只消耗注意力。
正确
面板之间要有向且带上下文。三层结构,一跳一层:
L1 服务概览(SLO / 错误率 / 延迟 / 流量) ← 告警唯一入口
│ 点击任一分区 → service 变量自动带过去
L2 服务下钻(实例、依赖、上下游、错误分类)
│ 点击异常实例 → pod / node 变量自动带过去
L3 资源与日志(Pod 指标、节点指标、Loki 日志、Trace 查询)三个落地要求:
- L1 是所有告警的
runbook_url唯一指向。告警消息里不给 L3 链接,只给 L1 + 一句"然后看第二行" - 跳转必须携带变量和时间范围。Grafana 的数据链接(Data links)里写:少传
/d/svc-drilldown?var-service=${__field.labels.service}&var-pod=${__field.labels.pod}&from=${__from}&to=${__to}from/to是面板跳转最常见的坏味道,跳过去之后图是空的,人只能手动调时间 - 每一层顶部有一行文字,写"本层回答什么、接下来该去哪"。这不是文档,这是界面的一部分
宽而不深的平铺是反面:47 块面板并列在收藏夹里,跳转全靠浏览器搜索。信息架构的成本最终会转嫁到凌晨两点的那个人身上。
七、原则 5:用变量把 20 块面板压成 1 块,但别让人在 20 个下拉框里选
反面
为 20 个服务建了 20 块一模一样的延迟面板。新人入职后问:"为什么 20 块图只长得一样?" 老员工答:"方便同时看。" 后来又加了 20 块错误率面板。
正确
一块带变量的面板,服务名下拉切换。Grafana 里就是:
Variable: service type=query
query: label_values(up{job="kubelet"}, service) # 或从 Prometheus label API 拉
Variable: pod type=query
query: label_values(up{service="$service"}, pod)
multi=true变量设计的三条纪律:
- 下拉框不超过 3 个,且必须是有依赖顺序的(service → pod → container)。超过 3 个说明你要拆面板了
- 必须有默认值,且默认值对 on-call 有意义。默认值优先取"当前有告警的服务",做不到就取上次访问值(Grafana 自带),最差才是空
- 变量的取值空间要裁掉噪音。
label_values(pod)在生产环境里动辄上千个值,下拉列表打开要 5 秒——加{namespace=~"prod-.*"}之类的前置过滤,或者把历史 Pod 从监控侧 limit 掉
什么时候该拆面板而不是加变量——判断标准是"看的人不同"。给 on-call 的用单服务变量视图;给容量规划的用全服务对比视图(这时"20 条线"是合理的,因为目的就是横向比较)。同一块面板服务两类人,最后一定服务不好任何一类。
八、原则 6:把"这一屏"当一页纸,用视线顺序排布
反面
一块"服务详情"面板:左上角是版本信息和 Pod 数,中间是 12 块资源利用率小图,右下角角落里,是唯一的错误率曲线。
正确
F 型 / Z 型视线顺序,按"决策优先级"排,不是按"指标分类"排。一张 1920×1080 的屏幕,四个格子的语义应该是固定的(团队内所有面板保持一致):
┌─────────────────────────────┬─────────────────────────────┐
│ 左上:出问题了没? │ 右上:影响多大? │
│ SLO 燃烧率 / 错误率 / 延迟 │ 流量、受影响租户/接口、错误分类│
├─────────────────────────────┼─────────────────────────────┤
│ 左下:什么时候开始的? │ 右下:为什么? │
│ 时间线 + 变更事件标注 │ 依赖、资源、上游/下游 │
└─────────────────────────────┴─────────────────────────────┘具体到版式,几条硬约束:
- 一屏装下核心信息,滚动是例外。滚动本身就是认知成本
- 2~4 列,不要 6 列。小屏上 6 列会折叠成一列乱序
- 时间轴必须对齐:同一面板里所有子图共享时间范围(Grafana 的
Shared crosshair+Shared tooltip打开),否则你要来回看三张图拼时间 - 把变更事件标在时间线上(Grafana Annotation,来源可以是发布系统的 webhook)。80% 的"什么时候开始的"问题,答案是一个发布事件
- 移动端:一个 6 列布局在手机上会变成一条长长的竖线。至少保证 L1 面板在手机上是可读的两列布局——on-call 有很大比例的第一次判断是在手机上完成的
关于大屏:墙上那块的用的面板,和 on-call 用的面板是两个物种。大屏要的是"一眼看见异常就够",字号 40px、只有 4 个数字;它不需要图例、不需要精确数值、不需要交互。别把 on-call 面板投到墙上,也别把大屏当工作台。
九、原则 7:面板要有生命周期——会过期、要评审、能删除
反面
三年积累 300 块面板。没人知道哪块还在用。想删一块,会有人说"这个好像有人看"。
正确
把面板当代码管,并且给它定死亡时间。
1. 面板即代码(Provisioning)
# provisioning/dashboards/provider.yaml
apiVersion: 1
providers:
- name: sre-dashboards
folder: SRE
type: file
disableDeletion: false # 关键:允许删,否则只增不减
updateIntervalSeconds: 30
allowUiUpdates: false # 关键:线上不准手改,改就走 MR
options:
path: /var/lib/grafana/dashboardsallowUiUpdates: false 是有争议的,但它解决了一个真实问题:手工修改的面板无法评审、无法回滚、无法追溯"谁在什么时候把它改坏了"。想调图?改 JSON 提 MR,顺带就是一次评审。
2. 观测数据反哺——让面板自己证明没人看
Grafana 企业版有 usage insights,开源版可以退一步:在反代/Ingress 层按 dashboard 路径打 access log,统计每个 /d/<uid> 的访问次数和访问者。跑一个月,你会得到一张很扎心的表:
| 面板 | 30 天访问 | 独立访问者 | 是否存在对应告警 |
|---|---|---|---|
| K8s Cluster Overview | 3 | 2 | 是 |
| payment-api SLO | 184 | 11 | 是 |
| 某中间件历史面板 | 0 | 0 | 否 |
0 次访问 + 无关联告警 = 可以删。
3. 评审 Checklist(放进 MR 模板)
新建或修改面板的 MR 必须勾选:
- 面板有
title和一句话description(写清"回答什么问题") - 目标面板有 owner(写进 tags 或 description)
- 每条曲线有基线或阈值,纵轴已明确固定/自适应
- 所有面板有单位和小数位
- 颜色和告警阈值同源,无"孤立的红"
- 默认时间范围和告警的
for时长匹配 - 从 L1 到这块面板的跳转路径存在(不超过 3 跳)
- 在手机宽度下看一眼,L1 可读
4. 定期清理
季度一次,半天时间。按"访问次数 + 有无告警 + 有无 owner"三个维度排序,从尾部删。删之前把 JSON 归档到一个 graveyard/ 目录——能恢复,所以敢删,这是清理能持续推进的关键。
十、一套可直接抄的面板清单
前面 7 条原则讲完,落到"我们到底要建哪几块"。不要多,四类就够:
| 面板 | 观众 | 回答的问题 | 关键内容 |
|---|---|---|---|
| L1 服务 SLO 总览 | on-call | 现在有没有事,是不是我的事 | 燃烧率(1h/6h/3d 多窗口)、错误率、P99、流量、错误预算剩余 |
| L1 全局健康页 | 值班/大屏 | 全站有没有事 | 各服务一盏灯(红黄绿)、按影响排序、直达链接 |
| L2 服务下钻 | on-call + owner | 哪里坏了 | 实例级、依赖级、错误分类、变更标注 |
| L3 资源与诊断 | owner / 平台 | 为什么坏 | Pod/节点资源、Loki 日志、Trace 查询、网络 |
L1 的燃烧率可以这样写(多窗口,短窗口抓急性、长窗口抓慢性):
# 急性:1h 烧掉 5% 预算(阈值 14.4x)
sum(rate(http_server_request_duration_seconds_count{service="payment-api",code=~"5.."}[1h]))
/ sum(rate(http_server_request_duration_seconds_count{service="payment-api"}[1h]))
# 慢性:6h 烧掉 5% 预算(阈值 6x),与上式取 and漏了 and 这一半、只配长窗口,是燃烧率面板最常见的错——慢性问题很好看,急性爆掉反而看不见。第 3、4、5 篇里讲的多窗口燃烧率规则,和这里是同一套数据,只是展示位置不同。
十一、一次真实改造:27 块面板 → 6 块
背景:一个 40 人研发团队,Grafana 里 27 块面板、约 900 个标的图,on-call 平均在面板上停留 11 分钟(从点开到查清或放弃)。
改造动作,按顺序:
- 导出全部面板 JSON,拉一张表:每块的访问量、有没有 owner、有没有对应告警
- 一刀切归档:27 → 9。14 块面板"无访问 + 无 owner + 无对应告警",直接进
graveyard/ - 重画 L1:把原来的
K8s Cluster Overview拆成两块——全局健康页(各服务一盏灯)+服务 SLO 总览(带 service 变量) - 锁纵轴、加基线、加阈值线:延迟面板固定 0–1s,叠加 7d 基线,画出 SLO 红线
- 打通跳转:所有告警
runbook_url指向 L1,L1 每个分区可点进 L2,跳转带service/pod/from/to - 面板入库:全部进 Git,关掉
allowUiUpdates,MR 加 Checklist - 两周后复盘:让 on-call 在每次值班结束时填一行——"这次有没有在面板上找到答案,没找到是因为缺什么"
结果(三个月后统计):
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 面板数量 | 27 | 6(+3 块归档待删) |
| on-call 在面板上平均停留 | 11 min | 3.5 min |
| 面板跳转中位数(每处理一次告警) | 5 次 | 2 次 |
| "看不清 / 找不着,直接登机器"占比 | 43% | 12% |
| 因"看错面板判断"导致的升级/误报 | 7 次/季 | 1 次/季 |
最后一个数字值得单独说:面板设计的改善,直接减少的是误升级,而误升级的代价不是时间,是信任——一晚上叫错两次人,值班表就排不下去了(第 6、8 篇讲的是同一件事)。
十二、给不同角色的一句话建议
| 角色 | 建议 |
|---|---|
| 一线 SRE | 下次被叫醒后,记下你在面板上每一个"这里到底是什么意思"的瞬间,那就是下一个 MR 要改的地方。 |
| 平台工程师 | 面板即代码 + 访问统计 + 默认带 owner,这三件事能把"只增不减"变成"有进有出"。 |
| 后端开发 | 你的服务面板上应该有且只有一条红线,它必须对应你亲手订的 SLO。别的颜色都是可选项。 |
| 技术决策者 | 面板不是美术,是 on-call 的作业工具。评估它别问"好不好看",问"新人半夜能自己判断吗"。 |
十三、和前面几篇的关系
- 第 7 篇(可观测性全景图):全景图解决了"数据从哪来",本篇解决"数据怎么变成第一次判断"。三支柱的消费端,最终都收敛在面板上
- 第 12 篇(OpenTelemetry):指标规范和语义约定(
http_server_request_duration_seconds这类命名)决定了面板能不能跨服务复用。命名混乱,面板必然千奇百怪 - 第 14 篇(eBPF 网络可观测性):网络层的结论(谁把请求搞慢了)不该只有在出事时才去查,把关键结论固化成一个面板分区,是它最重要的复用方式
- 第 3、4、5、10 篇(SLO):L1 面板的全部数字都应该来自 SLO 体系——燃烧率、错误预算剩余是唯一有资格"变红"的指标
- 第 6、8、13 篇(告警与 on-call):告警负责"叫人",面板负责"给判断"。两者必须通过
runbook_url串起来,否则每次都从零排查 - 第 16 篇(成本优化):成本效率面板同样适用这 7 条。一张只有 40 条 request 曲线、没有预算线、没有对比基线的成本面板,只能制造焦虑,不能带来动作
写在最后
这 7 条原则,如果只能带走一条,我选原则 2:
没有基线的曲线,等于没有信息。
因为它是唯一一条能让存量面板立刻变好的改动——不需要新建、不需要删、不需要提案。你只需要在每个自适应的纵轴上,把范围钉住,再加一条 7 天前的曲线或者一条 SLO 阈值。一个下午,任何一块旧面板的质量都会翻倍。
而如果只能带走一个观念,是这个:面板不是"给数据的看板",是"给决策的界面"。你为它做的每一处收敛——限定一个主角、钉住一个纵轴、砍掉一层跳转、删掉一块没人看的面板——都在替凌晨两点的那个陌生人省下 30 秒。
而 SRE 这行,很多时候就是靠这 30 秒撑着的。
本文是 SRE 实战系列第 17 篇。至此可观测性这条线走完了完整闭环:第 7 篇(全景图,Pillar)→ 第 12 篇(采集层 OTel)→ 第 14 篇(网络层 eBPF)→ 第 3/6/13 篇(度量与告警链路:第 6、13 篇讲告警治理与分级)→ 本篇(展示与决策层)。
下一篇预告:SRE 成熟度模型——你的团队在第几级,以及每一级怎么往上跳。