故障模型

本页目录

故障模型规定证明和设计允许遇到哪些异常。协议“能容忍 1 个故障”只有结合故障类型、持久化假设、通信条件和故障域才有意义。进程退出、数据盘丢失、单向丢包和节点伪造消息不是同一件事。

节点可以怎样失效

模型允许的行为设计关注点
Crash-stop节点停止后不再执行如何在剩余成员中保持安全和进展
Crash-recovery节点停止后可以重启哪些历史必须持久保存,如何重新加入
Omission漏发、漏收或漏处理消息重试、去重、超时与补传
Byzantine违反协议的任意行为,包括对不同对象说不同话认证、冲突证据与拜占庭容错协议

Fail-stop 在严格用法中还包含其他节点能够可靠识别停止的更强假设,不能总与 crash-stop 当作同义词。停止的进程在异步网络中可能无法与极慢进程区分。

Crash-stop 的纯模型可以不讨论该节点恢复后保存什么,但这不代表真实服务不需要持久性。一个要求断电重启后保留已确认数据的产品,已经提出了额外存储与恢复要求。

Crash-recovery:忘记承诺本身就是协议错误

Raft 节点重启后若忘记本任期投给谁,可能再投一次票;Paxos acceptor 若忘记已作出的 Promise,可能接受已经承诺拒绝的旧提案。因此持久化不仅保存用户数据,也保存约束未来行为的协议历史。

还需区分“数据写入操作系统缓存”和“已满足崩溃后的持久化要求”。磁盘、控制器与文件系统的实际保证是故障模型的一部分。新机器拿着旧节点 ID 和空数据目录直接上线,也不能无条件当作原节点正常恢复。

一次崩溃,怎样让两个候选人都凑到多数

考虑三个投票者 A、B、C,日志同样新。任期 7 中 A 自投一票,B 向 A 回复赞成,于是 A 获得 A+B 两票。若 B 的投票只写在内存,回复之后、持久化之前崩溃,恢复时它可能忘记已投给 A。随后 C 在同一任期自投一票,B 又赞成 C,就出现 A+B 与 B+C 两个选举多数。

两个多数依然相交,问题出在交点 B 没有保留约束自己未来行为的状态。因此“多数必然相交”必须配合“相交成员不违反投票规则”,才是完整的安全推理。

错误的回复顺序:多数相交,但交点忘记了自己的票 B 投给 A 仅内存记录 回复已到 A A+B 成为多数 B 崩溃恢复 旧稳定状态无投票 B 又投给 C B+C 也成为多数 正确顺序:记录 term / votedFor → 持久化完成 → 回复 所有请求位于任期 7,候选人日志同样新;问题是恢复后重复投票。

下面把稳定状态简化成一个不可变元组。durable=False 故意模拟错误的提前回复;重启只从稳定元组恢复。为了聚焦持久化顺序,所有请求都假设通过日志新旧资格检查,代码不实现完整 RequestVote。

class Voter:
    def __init__(self):
        self.stable = (0, None)
        self.reboot()

    def reboot(self):
        self.term, self.voted_for = self.stable

    def request(self, term, candidate, durable=True):
        if term < self.term:
            return False
        if term > self.term:
            self.term, self.voted_for = term, None
        if self.voted_for not in (None, candidate):
            return False
        self.voted_for = candidate
        if durable:
            self.stable = (self.term, self.voted_for)
        return True

broken = Voter()
assert broken.request(7, "A", durable=False)  # A 已经把这票计入多数
broken.reboot()
assert broken.request(7, "C")                # B 错误地又投了另一候选人

safe = Voter()
assert safe.request(7, "A")
safe.reboot()
assert not safe.request(7, "C")
assert safe.request(7, "A")                  # 重复请求仍可回复同一票
assert safe.request(8, "C")                  # 新任期可以重新选择
safe.reboot()
assert not safe.request(7, "A")              # 旧任期仍被拒绝

实际实现需要在发送依赖这些状态的响应之前,完成协议要求的稳定存储更新。若持久化完成但响应丢失,重试会看到旧决定;若响应已经发出却允许决定丢失,其他节点就可能依据一项恢复后不再成立的承诺继续行动。这两种崩溃位置的安全含义不同。Raft Figure 2:响应前更新持久状态

元组赋值只用于表达恢复边界,不是在验证文件系统 fsync、原子写或断电行为。真实故障测试要明确崩溃的是进程、内核还是整台机器,以及哪些存储层仍保留数据。

拜占庭故障:认证不等于诚实

一个已认证节点仍可能向 A 宣称值 X、向 B 宣称值 Y。签名能说明消息由谁发出,不能让发送者自动遵守协议。实现错误、内存损坏或被入侵都可能使节点行为超出单纯宕机模型。

经典 PBFT 在其异步安全性与最终进展所需条件下,使用至少 3f+1 个副本容忍 f 个拜占庭故障。直观上,两个大小为 2f+1 的 quorum 在 3f+1 个节点中至少相交 f+1 个,其中至少一个诚实节点;但安全还依赖诚实节点遵守协议的投票与锁定规则。

这不是所有同步、认证、可信硬件等模型下统一的节点数量下界。比较 2f+1 与 3f+1 时必须保持假设清楚。非拜占庭多数共识的相交节点被假定遵守协议,而拜占庭模型还要防止交集全由会撒谎的节点构成。

相交一个节点,为什么在拜占庭模型中还不够

三节点 A、B、C 中,两个两票集合 {A,B} 与 {A,C} 的交集只有 A。如果 A 可以对 B 签署 X、对 C 签署 Y,那么两边都能拿到两个不同身份的签名;认证没有让这个交点诚实。

