19  观测器:测不到的状态怎么管

小节 9.6 留下了一个没解决的问题:一条定时任务挂了四个月, 没有人发现,因为它坏了不产生任何可见后果。 这一章讲这个缺口该怎么补,而答案不是”加监控”

19.1 测不到的状态,控制不了

控制论里有一条硬结论,它不是经验之谈,是一个定理: 一个你无法观测的状态,你无法控制。

“无法观测”有精确的含义 —— 不是”没装传感器”, 而是从你能测到的全部输出,无论怎么组合、观察多久, 都推不出这个状态的值。翻译过来就是那句话: 一个不产生判定的动作,坏了你不会知道。

这句话还有一个没被说出来的下半句:不可观测的子系统 同时也是不可控的,所以你对它的任何”修复”,本身也是不可验证的。 那个挂了四个月的崩溃后来被修了,但在没有观测器的情况下, 没有任何人能知道它会不会再挂四个月 —— 修复和不修复,在可观测的世界里长得一模一样。

这一点比”没发现”更严重。没发现只是一次损失, 而不可验证意味着这个损失会重复, 而且重复的时候你同样不会知道。

19.2 三个实例,同一个缺陷

签名自动刷新挂了四个月,因为它失败不产生红灯; 28 条工作流里有 5 条从未运行过,因为它根本没有状态; 磁盘容量问题重复了五次,因为没有人测量分区水位。 三个实例的共同点是它们都不产生红灯。

代码有三层判定盯着,每一次违反都会立刻变成一个信号, 而这些东西一层都没有 —— 它们是给 Agent 生产确定性的东西, 自己却在确定性覆盖之外。

第二个实例尤其值得看。“从未运行”这个状态连一条失败记录都不会产生, 监控作业状态永远发现不了它,因为它根本没有作业实例可供监控。 你只能通过它应该产生而没有产生的东西来发现它—— 而这正好就是观测器的定义。

19.3 标准答案不是”加更多传感器”

面对”状态测不到”,工程上的第一反应总是”那就加个监控”。 控制论给的答案不是这个,它的答案是建一个观测器: 用一个模型,加上你能测到的量,去估计你测不到的状态。

flowchart LR
  X["测不到的状态<br/>(刷新机制健康吗)"]:::hidden
  X -->|"如果它真的在工作"| Y["可测的痕迹<br/>(证书到期日)"]
  Y --> O["观测器<br/>读一个数,比一个阈值"]
  O --> V["一个可以进 if 的估计值"]
  V --> J["判定"]
  M["直接监控作业状态"] -.->|"作业可以成功却什么都没干"| X
  classDef hidden stroke-dasharray: 5 5

这个东西在控制里有具体的名字(状态观测器、卡尔曼滤波器), 但它的核心思想很朴素:如果这个状态真的在影响世界, 那它一定在某个你能测到的地方留下了痕迹 —— 去测那个痕迹。

19.3.1 用在那个签名任务上

具体到那条定时任务:你不需要监控这个任务本身。 你已经有它应该产生的效果的模型 —— 刷新成功意味着证书的到期日往前移, 而那些证书是签入仓库的,状态就在手边,一条命令就能读出来。

于是观测器长这样:读出最早的那份证书距离到期还有多少天, 大于阈值说明刷新机制在工作,小于阈值说明它没在工作 —— 不管那个任务自己报告了什么。

从效果估计健康度比监控作业本身更鲁棒,原因有三条。 一个作业可以”成功”却什么都没干:那 5 条从未运行的工作流是极端情况, 但”跑完了、返回成功、实际没处理任何东西”是一个更常见、更隐蔽的形态。 观测器不关心实现:哪天这个刷新换了一种做法、换了一个工具、 拆成了三个任务,观测器不用改,因为它测的是效果。 观测器覆盖你没想到的失效方式:监控作业状态只能发现”作业失败了”这一种, 而观测证书到期日能发现所有导致证书没被刷新的原因, 包括你从来没想过的那些。

19.3.2 用在磁盘那五次上

