分布式故障检测:心跳、Phi Accrual、SWIM与健康探针
远程节点没有回应,可能是进程崩溃、长 GC、CPU 饱和、线程池耗尽、网络丢包,也可能只是响应迟到。分布式系统通常无法瞬间证明节点永久死亡,只能根据有限观测“怀疑”它不可用,并在误判速度与故障发现速度之间权衡。
一、学习目标
学完后应能区分心跳、探测、租约、Session 和业务健康,理解固定超时、Phi Accrual、SWIM 的原理,并能设计 Kubernetes Probe、摘流和恢复 Runbook。
二、为什么“没回应”不等于“已经死亡”
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
在正常协议消息上携带活跃信息,减少额外消息;业务流量低时仍需要独立探测。
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,而是根据历史心跳间隔计算当前延迟有多异常,输出怀疑等级 φ:
φ(t) = -log10(1 - F(t))F(t) 是历史间隔分布在时间 t 前到达的概率。若按历史分布,延迟到当前仍未到的概率只有 10^-φ,φ 越大越可疑。
| φ | 简化直觉 |
|---|---|
| 1 | 约十分之一概率出现这么晚,证据弱 |
| 3 | 约千分之一,较可疑 |
| 8 | 约一亿分之一,证据很强 |
实际实现可能使用正态近似、最小标准差、可接受暂停等参数,表格只是数学直觉,不表示所有组件阈值一致。
flowchart TD
A["记录最近心跳间隔样本"] --> B["估计均值、方差或分布"]
B --> C["计算距上次心跳的时间"]
C --> D["计算当前Phi怀疑值"]
D --> E{"Phi超过业务阈值"}
E -- "否" --> F["继续观察"]
E -- "是" --> G["怀疑、摘流或二次确认"]它能适应不同延迟基线,但样本不足、分布突变、长暂停和相关性故障仍会误导统计模型。Phi 是怀疑证据,不是死亡证明。
七、SWIM:可扩展成员与故障检测
一次探测:
- A 随机选择 B,发送
PING。 - B 返回
ACK,本轮可达。 - 未收到 ACK,A 请求 C、D 执行
PING-REQ(B)。 - C、D 从不同路径间接探测 B。
- 仍无响应,A 将 B 标记
SUSPECT,不立刻 DEAD。 - Suspect 通过 Gossip/Piggyback 传播。
- B 若存活,用更大的 Incarnation 反驳旧怀疑。
- 怀疑窗口结束仍无反驳,才传播 FAILED。
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 Lease | Key 与服务端 Lease 绑定 | KeepAlive 不证明业务接口健康 |
| Kubernetes Readiness | 当前是否应接流量 | 失败摘流,不应必然重启 |
| Kubernetes Liveness | 进程是否无法自行恢复 | 配错会造成重启风暴 |
客户端进程仍在续租不能证明端口可用、线程池有余量、数据库正常。注册中心应组合主动健康检查、Readiness、调用失败统计和业务指标。
九、Kubernetes 三类 Probe
Startup Probe
保护慢启动。成功前避免 Liveness/Readiness 过早处理,超过阈值才重启;成功后不再执行。
Readiness Probe
失败后 Pod 从 Service Endpoint/EndpointSlice 摘除,不接新流量,容器不因此自动重启。不要把所有非核心依赖设为硬失败,否则报表依赖异常也会摘除交易服务。
Liveness Probe
持续失败会触发重启,只用于进程无法自行恢复的状态。把数据库不可用设为 Liveness,会让所有实例在数据库故障期间不断重启,制造恢复风暴。
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 -- "是" --> DperiodSeconds、timeoutSeconds、failureThreshold、successThreshold、启动时间和终止宽限期共同决定检测窗口。
十、健康不是一个 boolean
- Process Alive:进程/事件循环存在。
- Ready:能否接收新流量。
- Dependency Degraded:依赖异常是否可降级。
- Saturated:线程池、连接池、队列是否接近耗尽。
- Business Healthy:交易成功率、Lag、数据积压是否正常。
一个 /health 返回 200 不代表所有维度健康。端点必须轻量、有超时和并发保护,不能每秒执行昂贵 SQL 或串行调用十个下游。
十一、JDK 8 Demo:单调时钟心跳怀疑器
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:
- Readiness 超时,实例从负载均衡端点摘除。
- 注册中心心跳进入 Suspect,但不立即永久删除成员。
- 调用方旧连接仍可能命中实例,需要被动失败和连接排空。
- GC 恢复后先完成自检、连接池恢复和缓存预热。
- 连续多个 Readiness 成功后小流量恢复。
- 若实例曾持有主节点租约,必须重新获得租约和更大 Fencing Token,不能继续旧任务。
探针、注册中心、客户端负载均衡和业务租约是不同层,不能只配置一个心跳就宣称高可用。
十三、故障检测失败窗口
| 窗口 | 现象 | 风险 | 应对 |
|---|---|---|---|
| 故障到检测 | 实例已坏但仍在列表 | 请求继续失败 | 合理阈值、被动摘除、重试预算 |
| 正常节点误判 | 长GC/网络抖动 | 容量下降、重新分片 | Suspect、间接探测、滞回 |
| 状态传播中 | 客户端列表不同 | 部分仍发往旧节点 | 超时、连接排空、版本通知 |
| 恢复过早加流 | 缓存/连接池未热 | 再次过载 | Readiness预热与慢启动 |
| 全体同时恢复 | 恢复风暴 | DB/MQ瞬时过载 | 抖动、分批恢复、限流 |
| 误判触发切主 | 两边都可能写 | 脑裂 | 共识、Fencing、STONITH |
十四、实例频繁上下线 Runbook
- 确认判断者:Kubernetes、注册中心、客户端、负载均衡还是业务监控。
- 保存探针失败原因、阶段、连续次数和状态转换时间。
- 对齐 GC/Safepoint、CPU Throttle、线程池、连接池、磁盘和网络重传。
- 检查探针端点是否调用数据库或下游并形成级联失败。
- 检查间隔、超时、Phi 阈值、Suspicion 窗口和恢复滞回。
- 检查 Endpoint/成员列表传播延迟和客户端缓存版本。
- 检查长连接是否仍绑定已摘除实例,是否正确排空。
- 若伴随选主,检查多数派、Term/Epoch 和 Fencing,禁止只凭 Probe 切主。
- 注入长 GC、单向丢包、延迟和依赖故障,验证误判率与发现时间。
关键指标包括探针延迟、失败原因、Suspect/Recover 次数、故障发现时间、误判率、成员版本差、Endpoint 传播时间、被动失败率、重连数和重启次数。
十五、常见错误
| 错误 | 为什么错 |
|---|---|
| 一次超时证明节点死亡 | 慢节点与网络分区不可区分 |
| 心跳线程活着代表业务健康 | 业务线程池和依赖可能耗尽 |
| Probe 越频繁越可靠 | 放大负载和误判,可能自激故障 |
| 数据库失败就触发 Liveness | 全体重启,制造恢复风暴 |
| 摘除实例后流量立即归零 | 客户端缓存和长连接有传播窗口 |
| SWIM 消除所有误判 | 相关性网络故障仍可能多路径失败 |
| 故障检测可替代共识 | 怀疑信息不能安全决定唯一主节点 |
| 恢复后立刻全量接流 | 冷缓存和重连可能再次压垮实例 |
