- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — "选二"的问题
- 三个字母的严格定义
- 为什么"三选二"是错的
- PACELC — 更完整的图景
- 真实系统站在哪一边
- CAP 的 C 和 ACID 的 C 不是一回事
- 一致性是一个光谱
- 常见的误解
- 给设计者的实战指南
- 结语
- 参考资料
引言 — "选二"的问题
学分布式系统时,你几乎必然会遇到 CAP 定理。而且几乎必然是以这种压缩过的形式遇到它:"一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者中只能选二。"接着是一张三角形图,把每种数据库钉在三条边中的一条上。
这个说法好记,但是错的。更准确地说,它被磨平到会误导人的地步。CAP 定理实际说的东西,范围窄得多、精确得多。这篇文章的目标就是把这层磨平去掉。不打马虎眼,我们会把三个字母各自严格的含义钉死,讲清楚为什么"三选二"是一种误解,以及这个定理对实际系统设计到底意味着什么。
三个字母的严格定义
先要把术语钉准。CAP 的三个字母听起来像日常用语,但在定理内部,它们各自有非常具体的含义。
C — 一致性(Consistency)。 这里的一致性指的是线性一致性(linearizability)。说白了,就是每个客户端看到的数据,就好像只存在一份副本一样。一旦某次写入成功完成,之后开始的每一次读取都必须看到这次写入(或者更新的写入)。绝不能返回旧值。这和后面会讲到的 ACID 里的 C,是完全不同的概念。
A — 可用性(Availability)。 这里的可用性指的是"每一个未失效的节点,都必须对每一个请求返回一个(非错误的)响应"。关键在于"每一个未失效的节点"。不是部分节点响应就够了,只要节点还活着,就必须应答。而且这个应答要在合理的时间内到达。让调用方无限期等待,不算可用。
P — 分区容错性(Partition tolerance)。 分区(partition)指的是网络被割裂,导致节点组之间无法互相传递消息的情况。分区容错性,就是"即便发生这样的分区,系统仍然继续运作"的性质。
把这三个定义并排放在一起看,就已经能看出那张常见的三角形图为什么会误导人了。
为什么"三选二"是错的
核心在这里:分区容错性(P)不是一个可选项。无论你选不选,网络分区在现实中都会发生。交换机会挂掉,网线会被剪断,数据中心之间的链路会瞬间堵塞。只要有多台机器通过网络通信,分区就不是"可能发生的事",而是"迟早会发生的事"。
所以"放弃 P"这个选项事实上并不存在。扛不住分区的系统,一旦分区发生,要么给出错误答案,要么数据损坏。在任何认真的分布式系统里,P 都不是可以拿来谈判的东西。
那真正的选择是什么呢?CAP 定理真正说的是这个:
在网络分区发生的那一刻,你必须在一致性(C)和可用性(A)之间放弃一个。
分区发生时,节点组之间无法互相通信。这时如果写请求打到其中一组,能选的就恰好只有两条路。
- 选可用性(AP):直接接受写入并响应。但另一组不知道这次变更,所以那边读到的会是旧值。这就是放弃了一致性。
- 选一致性(CP):因为没法和另一组达成一致,所以拒绝或阻塞这个请求。答不了,也就是放弃了可用性。
也就是说,CAP 不是"平时三选二"的问题,而是"分区发生时选 C 还是选 A"的问题。没有分区的平时,C 和 A 可以同时享有。选择只在分区这个例外情况下才被强制发生。
无分区(正常) 分区发生
┌─────────────┐ ┌─────────────┐
│ C 和 A 都能 │ --> │ C 或 A 中 │
│ 同时享有 │ │ 只能选一个 │
└─────────────┘ └─────────────┘
PACELC — 更完整的图景
CAP 漏掉了重要的另一半。那就是"没有分区的时候会怎样"。CAP 只谈分区状况,对正常状况保持沉默。但系统的大部分时间,都处在没有分区的正常状态。描述这段时间里的权衡的,正是 PACELC。
PACELC 是这样读的:
如果发生分区(P),就在可用性(A)和一致性(C)之间选。否则(Else, E),就在延迟(L)和一致性(C)之间选。
前半段(PAC)和 CAP 一样。新的是后半段(ELC)。即便没有分区,正常状况下依然存在权衡。要维持强一致性,多个副本就必须达成一致,而达成一致需要时间。也就是说,一致性拉得越高,延迟就越大。反过来,要降低延迟,就得放松甚至跳过这个一致过程,一致性也就随之变弱。
用这个框架看,可以把系统按两条轴分类。
| 分类 | 分区时(PAC) | 正常时(ELC) | 含义 |
|---|---|---|---|
| PA/EL | 可用性优先 | 延迟优先 | 始终选快和可用 |
| PC/EC | 一致性优先 | 一致性优先 | 始终选一致性 |
| PA/EC | 可用性优先 | 一致性优先 | 只在分区时不放弃可用性 |
| PC/EL | 一致性优先 | 延迟优先 | 只在分区时坚守一致性 |
PACELC 比 CAP 更好的地方在于,它捕捉到了我们实际每天都要面对的权衡。分区是罕见事件,但"一致性还是延迟"是贯穿整个正常运行时期的持续选择。
真实系统站在哪一边
把理论套到真实系统上,会清晰得多。
Amazon Dynamo 系列 — AP(以及 EL)。 Dynamo 和它的后代们(比如 Cassandra、Riak,以及 DynamoDB 的部分配置)是以可用性优先为设计目标的。即便发生分区,也继续接受写入,等副本之后再次相遇时,再解决冲突。这一系列提供的是最终一致性(eventual consistency)。此刻不同节点上的值可能不一样,但随着时间推移会收敛。即便在正常状况下,它们也不等待达成一致,而是选择低延迟,所以更接近 EL。"购物车绝不能卡住"这样的需求催生了这个选择。两台设备里放进去的东西即便一时对不上,之后合并就好。
Google Spanner — CP(以及 EC)。 Spanner 是完全相反的极端。它是一个遍布全球的数据库,却依然提供强一致性(外部一致性,事实上等同于线性一致性)。分区发生时,无法达成共识(Paxos)的那一侧会停止写入。也就是说,为了一致性让出了可用性。即便在正常状况下,也要经过副本间的共识和精密的时间同步,所以延迟会增加,即 EC。Spanner 出名的原因在于,它靠原子钟和 GPS 打造的 TrueTime 这套装置,把这种强一致性以实用的性能做到了全球规模。但它并没有绕开 CAP。分区发生时,它依然要放弃可用性。
传统的单节点 RDBMS。 一台 PostgreSQL 或 MySQL 实例呢?这里没有被网络分割开的副本,所以分区这个概念本身就变得模糊。CAP 本质上是一个关于数据在多个节点间复制的定理。单节点在这个舞台之外。当然,一旦加上复制(replication),就重新站到了这个权衡的舞台上。
总结一下:没有哪个系统能说自己"打破了 CAP"或者"三者都拥有"。分区一旦发生,所有系统都必须在 C 和 A 之间做选择。差别只在于被设计成选哪一边。
CAP 的 C 和 ACID 的 C 不是一回事
这里必须点破一个非常常见的混淆。CAP 的 C 和 ACID 的 C,只是拼写相同,含义完全不同。
- CAP 的 C = 线性一致性。在多个副本之间,每个客户端看到的都是最新的值。这是关于分布式复制的性质。
- ACID 的 C = 一致性(consistency),但这里指的是"约束的保持"。事务不破坏数据库的不变式(外键、唯一约束、用户定义的规则等),只从一个有效状态转移到另一个有效状态。这是关于单个事务正确性的性质。
两者处在不同的层面上。ACID 的 C 是数据库信守应用定义的规则这一承诺,CAP 的 C 是多个副本展示相同值这一承诺。把这两者混为一谈,就会写出"CP 系统保证 ACID"这类错误的句子。实际上它们是彼此独立的概念。
再往深一层,分布式事务里常说的隔离级别最高一级——可串行化(serializability),和 CAP 的线性一致性(linearizability)也不是一回事。可串行化指的是多个事务的结果等价于某种串行执行;线性一致性指的是单个对象上的操作遵循实时顺序。把这两者结合起来的强保证叫做严格可串行化(strict serializability),Spanner 追求的大致就是这个点。术语相似容易混淆,所以要经常反问自己:"这到底是关于什么的顺序保证?"
一致性是一个光谱
CAP 把一致性当成"是否线性一致"的二分法来处理,但现实中的一致性是一个宽广的光谱。从强的一端走到弱的一端,看几个台阶,就有感觉了。
- 线性一致(linearizable):最强。每次读取都能看到最新的写入。
- 顺序一致性(sequential):所有客户端看到的操作顺序相同,但这个顺序不必和实时顺序一致。
- 因果一致性(causal):只有存在因果关系的操作才保持顺序。互不相关的操作即便顺序颠倒也没关系。
- 最终一致性(eventual):只要没有新的写入,所有副本迟早会收敛到同一个值。在那之前,可能看到旧值。
当 AP 系统说"放弃一致性"时,多数情况下并不是掉进彻底的无序,而是选择了这个光谱较弱的一端(比如最终一致性或因果一致性)。所以比起"AP 还是 CP"这种二分法,更贴近实务的问法是"你在用什么代价换取哪个层级的一致性"。
常见的误解
把到目前为止的内容,重新整理成一份误解清单:
- "平时三选二" —— 不对。没有分区时,C 和 A 都能享有。选择只在分区那一刻才被强制。
- "P 是可以放弃的选项" —— 不对。网络分区在现实中必然发生,所以在分布式系统里 P 事实上是必需的。
- "存在 CA 系统" —— 在多节点复制的系统里,无视分区的 CA 实际上不成立。分区一旦发生,最终还是会失去 C 或 A 中的一个。
- "CP 系统总是处于不可用状态" —— 不对。CP 系统只在分区发生的那一刻、在受影响的那部分让出可用性。平时照常响应。
- "CAP 的 C 就是 ACID 的 C" —— 不对。正如前一节所说,这是完全不同的概念。
- "最终一致性就是数据乱七八糟" —— 不对。它保证收敛,甚至还有维持因果关系这样更强的形式。
给设计者的实战指南
把这套理论落到实际决策上时,有几个有用的问题。
首先要问的是这份数据能不能容忍短暂地展示旧值。社交动态里的点赞数、商品的浏览量,这类数据即便短暂对不上也不是大事。这种场景下,AP 加低延迟是合理的。反过来,银行余额、库存数量、座位预订这类不能卖两次的数据,需要强一致性,所以会偏向 CP。
接下来要问的是是否明确设计了分区发生时的行为。很多事故都来自"分区不会发生"这个隐含假设。分区必然会来,所以要提前决定:"那一刻,是阻塞写入,还是先接受再事后和解?"如果选择了后者,还要一并定好冲突解决策略(最新时间戳优先、版本向量、CRDT 等)。
最后要问的是是否清楚正常状况下的延迟预算。正如 PACELC 的 ELC 所提醒的,强一致性即便在正常状况下也会带来延迟。要决定的是:能不能承担等待地球另一端的副本达成一致的成本,还是允许各地区采用较弱的一致性来削减延迟。
结语
CAP 定理是分布式系统里被引用得最多、也被误解得最多的结果之一。"三选二"这个带着比划的总结好记,但恰恰藏住了最要紧的真相。真正的故事精确得多。分区容错性事实上是必需的,真正的选择只发生在分区出现的那一刻,在一致性和可用性之间。而正如 PACELC 所提醒的,即便在没有分区的平时,一致性和延迟之间那场安静的权衡也一直在流动。
Dynamo 选了可用性,Spanner 选了一致性,不是因为哪一边更对,而是因为它们要解决的问题本来就不同。好的设计者不会宣称自己战胜了 CAP。相反,他们会清楚地回答这个问题:"对于这份数据,分区发生时,我愿意放弃什么?"把比划去掉之后,CAP 就不再是一个魔法三角形,而是一件诚实的工具,逼你去问出那个清楚的问题。
参考资料
- Eric Brewer, "CAP Twelve Years Later: How the 'Rules' Have Changed": https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/
- Gilbert & Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services": https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf
- Daniel Abadi, "Consistency Tradeoffs in Modern Distributed Database System Design (PACELC)": https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf
- Google Spanner 论文: https://research.google/pubs/pub39966/
- Amazon Dynamo 论文: https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- Jepsen: Consistency Models: https://jepsen.io/consistency