Skip to content

分布式故障检测:心跳、Phi Accrual、SWIM与健康探针

远程节点没有回应,可能是进程崩溃、长 GC、CPU 饱和、线程池耗尽、网络丢包,也可能只是响应迟到。分布式系统通常无法瞬间证明节点永久死亡,只能根据有限观测“怀疑”它不可用,并在误判速度与故障发现速度之间权衡。

一、学习目标

学完后应能区分心跳、探测、租约、Session 和业务健康,理解固定超时、Phi Accrual、SWIM 的原理,并能设计 Kubernetes Probe、摘流和恢复 Runbook。

二、为什么“没回应”不等于“已经死亡”

mermaid
flowchart TD
    A["节点A没有按时收到B响应"] --> B["B进程崩溃"]
    A --> C["B长GC或CPU饱和"]
    A --> D["A到B网络分区"]
    A --> E["请求或响应丢包"]
    A --> F["线程池、连接池或磁盘排队"]
    A --> G["超时阈值过短"]

对 A 来说这些现象都可能表现为 Timeout。A 能决定暂时不把流量发给 B,却不能从一次超时推导出 B 永久死亡。

在纯异步模型中,消息延迟没有已知上限,慢节点与死亡节点不可区分。工程系统通常引入部分同步假设:正常心跳间隔和网络延迟有统计范围,超出后进入怀疑状态。

三、故障检测器关注什么性质

  • Completeness:真正故障的节点最终能否被怀疑。
  • Accuracy:正常节点是否尽量不被错误怀疑。

检测越快通常越容易误判;阈值越宽松,误判少但故障流量持续更久。理论还讨论 Strong/Weak Completeness、Strong/Weak Accuracy 与 Eventually Perfect Failure Detector;工程上要明确延迟稳定后能否形成稳定判断,以及误判会触发什么副作用。

误判若只让负载均衡暂时避开实例,代价相对可控;若直接触发主库提升、删除成员或重新分片,风险很高,必须经过共识、Fencing 和安全门。

四、心跳与主动探测

4.1 Push Heartbeat

被监控节点周期向中心或其他节点上报。实现简单,但中心承受所有心跳;心跳线程正常也不代表业务线程、数据库和依赖健康。

4.2 Pull/Probe

监控方主动请求健康端点,能覆盖网络路径和应用响应,但大规模全连接探测会形成 O(N²) 消息量。

4.3 Piggyback

在正常协议消息上携带活跃信息,减少额外消息;业务流量低时仍需要独立探测。

mermaid
flowchart TD
    A["到达探测周期"] --> B["发送Ping或健康请求"]
    B --> C{"阈值内收到有效响应"}
    C -- "是" --> D["更新成功时间和延迟分布"]
    C -- "否" --> E["进入怀疑或间接探测"]
    E --> F{"二次证据确认可达"}
    F -- "可达" --> D
    F -- "仍不可达" --> G["标记不可用并传播状态"]

五、固定超时为什么容易误判

心跳每 1 秒一次,连续 3 秒未到就摘除。正常 P99.9 偶尔超过 3 秒或 JVM 出现 4 秒 Full GC,节点会被错误摘除;阈值改成 30 秒后,真实故障又会承受 30 秒失败流量。

固定阈值要结合:心跳间隔、网络 P99/P99.9、故障发现目标、GC/Safepoint、CPU/磁盘排队、连续失败次数、恢复成功次数和地域链路基线。

恢复通常采用滞回:连续多次成功或经过稳定窗口才重新接流,避免节点在健康/不健康之间抖动。

六、Phi Accrual Failure Detector

Phi Accrual 不直接返回 true/false,而是根据历史心跳间隔计算当前延迟有多异常,输出怀疑等级 φ

text
φ(t) = -log10(1 - F(t))

F(t) 是历史间隔分布在时间 t 前到达的概率。若按历史分布,延迟到当前仍未到的概率只有 10^-φ,φ 越大越可疑。

φ简化直觉
1约十分之一概率出现这么晚,证据弱
3约千分之一,较可疑
8约一亿分之一,证据很强

实际实现可能使用正态近似、最小标准差、可接受暂停等参数,表格只是数学直觉,不表示所有组件阈值一致。

