2000年7月,加州大学伯克利分校的Eric Brewer在ACM Symposium on Principles of Distributed Computing上做了一个主题演讲。他提出了一个看似简单的观点:分布式系统不可能同时提供一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。

两年后,MIT的Seth Gilbert和Nancy Lynch在SIGACT News上发表了形式化证明,将"Brewer猜想"变成了"CAP定理"。此后二十多年,这个定理几乎成为了分布式系统设计的基石——也被严重误读了同样长的时间。

“三选二"的说法简洁有力,易于传播,但恰恰因为这种简化,它成为了分布式系统领域被误解最深的公理。

“CA系统"的悖论

打开任何一篇介绍CAP的文章,你几乎总会看到类似的分类表格:某数据库是"CP系统”,某数据库是"AP系统”,还有少数标榜自己为"CA系统"——提供一致性和可用性,但不提供分区容错。

问题在于:在分布式系统中,分区容错性不是一个可选项。

Gilbert和Lynch在原始论文中对分区容错性的定义是:网络可能丢失任意数量的消息。当网络被分区时,一个分区的节点发送给另一个分区的消息全部丢失。他们特别指出,任何消息丢失模式都可以建模为临时的网络分区。

这意味着什么?如果你声称自己的系统是"CA"——不需要分区容错——你实际上是在声称:你的网络永远不丢消息,你的节点永远不死。

这种系统在现实中不存在。即使是单机数据库,如果它接受远程客户端连接,也面临着网络分区的问题:客户端与服务器之间的网络中断,本质上就是一种分区。正如Coda Hale在2010年的一篇文章中尖锐指出的:你不可能选择"不分区",因为网络本身就是不可靠的。

更具体地说,随着节点数量增加,系统出现故障的概率呈指数级上升。如果单个节点在某个时间段内有99.9%的概率不发生故障,一个40节点的集群在同一时间段内不出故障的概率只有96.1%——也就是说,你有约4%的概率遇到某种形式的故障。这还是假设故障相互独立的情况,现实中故障往往会级联发生。

所以,真正的问题从来不是"选两个",而是:当分区发生时,你选择牺牲一致性还是牺牲可用性?

一致性的确切含义

CAP定理中的"一致性"也有特定的技术含义,它不等于ACID中的C。

在ACID事务中,一致性指的是数据库从一个合法状态转换到另一个合法状态——所有约束、触发器、级联操作都必须得到满足。这是应用层面的正确性。

但在CAP定理中,一致性指的是线性一致性(Linearizability),也称为原子一致性或强一致性。这是一个关于读写操作顺序的形式化定义。

线性一致性的核心要求是:如果写操作W在时间t完成,那么任何在t之后开始的读操作都必须能看到W的结果(或更新的结果)。用更直观的话说:一旦某个客户端完成了写入,所有后续的读取都应该能看到这个写入——不存在"时光倒流",不会有人看到更旧的数据。

Peter Bailis在2014年的一篇博客中给出了一个生动的例子:Alice和Bob在同一个房间里看世界杯决赛。Alice刷新手机看到了比赛结果,兴奋地告诉Bob。Bob随即刷新自己的手机,但他的请求被路由到了一个落后的副本——结果显示比赛还在进行中。

如果Bob不知道Alice已经看到了结果,这并不违反线性一致性,因为两个请求可能是并发的。但Bob知道他刷新的请求是在Alice告诉他结果之后发起的,所以他的手机应该显示至少和Alice一样新的信息。没有做到这一点,就违反了线性一致性。

这个例子揭示了线性一致性的一个关键特征:它是一种实时保证,依赖于墙上时钟的先后顺序。这也意味着,即使没有真正的分布式系统,仅靠客户端之间的"旁路通信"(这里是通过口头交流),就可以判断系统是否违反了线性一致性。