这是观测器价值的最好例证。五次事故的表象各不相同 —— 登录失败、CI 任务失败、全站错误、监控面板 500、代理 502; 根因也各不相同 —— 缓存文件损坏、容器日志、日志插件缓存、 没配保留期、保留期配了但和容量不匹配。 但它们的可观测量是同一个:分区水位。

这就是观测器的价值:它不要求你预测所有的失败模式, 只要求你找到一个所有失败模式都会经过的可测量量。 五个不同的根因,一个观测器全覆盖 —— 而且它还能覆盖第六个、第七个你还没遇到的根因, 只要它们同样表现为磁盘满。

这个性质值得单独强调,因为它和”针对每个根因加一条检查” 是两种完全不同的策略。后者的成本随失败模式的数量线性增长, 而且它永远落后一步(你只能防你已经见过的); 前者是一次性的,而且它对未来的失败模式一样有效。

19.3.3 构造一个观测器的通用方法

三步,任何组件都适用。

第一步:写下这个组件”在正常工作时,世界上会有什么不同”。 注意不是”它会返回成功”,而是外部世界的哪个可观测量会改变。 如果你写不出来,那么这个组件可能本来就没在做事 —— 而这个发现本身就抵得上这十分钟。

第二步:找到那个不同里,可以被便宜地测量的部分。 便宜很重要,因为一个需要跑一小时才能算出来的观测器不会被跑。

第三步:断言它 —— 断言效果,不是断言执行。

这三步的顺序不能换。大部分人从第三步开始 (“我们给这个任务加个告警吧”),于是断言的永远是执行, 而执行恰恰是最不可信的那个信号。

19.4 为什么这不叫”加监控”

区别不是措辞。监控回答的是”这个组件现在怎么样”, 它的输出是一个仪表盘;观测器回答的是”这个我看不见的状态现在是多少”, 它的输出是一个估计值 —— 而估计值可以被断言。

摊开来看,四处差别:监控的输出是面板上的一条曲线, 观测器的输出是一个可以进 if 的数;监控由人主动去看, 观测器由机器自动判定;组件换实现时监控要跟着改,观测器不用; 监控不覆盖未预见的失效,观测器覆盖。

最要紧的是”谁消费”那一条,因为它决定了这个信号 会不会在你需要它的那天真的起作用。

还有第五处差别,它其实是前四处的根源: 监控是给人看的,观测器是给判定用的。 这决定了它们的优化目标完全相反 —— 监控优化信息量, 尽可能多地展示;观测器优化可判定性,一个数、一个阈值。 一块新面板的边际成本接近于零(没人看也不会怎样), 而一个新观测器的边际成本是真实的: 它会产生告警,会误报,会需要被调阈值。

所以观测器应该少而准,而这和”加监控”的直觉正好相反。 具体到那 28 条定时任务,正确的做法不是给每一条建一个观测器, 而是找出它们共同的效果,用尽可能少的观测器覆盖尽可能多的任务 —— 小节 19.3.2 那个”五个根因,一个观测器”就是这个思路。

19.5 没人看的看板等于没有判定

这一节是这一章、也是我对单人和小团队最重要的一条建议。

需要被注意的东西在快速增长:28 条工作流、23 条规则、20 份路径清单、 45 份自动化配置、6 条检查通道、260 个工作区。注意力是常数。

所以正确的设计目标不是”增加监控面”—— 增加监控面在注意力恒定的前提下,等于降低每一块的被注意概率, 这和 小节 8.3 讲的常驻文件变长是同一个机制。 正确的目标是:让不需要注意的东西不出现在注意力里, 让需要注意的东西主动找上门。

具体到那 28 条定时任务,正确的补法不是加一块面板,而是 任何连续失败 N 次的定时任务,直接开一个改动请求。 这样做的理由是改动请求是这个系统里已经有判定覆盖的东西 —— 它会进队列、会被看到、会有人(或 Agent)去处理, 因为整个工作流已经围绕它建起来了。一块新面板不进任何队列, 它需要一个新的习惯来消费,而习惯是这个系统里最不可靠的部件。

