故障检测

本页目录

一个节点迟迟不回复,可能是进程崩溃、网络分区、CPU 长时间调度不到,也可能是观察者自己暂停了。故障检测首先描述观察者能获得什么证据,然后才是决定何时切换服务。

在消息延迟没有上界的异步模型中,有限时间的沉默不能证明对方已停止。这并不意味着不能构建系统,而是需要把“怀疑”“采取行动”和“保证安全”分开。

心跳与探测:两种取得证据的方法

心跳是被观察者主动发送存活信号;探测是观察者主动请求响应。两者都只能证明某条路径在某个时刻成功,不保证节点所有功能正常。

假设节点每 1 秒发一次心跳,观察者等待 3 秒后标记异常。较短阈值可以更早发现真故障,也更容易把短暂暂停判成故障;较长阈值减少误判,却延长接管时间。阈值应结合服务目标、延迟分布、调度暂停和观测成本,而不是固定要求超过某个分位数就永远正确。

节点 A 观察 B 失败,也不代表 C 观察 B 必然失败。故障检测结果带有观察者视角;把局部失败直接广播成不可争辩的全局事实,会放大局部网络抖动。

φ-accrual:把异常程度与行动阈值分离

设历史心跳间隔随机变量为 X,累计分布为 F,距离上次心跳已过 t。怀疑度可定义为:

若估计分布认为“还要等这么久”的尾部概率为 0.01,φ=2;若为 0.00001,φ=5。数字越大,当前沉默相对历史行为越异常。它不是“节点已死的后验概率”,后者还需要故障先验和更完整的统计模型。

检测器负责输出连续怀疑度,上层按不同风险选择阈值。例如移除一个负载均衡候选可以较敏感,触发昂贵的数据重建则可能要求更多证据。这个解耦比单个固定 alive/dead 开关更灵活。Akka 的 φ 检测说明 展示了基于历史间隔的实现思路。

分布估计也有边界:刚启动样本太少、长期负载变化、网络延迟多峰、观察者 GC 暂停,都会让旧统计失真。采样窗口、最小方差、启动宽限与允许的心跳暂停是模型的一部分,不能只抄一个 φ 阈值。

同样沉默 1200 ms,为什么一个观察者很紧张,另一个还在等

用正态分布近似历史间隔,平均间隔都是 1000 ms。链路 A 的标准差为 50 ms,1200 ms 已偏离均值四个标准差;链路 B 的标准差为 300 ms,同样的沉默还不到一个标准差。φ 把这两种背景下的异常程度区分开。

from math import erfc, sqrt, log10, isclose, inf

def phi(elapsed, mean, stddev, pause_margin=0):
    if elapsed < 0 or stddev <= 0 or pause_margin < 0:
        raise ValueError("间隔与宽限非负,标准差必须为正")
    z = (elapsed - pause_margin - mean) / stddev
    tail = 0.5 * erfc(z / sqrt(2))
    return -log10(tail) if tail else inf

stable = phi(1200, 1000, 50)
variable = phi(1200, 1000, 300)
assert 4.49 < stable < 4.51
assert 0.59 < variable < 0.61
assert stable > 2 and variable < 2
assert isclose(phi(1200, 1000, 50, pause_margin=200), log10(2))
assert phi(1300, 1000, 50) > stable
历史模型 / 宽限本次 φ 约值若示例阈值设为 2
均值 1000,标准差 50 ms4.50触发怀疑
均值 1000,标准差 300 ms0.60继续等待
标准差 50,额外宽限 200 ms0.30继续等待

代码直接用互补误差函数求尾部,避免先算接近 1 的 F 再相减带来的精度损失;极端大间隔仍可能下溢为零,这里返回无穷大怀疑度。正态分布只是近似,甚至给负间隔分配少量概率,不能把它当成网络的物理定律。

这段代码也没有学习样本窗口、设置最小方差或实现启动阶段,阈值 2 只用于展示比较,不是部署建议。宽限提高容忍度,也会推迟真实故障检测;若观察者自己暂停,不能靠修改远端节点的统计就断言问题解决。Akka 文档说明了历史均值、方差及可接受暂停对曲线的影响。φ 检测器说明

φ 是尾部概率的对数,不是死亡概率

右侧着色部分对应“历史模型预计下一次心跳还没到”的概率。保持沉默 1200 ms,只扩大标准差,就能让同一观察的 φ 明显降低;这改变的是参照分布,不是远端真实生死。反过来,固定分布继续延长沉默,尾部概率缩小,负对数增大。曲线展示有限区间,数值按完整正态近似计算。

正在呈现知识画面
φ 是尾部概率的对数,不是死亡概率

