告警路由主题封面,展示观察、判断与行动三个步骤。

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

Alertmanager 根据告警标签遍历路由树,选择通知接收器。路由树表达责任归属与升级策略,接收器则定义实际通知集成。配置能够解析,并不代表每一类告警都被分配给正确目的地。因此,不能把重载成功当作路由有效的完成证据;应该先核对进入系统的标签契约,再验证整棵树针对典型告警的选择结果。 既要看匹配,也要看何时停止 每条告警都会进入必须匹配所有告警的根路由。子路由继承没有主动覆盖的配置;没有子节点匹配时,由当前节点处理。否则,遍历通常在第一个匹配的子节点后停止。匹配节点配置 continue: true,才允许继续检查后续同级节点。这不表示配置中的所有接收器都会获得同一条告警,未匹配的节点仍然不会参与。 一个路由节点只能指定一个接收器。接收器内部可以包含多个通知集成,多条匹配路由也可以选择不同接收器,两者是向多个目的地通知的不同方式。不能把路由的 receiver 字段直接换成列表,就期待具有相同行为。选择方案时应核对是否会重复通知,以及不同目的地是否需要独立的分组方式、升级策略和发送节奏。 责任归属依赖稳定的标签契约 匹配 team="payments" 的路由,需要进入 Alertmanager 的告警真正带有这个标签值。名为 team 的注解不能满足该条件。Prometheus 表达式中的聚合可能提前移除一个维度,所以应该检查最终告警标签,包括告警重新标记的影响,不能假定原始指标上的标签会原样保留到通知阶段。原始指标存在团队字段,不足以证明路由可以看到它。 根接收器应当对没有匹配具体子节点的告警仍然有用。这类告警也需要可观察的去向,以及能够修正分类的负责人。测试时除了已知团队,还应覆盖标签缺失:缺少团队的警告可能留在根节点,但缺少同一标签的严重告警仍可能命中独立的严重程度路由。是否缺失与是否完全没有匹配路径,是不同问题。 路由排列顺序本身就是行为。较宽泛的前置路由,可能让后面的具体路由根本没有机会被检查,除非启用了继续匹配。因此,即使匹配器文字没有变化,移动一段配置也可能改变收件方。顺序调整应按策略变化审阅,并把代表性标签输入与配置放在一起,以便持续核对预期选择。 坏掉的样子 拼写错误或标签改名可能使告警落到根接收器,新添加的宽泛子路由可能截走后面团队的告警,而不必要的继续匹配则可能让一个事件产生多份通知。诊断这些问题,需要实际标签和完整匹配路径,单独阅读某个节点通常不够。还应核对新增规则与既有顺序共同作用后的结果,而不只是新增部分自身是否正确。 接收器选择正确仍不等于送达。分组等待、静默、抑制、模板错误、传输故障和目的地行为,都可能在之后介入。先验证路由,再沿所选接收器追踪通知链路。增加规则等待时间不会修复团队匹配条件,缩短分组等待也不能修复接收器凭据缺失。每个阶段都需要对应证据,不能用前一阶段通过代替后一阶段结果。 快速验证 把程序保存为 routing.py,执行 python3 routing.py。需要 Docker 与公开的 Alertmanager 0.28.1 镜像,镜像缺失时可能先下载。真实 amtool 检查配置并验证七组标签输入。实验接收器只有名称,没有实际发送集成,因此可以检查路由选择而不会联系任何人员或外部服务,也不会影响现有告警系统。 import subprocess import uuid # Requires Docker and Alertmanager 0.28.1. No receiver has a delivery integration. script = r"""set -eu cat > /tmp/routes.yml <<'ROUTES' route: receiver: default group_by: [alertname, service] routes: - receiver: payments matchers: ['team="payments"'] continue: true - receiver: pager matchers: ['severity="critical"'] receivers: - name: default - name: payments - name: pager ROUTES /bin/amtool check-config /tmp/routes.yml /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=payments,pager team=payments severity=critical /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=payments team=payments severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=pager team=data severity=critical /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default team=data severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default team=payments-preview severity=warning sed 's/continue: true/continue: false/' /tmp/routes.yml > /tmp/stop.yml /bin/amtool config routes test --config.file=/tmp/stop.yml --verify.receivers=payments team=payments severity=critical """ name = "xbcatm-route-test-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/alertmanager:v0.28.1", "-s"], input=script, text=True, capture_output=True, timeout=30, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: raise RuntimeError(result.stderr or result.stdout) print("Verified seven route cases, including continuation and unmatched labels.") 第一组结果为 payments,pager:支付团队路由先匹配并继续,严重程度路由随后也匹配。支付团队警告只选择支付接收器,数据团队严重告警选择呼叫接收器;没有匹配或缺少团队值的相应输入进入根接收器。payments-preview 不会匹配精确的支付团队值,避免把名称相近误认为条件成立。 ...

