专业书籍精读 · SRE · 第 10 章
Site Reliability Engineering · Ch 10 · Google · 2016
你半夜下单、刷视频都能用,是因为有人在值班——手机响了就起床救火。这一章讲的不是「怎么救」,而是那个电话到底该在什么时候响:Google 怎么把几十万台机器上密密麻麻的数字,变成一条「现在必须叫人」的判断。
你大概以为做监控最难的是发现问题。真实世界里最难的是别乱响。一个响了一百次、九十九次没事的报警器,第一百次真出事时,值班的人已经学会不理它了——不是他懒,是人对狼来了就这个反应。所以这一章真正在解决的问题是:怎么让电话少响,还不漏掉真事。
早年的监控像给每台机器装一个烟雾报警器:挨个问「你 CPU 高不高」「磁盘满没满」,谁超标谁喊。机器少的时候挺好,几千台之后就两头不讨好——一次例行重启能让上百个报警器同时叫(其实没人受影响),而真正的坏事又常常单看每一台都「正常」:每台只有百分之一的请求出错,加起来却是一大批用户点不开页面。更要命的是这种报警器只会说「现在超标了」,问不了「上周同一时刻是多少」「这条线照这个斜率多久会撞墙」。
换了个思路:不让机器自己判断好坏,只让它们报数。
每个程序在自己门口挂一块小黑板,上面写着「我一共收了多少请求」「其中多少出错」「现在有多少人在排队」这类计数。监控系统每隔十秒挨家挨户去抄一遍,抄下来的不是「好/坏」,而是一串随时间变化的数字——像体温记录表,不是一句「他病了」。
抄回来的每条记录还挂着几张标签:哪个服务、哪个机房、哪台机器。于是「要不要叫人」就变成了对这些记录做算术:先把同一个服务几千台的数字加在一起(不再一台台看),再算成比例(出错的占总数的百分之几,这样机器多机器少都能比),最后加一条「得连续超标五分钟」——抖一下就自己好了的,压根不该叫人。
这一步的妙处在于:同一份抄来的数据,既能用来打电话,也能画成曲线看趋势,还能在排查时回头问「昨天这个点长什么样」。判断从「哪台机器不舒服」挪到了「用户是不是真的受影响了」。
抄数字太便宜了,于是大家什么都抄:几年后曲线多到没人说得清哪条真有人在看,而存这些曲线的开销、和翻找它们的成本,反倒成了新的麻烦。
好的告警不是「阈值设得准」,而是先把世界记成可以做算术的曲线,再让「该不该叫人」成为这些曲线上的一道算式——算式可以被审阅、被改、被复用,而一万个散落的阈值不能。
想看 /varz 埋点、标签时序、速率与比率的规则代数和示意图? → 切到精读版
本章的主张是:告警不该是「给每台机器设一个阈值」,而应该是「在一堆带标签的时序数据上求一个表达式」——取变化率、跨实例求和、化成比率、越线且持续一段时间。Google 的 Borgmon 是这套东西的实现,而它的思想已通过 Prometheus 变成了行业默认。本章题记是一句 SRE 的祝词:愿查询长流,呼机长静。
本章是 SRE 书 Part III「实践」的开篇。上承第 6 章《监控分布式系统》(该监控什么:四个黄金信号、只报症状)与第 4 章《SLO》(阈值从哪来),下启第 11 章《值班》与第 12 章《排障》(电话响了之后的事)。它是全书少有的「亮工具」的一章,对应的现实就是今天 Prometheus + Alertmanager + Grafana 这条标准栈。
「加监控」谁都会说,难的是规模一大,旧模型会从三处同时塌方。旧模型即以 Nagios 为代表的按主机做检查:对每台机器跑一段脚本,看退出码判定好坏,超标就喊。
① 数量爆炸。1000 台机器 × 每台 20 项检查 = 20000 个独立阈值。一次滚动重启,几百个检查同时变红,而用户毫无感知——这就是最典型的噪声来源。
② 不可聚合。每台只有 1% 请求失败、单看都在阈值内,1000 台加起来却是每秒上千个失败请求;反过来,一台机器 CPU 打满而流量早被负载均衡切走,对用户毫无影响。「哪台机器不舒服」和「用户有没有受影响」是两回事,而按主机检查只能表达前者。
③ 没有历史,只有此刻。检查脚本的产出是「好/坏」,不是一串数字。于是排障时最常问的三个问题都问不了:上周同一时刻是多少?照这个斜率多久撞线?这次是从哪一刻开始偏的?
不解决的后果不是「发现晚了」,而是告警体系整体失去可信度:噪声把真报淹掉,值班的人学会忽略。Google Cloud 团队后来把这句写得很直白:「如果什么都重要,那就什么都不重要。」
第一块地基是白盒埋点。Google 的每个服务二进制都带一个 HTTP 端点 /varz,访问它吐出一张纯文本清单:一行一个变量名加当前值。加一个新指标对开发者几乎是一行代码的事——「导出一个变量」的成本被压到接近于零,这是后面一切的前提,也是本章最容易被低估的设计决定;一个成熟的二进制导出成百上千个变量是常态。
关键在于程序并不知道监控系统的存在:它只把内部状态摊在门口,谁来抄、怎么算、算成什么告警全在外面决定。这就是「拉取」模型的真正好处——不是省几个网络包,而是把「采什么」和「怎么判断」彻底解耦:改告警口径不必动业务代码。
抓回来的每个数值存成一条时间序列,由「变量名 + 一组标签」唯一标识:job(哪个服务)、instance(哪个实例)、datacenter(哪个机房)。Borgmon 靠服务发现拿到实例清单再逐个抓,扩容缩容都不用改配置;每次抓取它还顺手合成几条自己的时序(这次抓取成不成功、花了多久),于是「采集本身坏了」也能被同一套规则告警——一个很容易漏掉的闭环。
数据放在内存里一块固定大小的 arena 中:每条时序一段环形缓冲,新点挤掉最老的点,同时定期落盘。为什么敢全放内存?算一下量级:10 秒采样、存 12 小时是 4320 个点,按每点连时间戳带值占二十几字节估也就 100 KB,十万条时序才 10 GB 量级。换来的是每十几秒能把全部规则重算一遍——告警的及时性是用内存买的。
标签这件事看着朴素,却是整章的分水岭:它让「一条规则管一整个服务」成为可能。你写的不再是「A 机器的错误率」,而是「job 等于 checkout 的所有实例的错误率之和」——实例数从 3 台涨到 3000 台,规则一个字都不用改。
Borgmon 的规则是一组对向量(所有匹配某条件的时序)做的算术表达式,周期求值,结果又写回 arena 成为新时序。它分两类:计算规则把原始数据加工成有意义的量(供看板和后续规则复用),告警规则在此之上加一条「越线」判断。
第一步:对计数器取速率。累计值本身没意义(「一共处理了 84 亿个请求」说明不了现在好不好),有意义的是它的斜率 rate(),即每秒多少。这里有本章一个反直觉的偏好:能用计数器就别用量表。量表(当前队列长度)每次采样只抓到一个瞬间值,两次采样之间的尖峰永远丢了;计数器把区间内发生的事都累进去,哪怕漏抓一次,下一次采样算出的速率仍然是对的。计数器对采样缺失免疫,量表不免疫——这句话值得背下来。(代价是任务重启会让计数器归零,求速率的实现必须识别它,否则会算出一个荒谬的负值。)
第二、三步:跨实例聚合,再化成比率。把同一 job 所有实例的错误速率与请求速率各自求和再相除,得到一个无量纲的错误比例。为什么一定要比率?因为它与流量规模无关:errors / requests > 0.01 在半夜三百 QPS 和高峰三万 QPS 时同样成立,而「每秒失败超过 50 个」这种绝对阈值必然要么白天误报、要么半夜漏报。延迟同理,要看分位数不看均值——耗时按桶导出再算 p99,否则少数极慢请求会被大量快请求平均掉。
第四步是持续时间。条件一瞬间成立不代表要叫人:网络抖一下、一次 GC 停顿都能让比率短暂越线。所以告警规则都带一个最小持续时长(Prometheus 里写成 for: 5m):必须连续满足才真正触发。这是本章最实用的小机制,也是一笔明账:5 分钟的安静换来 5 分钟的发现延迟,怎么算取决于错误预算——99.9% 的月度 SLO 只有约 43 分钟额度,5 分钟就吃掉九分之一。
触发之后,告警不直接变成电话,而是先进 Alertmanager:去重(同一问题在几十个实例上各触发一次,只发一条)、分组(相关告警合成一封)、抑制(上游成立时压住下游一串——整个机房断网,不该为里面每个服务各打一个电话)、静默(计划维护期间闭嘴),最后分流:必须立刻有人动手的才呼叫,其余转工单。这一层最常被忽略,却是把「规则触发数」压成「人被打扰次数」的地方。
一个 Borgmon 抓不完全球。解法优雅:Borgmon 自己也用同样的格式导出时序,于是上层 Borgmon 可以把下层 Borgmon 当成普通抓取目标。每往上一层只保留聚合后的结果,数据量逐级收窄——集群级 Borgmon 存细节、全球级 Borgmon 只看跨集群的总量。这就是今天 Prometheus 的「联邦(federation)」。
还有一块必要的补位:白盒看不见「用户其实连不上来」——DNS 错、负载均衡配错、证书过期,内部指标一切正常。所以 Google 另跑黑盒探测器(Prober),像用户一样定期发请求,结果也导成时序、走同一套规则。白盒说为什么坏,黑盒说用户是不是真的坏了,不可互相替代。
这一章的灵魂不是「用哪个工具」,而是你按什么口径判定「出事了」。四种口径并不互斥,真实系统常同时跑两三种,但代价各不相同。
| 告警口径 | 怎么判定 | 强在哪 | 代价 / 漏掉什么 |
|---|---|---|---|
| 单机阈值 | 每台机器一个检查,超标即报 | 直白,能定位到具体机器;硬件类故障合适 | 规模一大就是噪声之王(1000 台 × 20 项);表达不了「用户受不受影响」 |
| 聚合比率 (本章主张) | 跨实例求和后取比率,越线且持续 N 分钟 | 与规模无关、一条规则管全服务;直接对着症状 | 必须先有埋点与时序库;掩盖个别实例的异常(要另配分实例规则兜底) |
| 错误预算燃烧率 | 看当前坏请求速度会多快烧光月度预算 | 告警密度自动与业务容忍度对齐,噪声最低 | 需要成熟 SLO;慢烧故障发现较晚,要多窗口叠加(快烧 1 小时 / 慢烧 6 小时) |
| 黑盒探测 | 从外部像用户一样发请求 | 抓得到 DNS、负载均衡、证书这类内部指标看不见的故障 | 采样稀疏、只覆盖探测路径;说不出「为什么」 |
第二张表是一个几乎每次埋点都会遇到、又几乎每次都被搞错的选择。
| 计数器(counter) | 量表(gauge) | |
|---|---|---|
| 语义 | 只增不减的累计量 | 可升可降的当前值 |
| 怎么用 | 先取速率,再聚合 | 直接看值,或看最大 / 分位数 |
| 漏一次采样 | 不失真,下次算出的速率仍然对 | 那段时间的尖峰永久丢失 |
| 典型坑 | 任务重启归零,实现必须识别 | 对量表取速率,得到一个毫无意义的数 |
| 适合 | 请求数、错误数、字节数、耗时总和 | 队列长度、内存占用、连接数、温度 |
还有一条拍板判据可以直接用:一条告警值不值得存在,看「它响的时候,有没有人必须在这一刻做点什么」。答案是「能等到上班」就转工单,是「脚本能自动处理」就去写脚本。
这一章的分量在于:今天你用的监控栈几乎是它的直系后代。Prometheus 与 Borgmon 几乎一对一对应——/metrics 对 /varz、抓取对抓取、标签时序对标签时序、PromQL 的 rate() 与按标签聚合对 Borgmon 的规则代数、Alertmanager 对 Alertmanager。学会这一章,拿到的是 Prometheus、Grafana、Datadog、云厂商监控背后的共同心智模型。
后续发展也公开了:Borgmon 是每集群各跑一套的,配置与运维负担随集群数线性增长;2020 年 Google 在 VLDB 发表 Monarch 论文,公开了取代它的那套全球规模、多租户内存时序数据库——方法论对,形态会继续变。
① 告警不是给每台机器设阈值,而是在带标签的时序数据上求一个表达式;Borgmon 是实现,Prometheus 是它的公开继承者。
② 旧的「按主机做检查」在三处塌方:阈值数量爆炸、不可聚合(答不了「用户受影响了吗」)、只有此刻没有历史。
③ 地基是白盒埋点:程序在 /varz 摊出内部计数,导出成本接近零;它不知道监控存在,于是「采什么」与「怎么判断」彻底解耦。
④ 数据模型是分水岭:变量名 + 标签 唯一标识一条时序,于是一条规则能管一整个服务,实例从 3 台涨到 3000 台不用改规则。
⑤ 全内存 arena 换来「每十几秒把所有规则重算一遍」——及时性是用内存买的。
⑥ 判断是四步代数:取速率 → 跨实例聚合 → 化成比率 → 越线且持续;每步产物都是新时序,可被看板和其他规则复用。
⑦ 能用计数器就别用量表:计数器对采样缺失免疫,量表会永久丢掉两次采样之间的尖峰。
⑧ 比率 > 绝对值,分位数 > 均值:前者让规则与流量规模无关,后者让少数极慢请求不被平均掉。
⑨ Alertmanager 那一层(去重、分组、抑制、静默、呼叫与工单分流)才是把「规则触发数」压成「人被打扰次数」的地方;持续时间窗口是用发现延迟换噪声。
⑩ 规模化靠Borgmon 抓 Borgmon(层级联邦),用户视角靠黑盒探测补位:白盒说为什么坏,黑盒说用户是不是真的坏了。