客户端发出一次写请求,等待几秒后超时。此时你无法确定服务器崩溃了、请求在网络中丢失了、服务器仍在处理,还是响应正在返回途中。
四种情况在客户端看来完全一样,却需要不同处理。直接重试可能让同一笔写入执行两次;不重试又可能把一次临时网络抖动变成用户可见错误。分布式系统最难的地方,正是节点之间无法立即分辨「失败」与「缓慢」。
Harshit Khosla 的系列开篇从这类不确定性出发,串起分布式计算八个误区、CAP、PACELC、FLP,以及同步、异步和部分同步模型。本文在此基础上补充这些理论的严格边界,并把它们转换成日常工程判断。
什么情况下算分布式系统
分布式系统由多个独立计算节点组成,对外提供一个整体服务。数据库集群、通过网络协作的微服务、负载均衡后的多个副本,都属于这个范围。
「独立」意味着每个节点拥有自己的内存、时钟和故障状态。节点 A 看不到节点 B 的内部状态,只能通过消息推断。消息又可能延迟、丢失、重复或乱序,因此本地函数调用中的确定性很难原样带到网络调用中。
一个很实用的区分方式是:
本地调用失败:调用方通常立即得到进程或系统返回的失败信息
远程调用超时:调用方只知道等待超过了期限,无法据此确定远端是否执行
这解释了为什么分布式接口通常需要请求 ID、幂等键、去重记录和可查询的最终状态。它们共同解决的核心问题是:调用方重试时,系统应识别这次动作是否已经发生。
八个危险的网络假设
Peter Deutsch 等人在 Sun Microsystems 总结的「分布式计算八个误区」,列出了设计者经常无意中采用的错误假设。
| 错误假设 | 常见后果 | 最小应对 |
|---|---|---|
| 网络可靠 | 丢包或断连导致请求悬挂 | 超时、重试、失败记录 |
| 延迟为零 | 跨服务调用链越来越慢 | 延迟预算、减少串行调用 |
| 带宽无限 | 大对象和重复传输拖垮链路 | 限制载荷、分页、压缩 |
| 网络安全 | 内网调用缺少认证和加密 | 身份验证、授权、传输加密 |
| 拓扑不变 | 扩缩容或故障切换后地址失效 | 服务发现、健康检查 |
| 只有一个管理员 | 多团队操作产生配置差异 | 自动化配置、变更记录 |
| 传输成本为零 | 序列化、跨区流量和调用开销失控 | 观察流量与编解码成本 |
| 网络同质 | 不同设备、协议和版本产生兼容问题 | 标准协议、版本协商 |
这些误区可以当作架构审查清单。只要设计跨越进程或机器,就应逐项问:网络中断时会发生什么,消息延迟时如何观察,重复请求会不会产生额外副作用。
CAP 讨论的是分区期间的保证
CAP 定理讨论分布式数据服务的三个性质:
- Consistency(一致性):严格定义通常接近线性一致性,每次读取都像发生在单一实时顺序中;
- Availability(可用性):每个发给未故障节点的请求最终都能得到非错误响应;
- Partition tolerance(分区容忍):节点之间部分消息丢失或无限延迟时,系统仍按既定保证运行。
Gilbert 与 Lynch 在异步网络模型中证明,发生网络分区时,系统无法同时满足这三项严格保证。
工程讨论里常把 CAP 概括成「三选二」,这句话容易让人以为三项可以自由组合。多节点系统无法控制网络是否发生分区。分区出现后,设计者需要决定:
- 拒绝或延迟一部分请求,以保护一致性;
- 继续响应所有可达节点,接受旧数据或冲突,之后再修复。
因此,CAP 更像一个故障期间的选择题。所谓 CP 和 AP 也只是简写,真实系统往往允许按操作、数据类型或配置选择不同保证。把整个数据库永久贴成一个标签,会隐藏大量细节。
一个库存例子
假设两个机房共同销售最后一件商品。机房之间断连时:
- 若两个机房都继续接受订单,服务保持响应,但可能超卖;
- 若无法确认全局库存的机房停止下单,可以避免冲突,但一部分用户会得到失败或等待。
哪个选择更合适,取决于错误成本。社交动态可以接受短暂旧读,付款、唯一编号和库存扣减通常需要更强约束。
PACELC 补上正常时期的取舍
网络分区只占系统运行的一小部分。大多数时间网络可用,系统仍需要在延迟与一致性之间选择。Daniel Abadi 提出的 PACELC 把两种情形放进同一框架:
P:发生分区时,在 A(可用性)与 C(一致性)之间选择
E:没有分区时,在 L(延迟)与 C(一致性)之间选择
强一致读取通常需要联系领导者或等待多个副本确认,会增加一次或多次网络往返。就近读取副本可以降低延迟,却可能返回尚未同步的数据。
PACELC 提醒我们,系统选择每天都在发生。一次跨区域同步写、一致读取的法定人数、缓存读取策略,都会在正常运行时支付延迟或数据新鲜度成本。
评估数据库时,可以具体询问:
- 分区时哪些请求继续,哪些请求失败?
- 正常时期读写需要联系几个副本?
- 一致性是全局固定,还是能按单次操作调整?
- 冲突由谁检测,如何合并,是否可能丢失更新?
这些问题比单独询问产品属于 CP 还是 AP 更有信息量。
FLP 限制了完全异步共识
Fischer、Lynch 和 Paterson 在 1985 年证明:在完全异步的消息系统中,只要允许一个进程发生崩溃,就不存在能够保证所有正确进程总会终止并达成共识的确定性算法。
这个结论需要注意三个限定:
- 完全异步:消息和处理时间没有已知上限;
- 确定性算法:相同状态和输入产生相同下一步;
- 保证终止:所有允许的执行过程都必须最终作出决定。
FLP 给出的结论是,任何协议都存在一种可能永远不终止的执行过程。它没有宣称每次运行都会失败,也没有否定工程系统中的共识。
Paxos、Raft 和实际协调服务会增加模型假设,例如超时、最终稳定的网络、故障检测器或随机化。网络恢复稳定后,系统可以选出领导者并继续推进。时间假设主要帮助活性,即系统最终取得进展;安全性仍应在消息延迟和重复时保持。
这也是自制分布式锁或领导者选举容易出错的原因。一个简单脚本可能在正常网络中运行良好,却没有处理旧领导者恢复、租约过期、时钟偏移和网络分区。成熟的共识组件已经围绕这些边界积累了协议设计、测试和运维经验。
三种时间模型
理解 CAP 和 FLP 之前,需要先知道系统对时间作了什么假设。
同步模型
消息延迟和处理时间都有已知上限。超过上限未响应时,可以可靠判断节点故障。这个模型容易推理,但互联网和跨区域网络很难长期提供如此严格的保证。
异步模型
消息可能很快到达,也可能延迟任意长时间。系统无法仅凭等待时长区分慢节点与故障节点。FLP 的不可能结果建立在这一模型上。
部分同步模型
现实系统大多采用部分同步假设:延迟上限可能存在,但起初未知,或系统经过一段不稳定期后才恢复到某个上限以内。
RPC 超时和心跳间隔可以看成工程师对这个上限的估计。设置过短,会把暂时变慢的健康节点判成故障;设置过长,会延迟故障发现和恢复。它们需要结合延迟分布、故障恢复目标和误判成本调整。
理论如何进入接口设计
这些结论最终会落到几个很具体的选择上。
远程写入要支持安全重试
为有副作用的请求提供唯一幂等键。服务端保存键与结果,再次收到相同请求时返回原结果,避免重复扣款、重复建单或重复发消息。
超时只表示等待结束
调用超时后,把结果记录为「未知」比直接记为「失败」更准确。若业务重要,应提供状态查询或异步回执,让调用方确认远端是否已经执行。
明确每类数据的一致性要求
账户余额、库存和权限数据通常需要更强一致性;浏览计数、推荐结果和部分缓存可以接受短暂旧读。要求所有数据使用最强一致性会增加延迟和协调成本,全部放宽又会把冲突推给业务层。
把共识交给成熟组件
需要分布式锁、成员变更、领导者选举或配置一致性时,优先使用经过验证的协调服务和数据库能力。自行实现前,应能说明故障模型、安全性质、活性条件和恢复过程。
在测试中主动制造不确定性
常规单元测试很难覆盖分区故障。集成测试可以加入延迟、丢包、重复消息、节点重启、时钟偏移和请求乱序,并检查系统是否保持关键业务约束。
设计前的六个问题
下一次评审分布式功能时,可以先回答:
- 请求超时后,远端是否可能已经完成操作?
- 重试是否安全,幂等记录保存多久?
- 分区期间,系统优先保留哪类请求和哪项业务约束?
- 正常时期,为更强一致性要付出多少网络往返?
- 哪个组件负责共识,它依赖怎样的时间与故障假设?
- 监控和测试能否观察延迟、丢包、重复执行与副本差异?
这些问题能把抽象定理转换成可验证的设计条件。分布式系统中的异常很少真正随机,它们通常来自网络不确定性、时间假设或一致性选择。把假设写清楚,再用幂等、观察信号、成熟协议和故障测试保护边界,系统在真实故障下才更容易预测。
Aide Hub 会继续分享 AI 助手、开发工具和软件工程实践。
参考
- Harshit Khosla:Distributed Systems #1 (Introduction)
- Oracle:Fallacies of distributed systems
- Seth Gilbert、Nancy Lynch:Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services
- Daniel J. Abadi:Consistency Tradeoffs in Modern Distributed Database System Design
- Fischer、Lynch、Paterson:Impossibility of Distributed Consensus with One Faulty Process