线性一致性是最强的一致性模型,但也是最昂贵的。因为它要求所有副本在任何时刻都对外呈现完全相同的状态。在分布式系统中,这意味着每次写入都需要等待所有副本确认——跨越数据中心的往返延迟可能达到数十甚至数百毫秒。

可用性的严格定义

CAP定理中的"可用性"同样比日常理解更为严格。

Gilbert和Lynch的定义是:系统中每一个非故障节点都必须能对请求做出响应。注意,是"每一个",而不是"至少一个"。如果系统有五个节点,其中两个因为网络分区无法响应,那么即使在另外三个节点上系统仍能正常工作,按照CAP的定义,这个系统也是不可用的。

这个定义的严格程度超出了大多数人的想象。即使是一个配置了主从复制的传统关系型数据库,如果客户端被分区到了从节点一侧,它就无法写入——因为只有主节点能接受写入。按照CAP的定义,这已经不符合可用性了。

更关键的是,CAP的可用性定义与工程实践中的"高可用"是两回事。在实际系统中,我们通常用SLA(服务等级协议)来定义可用性,比如"99.99%的请求在100毫秒内返回成功结果"。但CAP定理的可用性定义允许系统用任意长的时间来响应——只要最终能响应就算可用。一个响应时间长达2分钟的系统,在CAP的定义下仍然是"可用"的,但任何用户都不会认为这是一个可用的系统。

这也是为什么Martin Kleppmann在2015年发表了一篇题为"Please stop calling databases CP or AP"的文章。他指出,按照CAP的严格定义,大多数所谓的"高可用"系统实际上既不是CAP-可用,也不是CAP-一致。它们既不是CP也不是AP——它们只是"P"。

以ZooKeeper为例。作为一个共识系统的实现,ZooKeeper通常被认为是典型的"CP系统"。但Kleppmann指出,ZooKeeper默认情况下并不提供线性一致性的读取。每个客户端连接到一个服务器节点,读取操作只看到该节点上的数据,即使其他节点上有更新的写入。这违反了线性一致性,所以ZooKeeper默认不是CAP-一致的。

同时,ZooKeeper需要多数派才能处理写入。如果一个分区只包含少数派节点,这些节点无法处理写入——所以ZooKeeper也不是CAP-可用的。

ZooKeeper是一个优秀的分布式协调系统,提供包括因果一致性在内的多种保证。但按照CAP的二分法,我们甚至无法给它一个合适的分类。

“三选二"忽略了什么

“三选二"的表述最大的问题在于:它暗示了大多数时候我们只需要在C和A之间做一次选择,然后系统就会按照这个选择运行。

但现实远比这复杂。

首先,分区是罕见事件。正如Eric Brewer在2012年回顾CAP定理的文章中承认的,CAP禁止的设计空间其实很小:它只是说在分区存在时,你不能同时拥有完美的可用性和完美的一致性。但分区并不总是存在。在分区不存在的大部分时间里,系统完全可以同时拥有C和A。

Brewer在那篇文章中提出了处理分区的一个三步框架:

  1. 检测分区:通过超时或其他机制检测到通信失败。
  2. 进入分区模式:限制某些操作,记录额外的元数据以便恢复。
  3. 分区恢复:当通信恢复后,协调两侧的状态,修复任何违反的不变式。

这个框架的核心思想是:分区是一种临时状态,系统应该能够优雅地处理它,而不是简单地"选择C"或"选择A”。实际上,在一个设计良好的系统中,不同操作可以有不同的选择——甚至同一个操作在不同情况下可以有不同的选择。

其次,一致性是一个谱系,不是非黑即白的选择。在"强一致性"和"最终一致性"之间,存在着丰富的一致性模型层级:

  • 线性一致性(Linearizability):最强的实时保证。
  • 顺序一致性(Sequential Consistency):所有进程看到相同的操作顺序,但不一定是实时顺序。
  • 因果一致性(Causal Consistency):有因果关系的操作必须按顺序可见,独立操作可以任意顺序。
  • 会话一致性(Session Consistency):同一个客户端看到的操作顺序一致(读己之写、单调读等)。
  • 最终一致性(Eventual Consistency):如果没有新的写入,最终所有副本会收敛到相同的值。