经典 3f+1 布局用更大的交集抵消最多 f 个不诚实成员。由集合大小可得:

|Q1 ∩ Q2| ≥ |Q1| + |Q2| − N = 2(2f+1) − (3f+1) = f+1。

即使交集里包含全部 f 个坏节点,也还剩至少一个诚实成员。下表把公式展开,便于检查“副本数”“证据门槛”和“故障预算”是否混用了口径。

f副本数 3f+1quorum 大小 2f+1最少交集最少诚实交集
14321
27531
310741
from itertools import combinations

assert (set("AB") & set("AC")) - {"A"} == set()
for f in (1, 2, 3):
    n, q = 3 * f + 1, 2 * f + 1
    quorums = [set(c) for c in combinations(range(n), q)]
    smallest = min(len(a & b) for a in quorums for b in quorums)
    assert smallest == f + 1
    assert smallest - f >= 1

这段穷举只检查集合条件,不是 PBFT 的实现或安全证明。完整协议还需要区分阶段、视图和序号,并规定诚实节点何时投票、何时锁定、换主时继承什么证据。不能拿到任意 2f+1 条消息就宣布提交;把不同视图或不同日志位置的签名拼在一起,并不形成同一个决定的有效证据。PBFT 的服务保证与模型

相交的节点,够不够排除全部坏节点?

这里有两种交集数量:你选出的两集合实际相交多少,以及只知道集合大小时最少保证相交多少。协议需要后一种保证,因为不能预先指定每轮恰好由哪些节点回复。下面固定 N=7;两边各选 5 个节点时,最少交集为 3,扣除至多 2 个不诚实成员,仍至少留下 1 个诚实成员。

正在呈现知识画面
相交的节点,够不够排除全部坏节点?

两集合的最少交集为 max(0,∣Q₁∣+∣Q₂∣−N)。最多 f 个成员不诚实时,诚实交集下界再减 f,并截断到零。

网络分区:方向与规模是两个维度

分区可以使节点仍然运行,却无法交换必要消息。对称断连指两个方向都受影响,非对称断连指一个方向通、另一个方向不通;不是按“等分”和“孤立一个节点”来定义。

五节点分区为 3 + 2:只有一侧具备多数 A B C D E 3 票:具备 quorum 2 票:不能形成 quorum 少数侧仍可能有自认为在任的旧 leader,但不能因此获得新的合法提交能力。

固定成员集合中两个互不相交的分区不可能都包含严格多数。3+2 分区的多数侧也不必然立刻健康:它可能需要重新选举,或者受到慢盘和过载影响。若分成 2+2+1,则没有任何一侧有多数。

脑裂通常指本应唯一的服务权威出现多个实际写入者。合法的多主系统允许并发写入并定义冲突处理,不应仅因它有多个 writer 就称为协议失效。单主系统则应通过 quorum、任期或外部 fencing 阻止过期写入者继续造成效果。

时间模型:什么条件下保证继续前进

同步模型假定相关处理与通信存在已知上界;异步模型不提供这样的速度上界;部分同步模型则在某种最终稳定条件下允许获得足够的时序约束。它们是协议推理的假设,不是简单把“机房内网”叫同步、“公网”叫异步。

FLP 表明,在允许一个进程崩溃的完全异步模型中,确定性共识不能保证所有允许执行都终止。它没有说共识无法实现安全性,也没有说 Raft 或 Paxos 在实际稳定网络中无法工作。工程协议通常保留安全规则,并在可用 quorum、最终稳定通信、随机化或失败检测假设下追求进展。Gilbert 与 Lynch 对不可能性结果的讨论

超时是证据,不是节点死亡证明

响应未在截止时间内到达,可能来自对端故障、网络拥塞、本地调度暂停或回复丢失。若请求已经在对端生效,超时后重试还可能重复执行。故障检测、业务重试与接管授权需要分别设计。

超时过短会把长尾延迟放大成重试和选举,反过来加重负载;过长则延缓恢复。应根据端到端延迟分布、暂停、容忍的误报与恢复目标设置,并在过载时限制重试。P99 等统计值反映观测样本,不是未来延迟的硬上界。

φ-accrual 等探测器把心跳间隔转换成怀疑程度,帮助表达连续证据;它仍不提供完美故障判定。具体推演见故障检测。

相关故障:副本数不能替代故障域

三台机器若共用同一电源、交换机或有相同软件缺陷,可能同时失效。把副本分散到不同故障域可以降低某些相关风险,却不能消除公共控制平面、证书过期或错误发布造成的共同故障。

备份与复制也不同:误删除可能被正确复制到全部副本。需要恢复历史数据的目标,就要另行定义备份、保留和恢复验证,而不是用“有三副本”代替全部数据保护要求。

设计与验证速查

  1. 写成功后允许丢失哪些数据,容忍哪些节点或存储故障?
  2. 重启保留哪些协议状态,丢盘节点如何重新加入?
  3. 网络是否考虑单向故障、延迟、重复与断连恢复?
  4. 无 quorum 时哪些操作等待、拒绝或允许读取旧状态?
  5. 老进程恢复后,实际资源如何拒绝过期权限?
  6. 安全性检查是否覆盖异常执行,恢复进展是否有明确前提?

这些问题可以直接转为故障注入场景:响应前后崩溃、持久化后断电、隔离旧主后恢复、延迟重复请求,以及成员变更期间的节点重启。预期结果应写成不变量和恢复条件,而不是只检查“最后服务起来了”。

参考

Keywords: crash-stop, crash-recovery, fail-stop, Byzantine, partition, partial synchrony, FLP, safety, liveness