一个信号的价值,取决于它汇入的那条已有的处理流。 这条原则可以推广:给一个新信号找归宿时, 不要问”怎么让人看到它”,要问”它能不能变成一个已经有人处理的东西”。

19.6 这条缺口现在有多大

量一下。有判定覆盖的是约 310 万行代码、三层检查、四道证据门, 每一次改动都被扫描。没有判定覆盖的是 28 条工作流、 近四个月 18 万次执行,以及它们背后的全部基础设施。 而后者是前者赖以运行的地基。

这不是一个小缺口,它是这套系统目前最明显的一处, 而且它有一个很坏的性质:因为它不可观测,所以它也不可估量—— 上面这段话里唯一可靠的数字是”18 万次执行”, 而”其中有多少是无效的”这个问题现在答不上来。 一个连规模都说不清楚的缺口,没法排优先级, 而没法排优先级的事情通常不会被做。

19.7 观测器的阈值该怎么定

建一个观测器之后紧接着的问题是阈值 —— “证书距离到期少于多少天算不健康”这类数字。 这个数定错了,观测器要么天天误报(然后被静音), 要么永远不报(然后等于不存在)。

有一个不依赖直觉的定法:阈值应该等于 “从发现到修好”所需的时间,再乘一个安全系数。 如果那个刷新机制坏了,你需要三天才能有人注意到、 诊断、修好并重新跑一次,那么阈值至少是三天 —— 而在证书这个例子里,还要加上”修好之后新证书生效需要多久”。

这个定法的好处是它把一个感觉问题变成了一个可以问出来的问题: 这件事坏了,从告警响到它被真的修好,要多久? 这个问题的答案通常比人以为的长得多 —— 它包含了周末、包含了那个知道怎么修的人正在休假、 也包含了第一次尝试修错了的那一轮。

它还有一个副产品:如果算出来的阈值大得离谱 (比如证书总共只有 90 天有效期,而你算出来要 60 天), 那说明问题不在阈值上,在修复流程上。 这时候正确的动作不是把阈值调小碰运气, 是把修复流程缩短 —— 或者干脆承认这个东西 需要一个不依赖人的自动恢复。

一个定不出合理阈值的观测器, 是在告诉你一件关于这个系统的事,不是在为难你。

19.8 观测器和判定的关系

这一章讲的观测器,和前面三层判定是什么关系? 观测器是把”不可判定的东西”变成”可判定的东西”的那一步。

前三层判定都有一个前提:被判定的对象是可观测的。 一次代码改动是可观测的 —— 它有 diff,有构建产物,有测试结果, 所以判定可以直接施加在它上面。一个定时任务的健康度不可观测, 所以它不是”缺一层判定”,是”判定的前提不成立”。

这个区分决定了修法:缺判定就加一道检查, 缺可观测性就得先建观测器,再加检查,而顺序不能反。 在一个不可观测的状态上加检查,你只能检查那些能测到的东西 —— 也就是作业本身的状态 —— 而那正好是最不可靠的那个信号, 因为一个作业可以成功却什么都没干。

19.9 三个层次的可观测性

不是所有”看不见”都是同一种看不见,分三层,修法完全不同。

第一层是能测但没人测,最简单的一种。分区水位属于这一类: 数据一直在那儿,一条命令就能读,只是没有人去读。修法是读它,并且断言它。

第二层是测不到但效果可测。 定时任务的健康度属于这一类: 任务本身的成功与否不可信,但它的效果(证书到期日)是可测的。 修法是建观测器 —— 测效果,不测执行。

第三层是连效果都不可测。 比如”这个功能到底有没有用户在用”, 如果你没有埋点,那么它在任何意义上都不可观测。 修法不是观测器能解决的,得先建测量本身。

大部分团队以为自己在第三层,实际上在第一层。 这个误判很常见,因为”我们缺监控”听起来比 “我们没去读已经有的数据”更像一个正当的理由。 实际检查一遍通常会发现:需要的数据大多已经存在, 只是从来没有人把它接进任何一条判定。