mermaid
flowchart TD
    A["记录最近心跳间隔样本"] --> B["估计均值、方差或分布"]
    B --> C["计算距上次心跳的时间"]
    C --> D["计算当前Phi怀疑值"]
    D --> E{"Phi超过业务阈值"}
    E -- "否" --> F["继续观察"]
    E -- "是" --> G["怀疑、摘流或二次确认"]

它能适应不同延迟基线,但样本不足、分布突变、长暂停和相关性故障仍会误导统计模型。Phi 是怀疑证据,不是死亡证明。

七、SWIM:可扩展成员与故障检测

一次探测:

  1. A 随机选择 B,发送 PING
  2. B 返回 ACK,本轮可达。
  3. 未收到 ACK,A 请求 C、D 执行 PING-REQ(B)
  4. C、D 从不同路径间接探测 B。
  5. 仍无响应,A 将 B 标记 SUSPECT,不立刻 DEAD。
  6. Suspect 通过 Gossip/Piggyback 传播。
  7. B 若存活,用更大的 Incarnation 反驳旧怀疑。
  8. 怀疑窗口结束仍无反驳,才传播 FAILED。
mermaid
flowchart TD
    A["A直接PING节点B"] --> B{"B是否ACK"}
    B -- "是" --> C["B保持ALIVE"]
    B -- "否" --> D["A请求C和D间接PING B"]
    D --> E{"任一路径收到ACK"}
    E -- "是" --> C
    E -- "否" --> F["B进入SUSPECT"]
    F --> G{"更高Incarnation反驳"}
    G -- "反驳" --> C
    G -- "超时未反驳" --> H["B进入FAILED并传播"]

间接探测降低单路径故障误判,但不能消除整个机架或地域的相关性故障。Incarnation 防止迟到的旧 SUSPECT 覆盖已恢复的新 ALIVE 状态。

Gossip 以较低消息量概率传播,通常较快收敛但不是瞬时全局一致。不同节点短时间看到不同成员列表是正常协议现象,调用方仍需超时、幂等和被动失败反馈。

八、心跳、租约和 Session 的区别

机制主要判断典型边界
心跳/Probe某条路径近期是否可达不证明业务依赖健康
租约裁决服务在期限内授予权利旧持有者暂停恢复仍需 Fencing
ZooKeeper Session会话在超时内是否保持本地断开不等于 Session 立即失效
etcd LeaseKey 与服务端 Lease 绑定KeepAlive 不证明业务接口健康
Kubernetes Readiness当前是否应接流量失败摘流,不应必然重启
Kubernetes Liveness进程是否无法自行恢复配错会造成重启风暴

客户端进程仍在续租不能证明端口可用、线程池有余量、数据库正常。注册中心应组合主动健康检查、Readiness、调用失败统计和业务指标。

九、Kubernetes 三类 Probe

Startup Probe

保护慢启动。成功前避免 Liveness/Readiness 过早处理,超过阈值才重启;成功后不再执行。

Readiness Probe

失败后 Pod 从 Service Endpoint/EndpointSlice 摘除,不接新流量,容器不因此自动重启。不要把所有非核心依赖设为硬失败,否则报表依赖异常也会摘除交易服务。

Liveness Probe

持续失败会触发重启,只用于进程无法自行恢复的状态。把数据库不可用设为 Liveness,会让所有实例在数据库故障期间不断重启,制造恢复风暴。

mermaid
flowchart TD
    A["容器启动"] --> B["Startup Probe保护启动窗口"]
    B --> C{"启动成功"}
    C -- "否且未超阈值" --> B
    C -- "失败超阈值" --> D["重启容器"]
    C -- "成功" --> E["启用Readiness与Liveness"]
    E --> F{"Readiness失败"}
    F -- "是" --> G["从服务端点摘流"]
    E --> H{"Liveness持续失败"}
    H -- "是" --> D

periodSecondstimeoutSecondsfailureThresholdsuccessThreshold、启动时间和终止宽限期共同决定检测窗口。

十、健康不是一个 boolean

  • Process Alive:进程/事件循环存在。
  • Ready:能否接收新流量。
  • Dependency Degraded:依赖异常是否可降级。
  • Saturated:线程池、连接池、队列是否接近耗尽。
  • Business Healthy:交易成功率、Lag、数据积压是否正常。

一个 /health 返回 200 不代表所有维度健康。端点必须轻量、有超时和并发保护,不能每秒执行昂贵 SQL 或串行调用十个下游。