十二月 8, 2027 · 2 分钟 · XB Field Notes
依据目标指标调整工作负载副本数的控制器。

水平自动伸缩按需扩缩容

自动伸缩器根据当前用量与目标值推算出期望副本数,因此选对指标,往往比一开始把副本数定成多少更重要。 指标决定副本数 HPA 并不直接盯着负载看,它按周期采样你配置的指标,把实时读数与你设定的目标值比出比率,再由它推导出期望副本数:超过目标就扩容,不足就缩容,分母是当前 pod 的平均值。所以在它背后,选哪个指标、怎么聚合(按 pod 还是按集群)、以及取多长的窗口,才是真正决定「什么算负载」的东西。算出来的副本还会被 maxReplicas 与最小副本数夹住,并在默认同步周期里逐次校准。想让伸缩可预测,靠的是理解这条指标本身,而不是记住某一个常数。 扩容更激进、缩容更克制 缩容被刻意设计得不对称:控制器绝不会在利用率刚刚下降的那一瞬间动手,因为它要躲开瞬时尖峰,免得副本数像橡皮筋一样来回震荡。它会等一个稳定窗口(downscale-stabilization),守住 minReplicas 这条底线,然后才真正缩。扩容总是更快,因为排队那一刻用户已经感到了慢;缩容放慢,是因为留着多余副本远比让集群来回颠簸便宜。一套务实的参数是:下限留足真实尖峰的余量,目标值取一个能消化负载的比值,并把稳定窗口当成「我容忍多少负载噪声」的加注,而不是凭手感乱调的旋钮。 指标才是薄弱环节 整台控制器只会继承你喂给它的指标,指标怎样,伸缩器就怎样追。指标滞后于真实需求,控制器就总是在追赶慢半拍的数字;指标找一个与真正瓶颈相关性很差的代步量,伸缩器就是在缩错的东西。所以 HPA 最难的不是那几行 YAML,而是选哪个指标、定多少目标。给一条延迟敏感的服务配 CPU 目标、给 I/O 敏感的工作配内存目标,往往会让伸缩器朝反方向走。先搞清真正先饱和的是什么——队列深度、在途请求、连接占用——选择你可以在一个窗口内行动的平均值,并验证「当这个指标上升时,真实延迟确实跟着变坏」。到那时,HPA 才是伸缩你的流量的工具,而不是一台在废坐标上乱猜的机器。 坏掉的样子 指标一旧,伸缩就开始来回晃。管线延迟一两分钟时,控制器拿到的永远是上一波负载:尖峰过去,它才把副本加上去;加上去又被缩容稳定窗口按住,迟迟下不来。面板上不是跟随负载的曲线,而是一上一下的震荡。这时调目标值没有用,应当先修指标时延与采样窗口,再谈目标定多少。 指标直接缺失是更危险的一种。拉不到指标时,控制器会拒绝动作并报出 ScalingActive=False,这是保护而不是缺陷。最典型的故障是把“没有数据”当成“没有负载”:有人看到利用率掉零就手动缩容,结果把正在服务的容量一起删掉。正确姿势是把缺失当成未知,保持上一次决策,同时去查指标管线与事件流,而不是继续动副本数。 上下限会从另一个方向卡死控制循环。maxReplicas 当初为了省钱设得太小,真尖峰到来时期望副本被死死夹在上限,ScalingLimited=True 一直亮着,排队与超时却在上涨;minReplicas 设得太大,则会掩盖目标值估错的问题,因为底座托住了副本,你永远看不到需求下降后控制器敢不敢缩。最后不要让两个控制器拧同一个旋钮:HPA 管个数、VPA 改 request,请求值一动,利用率分母就变,两边互相追打;滚动更新太激进时,新 Pod 尚未接流量,老 Pod 仍在承压,平均值被稀释后误判为可缩容,副本走出锯齿。看到这种抖动,先把控制面分开排查,再调目标。 快速验证 把下面代码保存到临时目录的 hpa_loop.py,运行 python3 hpa_loop.py。它只用标准库模拟一条控制循环:期望副本等于当前副本数乘以当前指标除以目标值再向上取整,再用上下限夹住;指标缺失时保持上次决策;缩容必须等稳定窗口过期,否则先按住不动。脚本断言:热指标扩容、冷却期内冷指标不缩、窗口过期后可缩、超限被上下限夹住。 ...