19.10 反例:观测器也会骗人

观测器是一个模型加测量的组合,而模型可能是错的。

如果你用”证书到期日”作为”刷新机制健康”的观测器, 那么你隐含地假设了只有那个刷新机制会改变证书到期日。 如果有人手动刷新过一次,观测器会显示健康 —— 而机制仍然是坏的。

所以观测器本身也需要 小节 18.2.4 那种主动自检: 偶尔停掉那个机制,看观测量会不会如期恶化。 这在生产系统里通常做不到(没人愿意故意让证书过期), 但它至少应该被知道 —— 一个建立在未经验证的模型上的观测器, 提供的是”很可能健康”,不是”健康”。

这个区别在写进判定的那一刻会消失,因为判定的输出是布尔的。 所以模型的不确定性必须被记在别处:记在观测器旁边的注释里, 作为下一个读到它的人的背景 —— 这和 小节 13.4.2 那条”每条豁免都写理由”是同一条纪律, 守的都是”半年后的人能不能理解这行代码在假设什么”。

19.11 观测器的四种常见形态

给一些可以直接套用的模式。

19.11.1 形态一:新鲜度

问的是这个东西最后一次更新是什么时候。 适用于任何”应该被定期更新”的东西 —— 证书、缓存、索引、快照、生成物。 实现就是读一个时间戳,断言它在阈值之内。 这是最便宜的一种观测器,而且它覆盖了大量”任务静默失效”的场景。

19.11.2 形态二:单调性

问的是这个应该只增不减的量,减了吗。 适用于累计计数、版本号、数据行数、覆盖的目标数。 小节 13.4.3 那个哨兵就是这个形态的一个特例, 而 小节 18.5 建议的”相对变化率”版本更是。

19.11.3 形态三:守恒

问的是进去的和出来的对得上吗。 适用于任何管道 —— 消息队列、数据管道、批处理。 这个形态的强大之处在于它不需要知道中间发生了什么: 上游发了一万条,下游应该收到一万条,对不上就说明中间某处有问题, 不管那个问题是什么。形状 E(边界处的静默降级) 几乎总能被这个形态抓到,因为静默降级的本质就是 “进去的和出来的不一样,而没有人在比”。

19.11.4 形态四:因果

问的是如果 A 真的发生了,B 应该也发生。 适用于任何”做了一件事应该留下痕迹”的场景: 刷新了证书,到期日应该往前移;发布了版本,应该有一个对应的构建产物; 处理了消息,位点应该前进。

这是最强的一种,也是最需要动脑的一种—— 因为你得先想清楚”如果它真的工作了,世界上会有什么不同”, 也就是 小节 19.3.3 的第一步。

19.11.5 四种形态的覆盖能力

四种形态各有各的盲区:新鲜度能抓到完全停止工作, 抓不到工作了但做错了;单调性能抓到突然的退化,抓不到缓慢的退化; 守恒能抓到中间的丢失,抓不到中间的错误转换; 因果能抓到做了没做,抓不到做了但做得不对。

四种全都抓不到”做了但做错了”—— 那需要的是内容层面的校验, 而那属于判定,不属于观测。观测器解决的是”我看不见”, 判定解决的是”它对不对”,先有前者才谈得上后者。

19.12 为什么这个缺口在这套系统里特别刺眼

这套系统对代码的判定覆盖做到了很高的程度:三层判定、四道证据门、 每条规则带哨兵、每次判定绑定版本。生产这些判定的东西, 一层覆盖都没有。

这个对比之所以刺眼,不是因为它是一个疏忽, 而是因为它精确地符合我自己讲的那条规律: 判定覆盖到哪里,纪律就执行到哪里。 同一个人,在有判定覆盖的地方做到了极致, 在没有覆盖的地方,问题重复了五次、崩溃挂了四个月。