同样的沉默,在稳定链路上更反常。φ=−log₁₀P(X>t):尾部概率每缩小十倍,φ 增加一。

SWIM:检测与成员传播分工

间接探测:直连失败后,尝试不同网络路径 观察者 A 目标 P 直接 PING 未收到 ACK 中介 B 中介 C 任一间接路径返回有效 ACK 即提供存活证据;都失败后进入怀疑阶段。

SWIM 使用随机探测,并在直接探测失败后借助其他成员探测目标,以降低单条路径异常造成的误判。间接探测不要求所有中介都成功;任一有效 ACK 就能提供证据。

怀疑阶段给活节点机会反驳过时判断,再通过成员更新传播结果。成员 incarnation 等版本信息用于区分新的存活声明与旧怀疑,不能让任意旧 ACK 永久覆盖较新的状态。SWIM 原论文 将故障检测和成员更新传播明确分开。

SWIM 与 φ 是不同设计:前者描述探测与传播协议,后者描述统计怀疑度计算。它们可以在某些系统中组合,但不能把 SWIM 定义为“把 φ 与 gossip 合起来”。

全互连心跳每节点每轮发送 O(N) 条、全组 O(N²) 条。固定探测扇出可把单节点探测消息数量控制在常数量级,但成员列表、传播载荷、重传和故障发现尾延迟仍需单独计入,不能由 O(1) 探测推出整个系统成本为常数。

检测到异常以后,谁有权接管

检测器说“怀疑旧主不可达”,不等于旧主已停止。若新主开始写数据库,而旧主恢复后继续写,可能同时存在两个操作源。

安全接管通常需要由共识或租约协议授予权限,并让被保护资源检查 fencing token 等代际信息。检测器加速作出怀疑;权限协议防止过时实例继续生效。即使检测器偶尔误判,也应由后者保护安全性。

这与 故障模型 的区分一致:安全性不能只依赖一个经验超时永远不会误判。

Fencing 必须在资源侧检查,并与写入原子发生

假设授权服务依次发出 token 41、42。旧主持有 41,暂停后恢复;新主持有 42。资源保存自己见过的最大 token,在写入时拒绝更小代次。下例用顺序函数调用表示原子的“比较代次并写入”,没有实现授权服务或数据库事务。

资源见到新代次后,拒绝迟到的旧写 新主 token 42 写入 new 旧主 token 41 迟到写入 late-old 资源保存 highest = 42 比较与修改必须原子执行 接纳 拒绝:41 < 42 检测超时用于触发接管;写入权限由授权协议及资源侧校验保护。
class Resource:
    def __init__(self):
        self.highest = 0
        self.value = None

    def write(self, token, value):
        if token < self.highest:
            return False
        self.highest, self.value = token, value
        return True

r = Resource()
assert r.write(41, "old")
assert r.write(42, "new")
assert not r.write(41, "late-old")
assert r.value == "new"
assert r.write(42, "new-again")  # 同一授权期内可多次操作

# 新 token 只被发出、尚未到达资源,不会自动改变资源的检查状态。
r2 = Resource()
assert r2.write(41, "old")
issued_token = 42
assert r2.write(41, "late-but-before-fence")
assert r2.write(issued_token, "new")
assert not r2.write(41, "now-rejected")

第二组调用界定了保证:资源看到 42 以后,41 不能再覆盖它;仅仅发出 42,不会瞬间通知所有外部资源。若需求是授权切换后立即拒绝任何旧写,必须让资源参与这个切换或原子校验当前授权状态,不能只依赖“最大已见 token”。fencing 的写入侧机制可参见 Martin Kleppmann 对过期持锁者的分析。

真实 token 必须由可信授权协议单调发放,资源不能允许任意客户端自报一个超大数字。资源还要持久保存代次,并把检查与业务修改放在同一个原子操作中;若检查通过后先解锁再写值,新旧请求仍能穿插。对于同一 token 内的重复扣款,仍需请求 ID 去重:fencing 隔离过时代次,幂等消除同一操作重试,两者并不互相代替。

Deadline、超时与恢复预算

一次请求有总 deadline,各下游操作只能使用剩余预算。A 已花 80 ms、总预算 100 ms,再给 B 一个独立的 100 ms 超时会突破调用方承诺。超时后还需传播取消,避免过期工作继续占用资源。

重试同样消耗预算和容量。指数退避与 jitter 能分散重试,但不能替代限次、幂等和过载保护。成员失联超时、RPC deadline 和健康检查阈值服务于不同决策,不应共用一个“全局超时常数”。

参考

关键词:heartbeat、timeout、φ-accrual、SWIM、indirect probe、suspicion、incarnation、fencing、deadline