故障模型规定证明和设计允许遇到哪些异常。协议“能容忍 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 没有保留约束自己未来行为的状态。因此“多数必然相交”必须配合“相交成员不违反投票规则”,才是完整的安全推理。
下面把稳定状态简化成一个不可变元组。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) broken.reboot()
assert broken.request(7, "C")
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+1 | quorum 大小 2f+1 | 最少交集 | 最少诚实交集 |
| 1 | 4 | 3 | 2 | 1 |
| 2 | 7 | 5 | 3 | 1 |
| 3 | 10 | 7 | 4 | 1 |
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 个诚实成员。
hmin=max(0,∣Q1∣+∣Q2∣−N−f)
正在呈现知识画面
相交的节点,够不够排除全部坏节点?两集合的最少交集为 max(0,∣Q₁∣+∣Q₂∣−N)。最多 f 个成员不诚实时,诚实交集下界再减 f,并截断到零。
{"id":"quorum-honest-intersection","title":"相交的节点,够不够排除全部坏节点?","summary":"两集合的最少交集为 max(0,∣Q₁∣+∣Q₂∣−N)。最多 f 个成员不诚实时,诚实交集下界再减 f,并截断到零。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"hint\">Q₁</p><div id=\"q0\" class=\"choices\"></div><p class=\"hint\">Q₂</p><div id=\"q1\" class=\"choices\"></div><div id=\"overlap\" class=\"track\"></div><div class=\"control\"><label for=\"faults\" data-i18n=\"f\"></label><output for=\"faults\" id=\"faults-value\"></output><input id=\"faults\" type=\"range\" min=\"0\" max=\"3\" value=\"2\" step=\"1\"></div><div class=\"metrics\"><div><span data-i18n=\"actual\"></span><strong id=\"actual\"></strong></div><div><span data-i18n=\"lower\"></span><strong id=\"lower\"></strong></div><div><span data-i18n=\"honest\"></span><strong id=\"honest\"></strong></div></div><div id=\"formula\" class=\"formula\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"yes\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"no\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.choices{grid-template-columns:repeat(7,minmax(0,1fr))}.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.metrics span{min-height:5.4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let qs=[new Set([0,1,2,3,4]),new Set([2,3,4,5,6])];for(let q=0;q<2;q++)for(let i=0;i<7;i++){const b=document.createElement('button');b.textContent='ABCDEFG'[i];b.dataset.node=i;b.onclick=()=>{qs[q].has(i)?qs[q].delete(i):qs[q].add(i);draw();};$('#q'+q).append(b);}function draw(){const a=qs[0].size,b=qs[1].size,f=+$('#faults').value,x=[...qs[0]].filter(i=>qs[1].has(i)).length,lo=Math.max(0,a+b-7),honest=Math.max(0,lo-f);set('faults-value',f);set('actual',x);set('lower',lo);set('honest',honest);set('formula','max(0, '+a+' + '+b+' − 7 − '+f+') = '+honest);cells('overlap',Array.from({length:7},(_,i)=>({text:'ABCDEFG'[i],cls:qs[0].has(i)&&qs[1].has(i)?'gold':'empty'})));qs.forEach((set,q)=>$('#q'+q).querySelectorAll('button').forEach(e=>e.setAttribute('aria-pressed',String(set.has(+e.dataset.node)))));set('explanation',t(honest?'yes':'no'));}$('#faults').oninput=draw;draw();","audio":false,"strings":{"actual":"实际交集","f":"最多不诚实成员 f","heading":"相交的节点,够不够排除全部坏节点?","honest":"诚实交集的保证下界","intro":"点击节点,分别选择两份证据集合。调节故障预算,对照实际交集和最坏情况下的下界。","lower":"任意同大小集合的交集下界","no":"下界为零:仅凭人数不能保证交集含有诚实成员。","reset":"重置","scope":"固定七个节点,仅验证集合条件。诚实交集还必须遵守同一视图、位置和阶段的协议规则;这不是完整 PBFT 模拟。","scopeLabel":"模型范围与约定","yes":"下界大于零:相交处必有诚实成员,仍需协议规则约束其投票。"}}
网络分区:方向与规模是两个维度
分区可以使节点仍然运行,却无法交换必要消息。对称断连指两个方向都受影响,非对称断连指一个方向通、另一个方向不通;不是按“等分”和“孤立一个节点”来定义。
固定成员集合中两个互不相交的分区不可能都包含严格多数。3+2 分区的多数侧也不必然立刻健康:它可能需要重新选举,或者受到慢盘和过载影响。若分成 2+2+1,则没有任何一侧有多数。
脑裂通常指本应唯一的服务权威出现多个实际写入者。合法的多主系统允许并发写入并定义冲突处理,不应仅因它有多个 writer 就称为协议失效。单主系统则应通过 quorum、任期或外部 fencing 阻止过期写入者继续造成效果。
时间模型:什么条件下保证继续前进
同步模型假定相关处理与通信存在已知上界;异步模型不提供这样的速度上界;部分同步模型则在某种最终稳定条件下允许获得足够的时序约束。它们是协议推理的假设,不是简单把“机房内网”叫同步、“公网”叫异步。
FLP 表明,在允许一个进程崩溃的完全异步模型中,确定性共识不能保证所有允许执行都终止。它没有说共识无法实现安全性,也没有说 Raft 或 Paxos 在实际稳定网络中无法工作。工程协议通常保留安全规则,并在可用 quorum、最终稳定通信、随机化或失败检测假设下追求进展。Gilbert 与 Lynch 对不可能性结果的讨论
超时是证据,不是节点死亡证明
响应未在截止时间内到达,可能来自对端故障、网络拥塞、本地调度暂停或回复丢失。若请求已经在对端生效,超时后重试还可能重复执行。故障检测、业务重试与接管授权需要分别设计。
超时过短会把长尾延迟放大成重试和选举,反过来加重负载;过长则延缓恢复。应根据端到端延迟分布、暂停、容忍的误报与恢复目标设置,并在过载时限制重试。P99 等统计值反映观测样本,不是未来延迟的硬上界。
φ-accrual 等探测器把心跳间隔转换成怀疑程度,帮助表达连续证据;它仍不提供完美故障判定。具体推演见故障检测。
相关故障:副本数不能替代故障域
三台机器若共用同一电源、交换机或有相同软件缺陷,可能同时失效。把副本分散到不同故障域可以降低某些相关风险,却不能消除公共控制平面、证书过期或错误发布造成的共同故障。
备份与复制也不同:误删除可能被正确复制到全部副本。需要恢复历史数据的目标,就要另行定义备份、保留和恢复验证,而不是用“有三副本”代替全部数据保护要求。
设计与验证速查
- 写成功后允许丢失哪些数据,容忍哪些节点或存储故障?
- 重启保留哪些协议状态,丢盘节点如何重新加入?
- 网络是否考虑单向故障、延迟、重复与断连恢复?
- 无 quorum 时哪些操作等待、拒绝或允许读取旧状态?
- 老进程恢复后,实际资源如何拒绝过期权限?
- 安全性检查是否覆盖异常执行,恢复进展是否有明确前提?
这些问题可以直接转为故障注入场景:响应前后崩溃、持久化后断电、隔离旧主后恢复、延迟重复请求,以及成员变更期间的节点重启。预期结果应写成不变量和恢复条件,而不是只检查“最后服务起来了”。
参考
Keywords: crash-stop, crash-recovery, fail-stop, Byzantine, partition, partial synchrony, FLP, safety, liveness