十一月 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
符合统计条件的请求被分成成功事件与失败条件的并集。

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

“可靠性达到百分之九十九点九”还不是一项完整的工程约定。它针对哪些用户、在哪个位置测量、观察多长时间,又把怎样的结果视为成功?只有回答这些问题,错误预算才能把允许的失败变成可计算的数量。因此,首先要定义统计总体和成功规则,而不是先挑选仪表盘上的绿色阈值。两个页面展示相同的百分比,如果分母和判定不同,也不能直接比较。 统计真正承诺的用户体验 服务水平指标描述实际测得的量,服务水平目标则为它规定阈值与时间窗口。对于基于事件的指标,成功比例等于符合条件的成功事件数除以全部符合条件的事件数。成功可以要求一次业务操作正确完成,并且没有超过延迟上限。仅有正常的响应状态码,不能证明订单已经正确写入;服务器内部计时也可能没有包含用户实际经历的网络等待。 分母必须明确,例如某个接口收到的有效客户请求尝试,而文档约定的合成探针可以另行统计。还要事先决定重试、客户端取消、认证失败、超时以及异步完成如何表示。排除条件应来自服务承诺的边界,不能在事故后临时创造,用来美化结果。应保留足够的记录,分辨一条事件是按规则排除,还是因为采集失败而没有进入统计;这两者的含义完全不同。 一条失败可以同时触发多个条件 假设成功请求既要返回预期响应,也要在三百毫秒内完成。一个缓慢的服务器错误同时违反两个条件,但仍然只是一条不良请求。应计算失败条件的并集,或者直接对成功判定取反。如果把“超时延迟计数”和“错误状态计数”相加,而两者不是互斥类别,就会重复计入交叉部分,导致预算消耗被夸大。 对于目标值与符合条件的请求总数,允许失败数就是请求数乘以一减目标值,剩余预算再减去实际失败数。请求型预算统计的是事件,时间型预算统计的是服务处于指定状态的时长。三十天内百分之九十九点九的时间型目标,对应四十三点二分钟的不良时间;不能把这个换算直接套到任意流量下的请求统计。部分用户失败、高低峰切换和不同请求权重,都会破坏两种口径之间的简单对应关系。 坏掉的样子 只统计应用服务器日志,可能看不到到达应用之前就被拒绝的请求。只记录已经完成的请求,也可能遗漏仍然卡在队列中的请求。这样得到的成功率可以很好看,用户却已经无法使用服务。需要选择合适的观测位置,纳入超时结果,并展示覆盖程度或独立检查;不能把遥测缺失解释成服务成功,更不能靠丢掉失败样本让分母看起来干净。 如果窗口里没有符合条件的事件,成功比例没有可解释的分母,应标记为没有观察值,并另行处理监控缺失。请求量很小时,高目标也会对单次失败特别敏感,因为预算是一个数学额度,不一定恰好等于整数次事件。不要为了得到一个“至少允许失败一次”的结果而向上取整。跨实例比较时也必须保留同样的范围和请求权重,直接平均各实例的百分比会改变指标含义。 快速验证 把代码保存为 slo-population.py,用 Python 3 执行。这个模拟接口的规则是:只有状态二百且耗时不超过零点三秒才算成功。样本包含已完成的客户事件,以及明确标记为探针的独立事件,用来验证分类计算。它没有证明真实采集器的完整性,也没有把正常状态码等同于真实业务结果已经正确完成。 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) 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 · 1 分钟 · XB Field Notes