揭示数据管道中与实现细节紧耦合的告警在代码重构时频繁失效的模式,倡导基于结果而非原因的告警策略。
在几乎所有存在超过一两年的数据自动化代码库中,都会出现一个特定的模式:告警与实现细节紧密耦合,一旦有人重构下层代码,这些告警要么会悄悄停止触发,要么开始错误地触发。直到本应被捕获的告警在事件发生时保持沉默,才有人注意到。
这不是一个罕见的边界情况。在任何随时间推移而发生变化的系统中,这几乎是不可避免的,而每个值得维护的系统都在变化。问题不在于这种情况是否最终会发生在你的告警上,而在于你是通过受控审计发现它,还是通过一个被遗漏的事件发现它。
基于原因的告警会在特定的失败模式上触发:"这个 API 调用返回了 500","这个特定的重试计数器达到了最大值","这个特定的异常类型被抛出"。当你写下它们时,这些看起来很精确也很有用,因为它们直接与提示创建告警的错误或事件相关。
问题出现在后来。一旦有人改变了重试的实现方式、替换了 HTTP 客户端库,或者重构了错误处理来使用不同的异常层级,告警的底层触发条件就不再与现实相匹配。这种情况发生时,告警不会抛出错误。它只是悄悄停止触发,或者开始在错误的条件下触发,直到下一个事件没有被注意到,才有人发现。
重构代码和重构告警逻辑几乎从不由同一个人、在同一时间、带着相同的上下文来完成。一名工程师改进重试逻辑时,理所当然地关注于重试行为,而不是审计碰巧引用旧实现的每一条告警规则。告警配置通常存在于一个完全独立的系统中,一个监控平台的 UI、一个不同仓库中的 YAML 文件,与代码审查流程脱离,而正常的代码审查流程会捕捉这种漂移。
基于症状的告警监测可观察的结果,而不是实现细节:"这个表在预期的时间窗口内没有被更新","这个管道在过去 X 小时内没有成功完成","这个数据质量检查已经连续失败 Y 次"。这些条件描述的是对业务或下游消费者真正重要的东西,无论底层代码如何实现重试、错误处理或任务编排,它们都保持有效。
完全重写重试逻辑,迁移到不同的编排工具,交换底层数据库,基于症状的告警继续工作,因为它从一开始就不与实现耦合。
之前:"如果供应商 API 调用连续 3 次返回非 200 状态码以上,则触发告警。" 一旦有人添加了一个优雅处理这些故障的断路器,而不是以相同的方式表面化原始状态码,或者切换了具有不同错误语义的 HTTP 库,这个告警就会失效。
之后:"如果供应商数据表在预期的每日时间窗口内没有收到新批次,则触发告警。" 这样的告警能够存活于对摄入作业如何实际调用 API 的任何改变,因为它监测的是重要的结果,而不是机制。
告警配置通常完全存在于代码库之外,存在于监控平台的 UI 中或者一个单独的 YAML 配置仓库中,不是管道本身的常规 pull request 流程的一部分。这种分离意味着审查重试逻辑重构的审查者没有自然的提示去检查是否有任何告警规则引用正在被改变的代码。这两个系统独立地漂移,直到一个事件暴露了这个差距,才会强制它们重新同步。
一些团队通过将告警定义与他们监测的管道代码共存来解决这个问题,作为基础设施即代码,这样改变错误处理的 pull request 至少有机会触发审查者注意到一条引用相同逻辑的附近告警规则。这不是一个完全的解决方案,但它缩小了两个系统之间的距离。
基于原因的告警并不是从根本上就错了。它们对于在早期捕捉特定的、理解充分的失败模式,在它们级联成一个需要更长时间才能检测到的症状之前,是真正有用的。错误在于依靠基于原因的告警作为给定管道的唯一安全网。将少量有针对性的基于原因的告警与更广泛的基于症状的告警作为后备措施配对,可以捕捉到既定的、已知的失败模式和没人预料到的失败模式。
浏览你当前的告警规则,对于每一条都问自己,"这引用的是一个具体的实现细节、一个函数名、一个异常类型、一个重试计数,还是引用的是一个可观察的结果。" 第一类中的任何东西都是重构脆性审计的候选对象:从这个告警被写入以来,底层代码是否发生了改变,如果是这样,有没有人知道它是否仍然能正确地触发?
第一次做这个审计很乏味,但非常值得做,特别是对于那些数月都没人见过触发的告警。一个沉默的、破损的告警比根本没有告警更糟糕,因为团队相信他们得到了覆盖,而实际上他们并没有,这是告警疲劳问题的一个变种,其中失败模式是虚假的信心而不是噪音。
原因对症状的设计是一个更大的告警策略中的一个部分,这个策略还需要严重程度分层、去重和对虚假正报的定期审查来保持信任随着时间的推移。如果你是从头重建告警而不是修补单个规则,那么设计不会侵蚀信任的管道告警的更完整的分解涵盖了这个策略的其余部分。
标准化的可观测性工具,如用于仪器的 OpenTelemetry 和像 Grafana 这样的平台,用于针对一致的指标构建仪表板和告警规则,使得定义不需要预先了解实现细节的基于症状的告警变得非常容易,因为它们是建立在标准化的、业务级别的指标之上,而不是原始应用日志。
根据我们的经验,一个破损的基于原因的告警通常在触发重构后的数周到数月内不被注意,因为这个失败模式的全部要点是没有任何东西会出错,告警只是悄悄停止匹配现实。这个差距通常以两种方式之一关闭:要么一个基于症状的后备告警捕捉到底层问题,然后有人追溯发现特定的告警已经沉默,要么一个真实的事件发生,没有告警,事件后审查才最终暴露漂移。第一条路显然更可取,这正是配对基于原因的告警和基于症状的后备措施的论据,而不是单独依靠其中任何一个。
人们很容易认为一双新的眼睛在代码库上会在入职期间捕捉到陈旧的、基于原因的告警,但实际上往往相反。新团队成员通常相信现有的告警配置反映了当前的现实,因为没有明显的理由怀疑,审计每个告警规则是否与当前代码库相对应不是一个典型的入职任务。最可能已经悄悄失效的告警正是新员工最不有能力质疑的那些。
如果一条告警规则引用的是一个工程师只有通过阅读当前实现才能了解的东西,一个具体的函数、异常类型或重试机制,那么它在设计上就很脆弱,最终会在没人注意到的情况下沉默。围绕可观察的、业务相关的结果来构建需要在前期花一点更多的思考,但能够节省大量安静的、未检测到的告警衰退。137Foundry 在为数据团队重建告警时多次遇到过这个确切的模式,一旦真正被识别,几乎总是一个简单的修复。
若要进一步的行动,你可以考虑屏蔽此人和/或举报滥用