十一、JDK 8 Demo:单调时钟心跳怀疑器

java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public final class HeartbeatSuspicion {
    private final long timeoutNanos;
    private final AtomicLong lastSuccessNanos = new AtomicLong(System.nanoTime());

    public HeartbeatSuspicion(long timeout, TimeUnit unit) {
        if (timeout <= 0L) {
            throw new IllegalArgumentException("timeout必须大于0");
        }
        this.timeoutNanos = unit.toNanos(timeout);
    }

    public void onSuccess() {
        lastSuccessNanos.set(System.nanoTime());
    }

    public boolean isSuspected() {
        long elapsed = System.nanoTime() - lastSuccessNanos.get();
        return elapsed >= timeoutNanos;
    }

    public static void main(String[] args) throws Exception {
        HeartbeatSuspicion detector = new HeartbeatSuspicion(60L, TimeUnit.MILLISECONDS);
        detector.onSuccess();
        Thread.sleep(20L);
        System.out.println("suspectedEarly=" + detector.isSuspected());
        Thread.sleep(60L);
        System.out.println("suspectedLate=" + detector.isSuspected());
    }
}

true 只表示超过本地怀疑阈值,不代表节点已被证明死亡。生产系统还要有连续成功恢复、间接探测、状态版本、传播和副作用安全策略。

十二、商业场景:服务发现中的上下线闭环

订单服务实例发生 8 秒长 GC:

  1. Readiness 超时,实例从负载均衡端点摘除。
  2. 注册中心心跳进入 Suspect,但不立即永久删除成员。
  3. 调用方旧连接仍可能命中实例,需要被动失败和连接排空。
  4. GC 恢复后先完成自检、连接池恢复和缓存预热。
  5. 连续多个 Readiness 成功后小流量恢复。
  6. 若实例曾持有主节点租约,必须重新获得租约和更大 Fencing Token,不能继续旧任务。

探针、注册中心、客户端负载均衡和业务租约是不同层,不能只配置一个心跳就宣称高可用。

十三、故障检测失败窗口

窗口现象风险应对
故障到检测实例已坏但仍在列表请求继续失败合理阈值、被动摘除、重试预算
正常节点误判长GC/网络抖动容量下降、重新分片Suspect、间接探测、滞回
状态传播中客户端列表不同部分仍发往旧节点超时、连接排空、版本通知
恢复过早加流缓存/连接池未热再次过载Readiness预热与慢启动
全体同时恢复恢复风暴DB/MQ瞬时过载抖动、分批恢复、限流
误判触发切主两边都可能写脑裂共识、Fencing、STONITH

十四、实例频繁上下线 Runbook

  1. 确认判断者:Kubernetes、注册中心、客户端、负载均衡还是业务监控。
  2. 保存探针失败原因、阶段、连续次数和状态转换时间。
  3. 对齐 GC/Safepoint、CPU Throttle、线程池、连接池、磁盘和网络重传。
  4. 检查探针端点是否调用数据库或下游并形成级联失败。
  5. 检查间隔、超时、Phi 阈值、Suspicion 窗口和恢复滞回。
  6. 检查 Endpoint/成员列表传播延迟和客户端缓存版本。
  7. 检查长连接是否仍绑定已摘除实例,是否正确排空。
  8. 若伴随选主,检查多数派、Term/Epoch 和 Fencing,禁止只凭 Probe 切主。
  9. 注入长 GC、单向丢包、延迟和依赖故障,验证误判率与发现时间。

关键指标包括探针延迟、失败原因、Suspect/Recover 次数、故障发现时间、误判率、成员版本差、Endpoint 传播时间、被动失败率、重连数和重启次数。

十五、常见错误

错误为什么错
一次超时证明节点死亡慢节点与网络分区不可区分
心跳线程活着代表业务健康业务线程池和依赖可能耗尽
Probe 越频繁越可靠放大负载和误判,可能自激故障
数据库失败就触发 Liveness全体重启,制造恢复风暴
摘除实例后流量立即归零客户端缓存和长连接有传播窗口
SWIM 消除所有误判相关性网络故障仍可能多路径失败
故障检测可替代共识怀疑信息不能安全决定唯一主节点
恢复后立刻全量接流冷缓存和重连可能再次压垮实例

十六、关联知识点