这些一致性模型形成了一个从强到弱的谱系。在这个谱系中做选择,远比"C或非C"的二选一更有意义。

第三,延迟是更常面对的权衡。即使没有分区,分布式系统也面临着一个更频繁的取舍:一致性与延迟。

Daniel Abadi在2010年的CIDR会议上提出了PACELC模型,作为CAP的扩展:

  • 如果存在分区(P),必须在可用性(A)和一致性(C)之间选择。
  • 否则(Else),必须在延迟(L)和一致性(C)之间选择。

PACELC揭示了一个被CAP掩盖的事实:在正常操作(没有分区)时,一致性要求越高,延迟就越高。同步复制需要等待所有副本确认,跨数据中心的同步可能增加数百毫秒的延迟;异步复制则可以立即返回,但可能导致数据丢失或不一致。

Abadi在论文中分析了几个主流数据库:

  • Dynamo、Cassandra、Riak:PA/EL系统。分区时选择可用性,正常时选择低延迟。
  • 传统关系数据库(主从复制):PC/EC系统。分区时选择一致性,正常时也选择一致性(同步复制)。
  • Google Spanner:PC/EC系统,但通过创新的TrueTime API和两阶段提交,在保持一致性的同时最小化延迟。

这个框架比CAP更能指导实际的系统设计决策。

正确理解的意义

为什么正确理解CAP定理如此重要?

因为错误的"三选二"思维会导致错误的设计决策。

如果你认为分区容错是可选的,你可能会设计一个"CA系统”,然后在真实世界的网络故障面前措手不及。如果你认为一致性是二元选择,你可能会错过因果一致性这样的折中方案——它在提供有意义保证的同时,性能损失远小于线性一致性。如果你只关注CAP而忽略延迟,你的系统可能在"技术上可用"的同时,实际上已经无法使用了。

2018年10月21日,GitHub经历了一次持续24小时的服务中断。事后分析显示,一次43秒的网络分区导致了数据库集群的分裂。虽然这不是一个CAP选择的直接例子,但它展示了网络分区的真实性和严重性——即使在有完善基础设施的云环境中,分区仍然会发生。

分布式系统的设计,本质上是在约束条件下寻找最优解的过程。CAP定理告诉我们一个约束的存在,但它不应该成为思维的枷锁。正如Brewer在2012年文章结尾所说:现代CAP的目标应该是最大化一致性和可用性的组合——根据特定应用的需求,做出有意义的选择。

在分区发生的瞬间做出选择是必然的。但更重要的是,如何在分区结束后恢复一致性,如何在正常操作时平衡延迟与一致性,如何设计操作语义使得系统能够自动收敛——这些才是分布式系统设计的真正挑战。

CAP定理是有价值的,但它的价值在于揭示了约束的存在,而非提供简单的答案。理解这个定理的正确含义,是设计可靠分布式系统的第一步。


参考文献

  1. Brewer, E. (2000). Towards robust distributed systems. Proceedings of the Annual ACM Symposium on Principles of Distributed Computing.
  2. Gilbert, S., & Lynch, N. (2002). Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services. ACM SIGACT News, 33(2), 51-59.
  3. Brewer, E. (2012). CAP twelve years later: How the “rules” have changed. IEEE Computer, 45(2), 23-29.
  4. Abadi, D. (2010). Consistency tradeoffs in modern distributed database system design: CAP is only part of the story. CIDR 2011.
  5. Kleppmann, M. (2015). Please stop calling databases CP or AP. martin.kleppmann.com.
  6. Bailis, P. (2014). Linearizability versus serializability. bailis.org.
  7. Hale, C. (2010). You Can’t Sacrifice Partition Tolerance. codahale.com.
  8. Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011). Conflict-free replicated data types. Symposium on Self-Stabilizing Systems.