这不是能力问题,也不是态度问题。 这是全书最重要的一个论据,而它是反直觉的 —— 你以为你在靠自律维持质量,实际上你在靠判定维持质量, 而在没有判定的地方,自律的实际效力接近于零。 证据就是同一个人在两侧的表现差异, 而这个对照组的干净程度是任何跨团队比较都做不到的。

19.13 这个缺口该由谁来补

一个实际的问题:观测器本身也是代码,那它需要判定覆盖吗? 答案是需要,但不能无限递归 —— 而终止这个递归的方式是让观测器足够简单, 简单到它的正确性可以被一眼看出来。

小节 19.11 里那四种形态之所以有价值, 正是因为它们都很简单:新鲜度是读一个时间戳比一个阈值, 单调性是比两个数,守恒是比两个计数,因果是断言一个存在性。 四种都是几行代码,都没有分支逻辑,都没有配置。

这是刻意的:一个复杂的观测器需要它自己的观测器, 而那条递归没有终点。所以观测器的设计目标不是”准确”, 是”简单到不需要被验证”—— 一个粗糙但显然正确的观测器, 比一个精确但需要维护的好,因为后者会腐化, 而它腐化的时候,你会以为你还有覆盖。

19.14 补这个缺口的最小动作

对任何一个定时任务或自动化,做三件事,总成本大概半小时。

写一句话:它在正常工作时,世界上什么会变。 写不出来就说明这个任务可能本来就没在做事,先查这个。 找到那个”变”里最便宜的可测量部分,通常是一个时间戳、 一个计数、一个存在性。把它接进一个已经有人处理的流—— 不要建面板(小节 19.5), 让它变成一个改动请求、一个工单、或者一条会被看到的消息。

第三步最容易被做错,因为它不是技术问题 —— 它是”这个信号最终会被谁看到”的问题, 而这个问题的答案通常在技术之外。

19.15 ⚙️ 小规模怎么做

把你所有的定时任务列出来,逐个问那三步。 第一步就会淘汰掉一批 —— 你会发现有些任务你根本说不出 它在正常工作时世界上有什么不同,而那本身就是结论。

给每个定时任务一个”效果断言”,哪怕很粗糙。 “最近 7 天内应该有至少一次成功的产物”这种级别的断言就够用了 —— 它不精确,但它能抓到”完全停止工作”,而那是最常见的一种失效。

让失败汇入你已有的处理流,不要新建一条。 如果你的团队盯合并请求,就让它变成合并请求; 如果你的团队盯工单,就让它变成工单。不要建面板。

19.16 一个自反的观察

最后一点,它有点绕但很重要:我讲的这整套东西, 本身也需要观测器。 具体地说 —— 你怎么知道你建的这套判定系统还在工作?

小节 15.13 那张清单是规则集的观测器, 小节 17.7 那三条曲线是回路的观测器, 小节 C.9 那三个指标是常驻文件的观测器。 三处,而它们在源系统里一处都没有被建起来。

这不是批评,它恰恰印证了这一章的核心论点: 判定覆盖到哪里,确定性就只到哪里; 而”判定系统自己”是最容易被漏掉的那一层,因为建它的人最相信它。

“最相信它”正是问题所在。 小节 12.9 里那个观察在这里再次出现:手艺最强的地方,验证反而最弱, 因为”我知道它对”会降低建验证的动力 —— 而这句话对一个人的代码成立,对一个人的基础设施同样成立。

19.17 练习:给你的定时任务填一张表

对你系统里的每一个自动化任务 —— 定时的、事件驱动的、 手工触发但很少被检查的 —— 填一行:

任务 它正常工作时,世界上什么会变? 那个”变”能被便宜地测到吗?

第二列填不出来的每一行,都值得立刻查一下, 因为”它在正常工作时世界上什么都不变”通常意味着它本来就没在做事。 第三列填”能”的每一行,都是一个成本极低的观测器: 读那个量,断言它在合理范围内,把失败接进一条已经有人处理的流。

十分钟填表,而它的产出通常包含一到两个 “原来这个东西早就不工作了”。