带 payments 团队和 critical 严重程度标签的告警进入根路由。支付路由匹配后继续检查后面的严重程度路由,选择 payments 与 pager 接收器。箭头表示按顺序选择,不表示已经送达。

Alertmanager 通过标签匹配树选择告警接收器

Alertmanager 根据告警标签遍历路由树,选择通知接收器。路由树表达责任归属与升级策略,接收器则定义实际通知集成。配置能够解析,并不代表每一类告警都被分配给正确目的地。因此,不能把重载成功当作路由有效的完成证据;应该先核对进入系统的标签契约,再验证整棵树针对典型告警的选择结果。 每个路由指定一个接收器,子路由继承未覆盖配置。先按顺序匹配并选择接收器,再单独验证送达链路。图示依据原文预期路由用例;未改动的 Docker/amtool 实验仍为受阻状态,本轮没有重跑。 既要看匹配,也要看何时停止 每条告警都会进入必须匹配所有告警的根路由。子路由继承没有主动覆盖的配置;没有子节点匹配时,由当前节点处理。否则,遍历通常在第一个匹配的子节点后停止。匹配节点配置 continue: true,才允许继续检查后续同级节点。这不表示配置中的所有接收器都会获得同一条告警,未匹配的节点仍然不会参与。 一个路由节点只能指定一个接收器。接收器内部可以包含多个通知集成,多条匹配路由也可以选择不同接收器,两者是向多个目的地通知的不同方式。不能把路由的 receiver 字段直接换成列表,就期待具有相同行为。选择方案时应核对是否会重复通知,以及不同目的地是否需要独立的分组方式、升级策略和发送节奏。 责任归属依赖稳定的标签契约 匹配 team="payments" 的路由,需要进入 Alertmanager 的告警真正带有这个标签值。名为 team 的注解不能满足该条件。Prometheus 表达式中的聚合可能提前移除一个维度,所以应该检查最终告警标签,包括告警重新标记的影响,不能假定原始指标上的标签会原样保留到通知阶段。原始指标存在团队字段,不足以证明路由可以看到它。 ...

十二月 8, 2027 · 2 分钟 · XB Field Notes
二百四十秒的高建议让六副本保持到四百秒;这条建议过期后,模型在五百四十一秒缩到两个。

水平自动伸缩按需扩缩容

HorizontalPodAutoscaler 根据配置的指标目标调整可伸缩工作负载。它的输出是期望副本数,不是“额外 Pod 已经接收流量”的保证。调度、镜像启动、初始化、就绪状态以及节点容量,都会决定这个建议多久才能转变成真正有用的服务能力。设计控制循环时,必须把这些延迟一起考虑,不能只根据一条公式估计恢复时间。 仅展示原文的算术与列表状态模型。历史 Python 模型执行仍为通过;未测试容差、速率策略、逐 Pod 处理、Kubernetes 协调或真实就绪容量。本次未运行样例。 指标产生的是副本建议 对于简单的逐 Pod 平均值,期望副本等于当前副本数乘以当前指标除以目标指标,再向上取整。比值为二时,在额外控制介入之前会建议翻倍。比值低于一可能建议缩容,但容差、缺失测量、就绪处理、上下限和扩缩行为,都可能阻止或限制最终动作。公式描述起点,不是所有条件下都直接生效的完整算法。 CPU 利用率以资源 requests 为分母。改变请求值,即使用掉的 CPU 没变,也可能改变利用率,所以纵向与横向伸缩需要协调。指标接口必须可用,目标还要支持 scale 子资源。HPA 修改 Deployment 的副本数量,再由 Deployment 控制器管理 ReplicaSet 与 Pod;这几层职责不同,不应把它们统称为一个瞬间完成的操作。 ...

十一月 4, 2027 · 2 分钟 · XB Field Notes
滚动三十天窗口移除最旧一天后,预算从负三百三十变为正九百二十。旁边的红色暂停标志仍然有效:示例策略中,事故未结束仍会暂停非必要功能发布。

错误预算是真预算

