Skip to content

필사 모드: CAP 定理,不打马虎眼地讲清楚

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — "选二"的问题

学分布式系统时,你几乎必然会遇到 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 就不再是一个魔法三角形,而是一件诚实的工具,逼你去问出那个清楚的问题。

参考资料

현재 단락 (1/65)

学分布式系统时,你几乎必然会遇到 CAP 定理。而且几乎必然是以这种压缩过的形式遇到它:"一致性(Consistency)、可用性(Availability)、分区容错性(Partition tole...

작성 글자: 0원문 글자: 5,289작성 단락: 0/65