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在那篇文章中提出了处理分区的一个三步框架:
- 检测分区:通过超时或其他机制检测到通信失败。
- 进入分区模式:限制某些操作,记录额外的元数据以便恢复。
- 分区恢复:当通信恢复后,协调两侧的状态,修复任何违反的不变式。
这个框架的核心思想是:分区是一种临时状态,系统应该能够优雅地处理它,而不是简单地"选择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定理是有价值的,但它的价值在于揭示了约束的存在,而非提供简单的答案。理解这个定理的正确含义,是设计可靠分布式系统的第一步。
参考文献
- Brewer, E. (2000). Towards robust distributed systems. Proceedings of the Annual ACM Symposium on Principles of Distributed Computing.
- 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.
- Brewer, E. (2012). CAP twelve years later: How the “rules” have changed. IEEE Computer, 45(2), 23-29.
- Abadi, D. (2010). Consistency tradeoffs in modern distributed database system design: CAP is only part of the story. CIDR 2011.
- Kleppmann, M. (2015). Please stop calling databases CP or AP. martin.kleppmann.com.
- Bailis, P. (2014). Linearizability versus serializability. bailis.org.
- Hale, C. (2010). You Can’t Sacrifice Partition Tolerance. codahale.com.
- Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011). Conflict-free replicated data types. Symposium on Self-Stabilizing Systems.