错误预算把可靠性约定变成能够参与排期的依据。产品与运维先确定目标,再度量失败是否超出允许范围,并提前约定预算耗尽后如何调整工作。这个数字支持共同决策,却不能预测明天的部署会不会引发事故。发布会引入风险,真正消耗已度量预算的是观察到的不良事件;不能把每次发布机械地扣成固定额度,也不能把余额充足当成变更必然安全的证明。 请求数量与判断分支来自原文合成样例,条形使用同一请求数比例。本图未执行代码,也未批准部署;事故关闭且余额为正的场景仅可进入发布审查。 先定义这本账如何计算 对于基于请求的目标,允许失败数等于符合统计条件的请求总数乘以一减目标值。例如,三百万次请求的目标为百分之九十九点九,便允许三千次不良请求。接入发布规则之前,需要写清请求范围、成功条件、测量位置和观察窗口。按流量加权的失败额度,不会自动变成一个固定的宕机分钟数;高峰和低谷的同样一分钟可能影响完全不同数量的请求。 滚动三十天的统计会持续移除旧记录并加入新记录。流量变化时,分母与允许失败数都可能变化,即使目标百分比没有改动。自然月统计则在约定的日历边界重新开始,两种窗口回答的问题不同。事故处理中临时换窗口,可能让看似精确的余额失去可追溯含义。因此应将计算定义与策略一起版本化,并在余额旁显示实际窗口、请求范围和统计截止时间。 在争论之前约定如何响应 一种示例策略是:预算超支时暂停非必要功能发布,把资源转向可靠性工作,同时允许紧急安全修复和事故修复经过审核后执行。需要明确决策负责人、例外所需材料、记录方式以及恢复普通发布审查的条件。谷歌公开的预算策略针对书中的示例服务,不能不加判断地把所有阈值和升级处理规则照搬到另一项业务。 预算余额也不能独立决定是否具备发布条件。事故仍未结束、监控覆盖不足、回归失败或缺少可靠回退方案,都可能继续阻止变更。反过来,修复当前事故的必要变更,不应只因为该事故已经花光预算而被机械阻挡。把判断及依据写下来,才能让规则服务于用户影响和工程取舍,而不是让仪表盘变红的团队承担未经分析的责任。 坏掉的样子 一个大事故离开滚动窗口后,预算可能突然恢复。计算本身没有错,但这不能说明根因已经消除。悄悄更改排除条件,把无法采集的样本填成成功,或者直接平均不同流量服务的成功百分比,同样会制造虚假的盈余。看到余额改善时,应先检查测量覆盖与定义是否变化,再判断服务状态是否确实改善,尤其不能把未知数据解释为零失败。 短窗口燃烧率提供另一种观察角度:把实际不良事件比例除以目标允许的不良比例,可以看出近期消耗速度是否异常。高燃烧率能够在长窗口预算耗尽前发出警告,但由它推算剩余时间,需要假设流量与错误行为继续保持可比,不能当成确定的倒计时。应结合长窗口余额、短窗口变化和低流量条件设置可执行的告警,而不是每出现一次失败就叫醒值班人员。 快速验证 把下面代码保存为 budget-policy.py,使用 Python 3 执行。数据是人为构造的每日请求统计,用来核对滚动窗口及一个刻意保持简单的策略。程序不会连接发布系统,也没有对任何真实变更的事故概率作出估计;运行输出只证明给定输入下的计算和判断分支。 from fractions import Fraction target = Fraction(999, 1000) # Every synthetic daily bin has 100,000 eligible requests. days = [(100000, 1300)] + [(100000, 70)] * 29 def account(window): eligible = sum(total for total, _ in window) bad = sum(errors for _, errors in window) allowed = eligible * (1 - target) return bad, allowed, allowed - bad def feature_policy(remaining, incident_open): return "hold" if remaining < 0 or incident_open else "eligible for review" before = account(days) after = account(days[1:] + [(100000, 50)]) assert before == (3330, 3000, -330) assert after == (2080, 3000, 920) assert feature_policy(before[2], False) == "hold" assert feature_policy(after[2], True) == "hold" assert feature_policy(after[2], False) == "eligible for review" for label, (bad, allowed, remaining) in [("before", before), ("after", after)]: print(f"{label}: bad={bad}, allowed={float(allowed):.0f}, remaining={float(remaining):.0f}, spent={float(bad / allowed):.2%}") print("after rotation, incident still open:", feature_policy(after[2], True)) print("after rotation, incident closed:", feature_policy(after[2], False)) 第一个窗口包含三千三百三十次不良请求,允许三千次,预算使用率为百分之一百一十一,余额为负三百三十。移除最旧一天、加入新一天后,不良请求降为二千零八十次,使用率约百分之六十九点三三,剩余九百二十次。示例特意让两个窗口都包含三百万次符合条件的请求,以便单独观察旧事故离开窗口的影响;真实系统的分母不一定保持不变。 ...

三月 25, 2027 · 1 分钟 · XB Field Notes
两个失败集合各有二十次请求,其中十次同时违反状态与延迟条件,因此失败并集是三十次,不能直接相加为四十次。

错误预算为允许的失败命名

“可靠性达到百分之九十九点九”还不是一项完整的工程约定。它针对哪些用户、在哪个位置测量、观察多长时间,又把怎样的结果视为成功?只有回答这些问题,错误预算才能把允许的失败变成可计算的数量。因此,首先要定义统计总体和成功规则,而不是先挑选仪表盘上的绿色阈值。两个页面展示相同的百分比,如果分母和判定不同,也不能直接比较。 图中采用原文模拟 HTTP 成功规则与准确集合计数。圆面积仅为示意,数量由标注、交集与算式表示。本次只阅读代码而未执行,也未证明生产采集完整。 统计真正承诺的用户体验 服务水平指标描述实际测得的量,服务水平目标则为它规定阈值与时间窗口。对于基于事件的指标,成功比例等于符合条件的成功事件数除以全部符合条件的事件数。成功可以要求一次业务操作正确完成,并且没有超过延迟上限。仅有正常的响应状态码,不能证明订单已经正确写入;服务器内部计时也可能没有包含用户实际经历的网络等待。 分母必须明确,例如某个接口收到的有效客户请求尝试,而文档约定的合成探针可以另行统计。还要事先决定重试、客户端取消、认证失败、超时以及异步完成如何表示。排除条件应来自服务承诺的边界,不能在事故后临时创造,用来美化结果。应保留足够的记录,分辨一条事件是按规则排除,还是因为采集失败而没有进入统计;这两者的含义完全不同。 一条失败可以同时触发多个条件 假设成功请求既要返回预期响应,也要在三百毫秒内完成。一个缓慢的服务器错误同时违反两个条件,但仍然只是一条不良请求。应计算失败条件的并集,或者直接对成功判定取反。如果把“超时延迟计数”和“错误状态计数”相加,而两者不是互斥类别,就会重复计入交叉部分,导致预算消耗被夸大。 对于目标值与符合条件的请求总数,允许失败数就是请求数乘以一减目标值,剩余预算再减去实际失败数。请求型预算统计的是事件,时间型预算统计的是服务处于指定状态的时长。三十天内百分之九十九点九的时间型目标,对应四十三点二分钟的不良时间;不能把这个换算直接套到任意流量下的请求统计。部分用户失败、高低峰切换和不同请求权重,都会破坏两种口径之间的简单对应关系。 滚动窗口还会随着旧事件离开而变化,即使没有新增失败。应在同一个窗口内重新计算额度与失败数,不能把终身累计扣款直接当成滚动余额。发布政策还必须说明负余额会触发哪些实际行动;计算本身既不授权继续制造失败,也不会自动阻止部署,这些决策需要事先约定。 坏掉的样子 只统计应用服务器日志,可能看不到到达应用之前就被拒绝的请求。只记录已经完成的请求,也可能遗漏仍然卡在队列中的请求。这样得到的成功率可以很好看,用户却已经无法使用服务。需要选择合适的观测位置,纳入超时结果,并展示覆盖程度或独立检查;不能把遥测缺失解释成服务成功,更不能靠丢掉失败样本让分母看起来干净。 如果窗口里没有符合条件的事件,成功比例没有可解释的分母,应标记为没有观察值,并另行处理监控缺失。请求量很小时,高目标也会对单次失败特别敏感,因为预算是一个数学额度,不一定恰好等于整数次事件。不要为了得到一个“至少允许失败一次”的结果而向上取整。跨实例比较时也必须保留同样的范围和请求权重,直接平均各实例的百分比会改变指标含义。 快速验证 把代码保存为 slo-population.py,用 Python 3 执行。2026-10-19 以 Python 3.12.14 完整运行原样程序并通过。保留图注中的未执行说明,描述的是此前的图形审核。这个模拟接口的规则是:只有状态二百且耗时不超过零点三秒才算成功。样本包含已完成的客户事件,以及明确标记为探针的独立事件,用来验证分类计算。它没有证明真实采集器的完整性,也没有把正常状态码等同于真实业务结果已经正确完成。 from fractions import Fraction # Synthetic events: (kind, status, duration_seconds). events = ([('user', 200, 0.10)] * 9970 + [('user', 200, 0.50)] * 10 + [('user', 503, 0.02)] * 10 + [('user', 503, 0.50)] * 10 + [('probe', 503, 0.50)] * 100) eligible = [(status, duration) for kind, status, duration in events if kind == 'user'] bad_status = sum(status != 200 for status, _ in eligible) too_slow = sum(duration > 0.30 for _, duration in eligible) bad = sum(status != 200 or duration > 0.30 for status, duration in eligible) good = len(eligible) - bad target = Fraction(999, 1000) allowed = len(eligible) * (1 - target) remaining = allowed - bad assert len(eligible) == 10000 and bad_status == too_slow == 20 assert bad == 30 and bad_status + too_slow == 40 assert allowed == 10 and remaining == -20 assert Fraction(good, len(eligible)) == Fraction(997, 1000) # The inclusive threshold is part of the synthetic contract. assert not (200 != 200 or 0.30 > 0.30) assert 200 != 200 or 0.300001 > 0.30 # Aggregation must weight each eligible event, not each instance equally. assert Fraction(999 + 0, 999 + 1) == target assert (Fraction(999, 999) + Fraction(0, 1)) / 2 != target assert 1 * (1 - target) == Fraction(1, 1000) print(f"eligible={len(eligible)}; good={good}; bad_union={bad}") print(f"status_failures={bad_status}; slow={too_slow}; wrong_sum={bad_status + too_slow}") print(f"SLI={good / len(eligible):.3%}; allowed={float(allowed):.0f}; remaining={float(remaining):.0f}") 样本里有一万次符合条件的客户事件,另有一百次探针被排除。客户事件中,状态不符合预期的有二十次,延迟超限的也有二十次,其中十次同时属于两类。因此失败并集实际只有三十次,成功事件为九千九百七十次;直接相加会错误地得到四十次失败。 ...

九月 17, 2026 · 2 分钟 · XB Field Notes