Martin Kleppmann | 作者
Lu Pan | 译者
来源:https://blog.the-pans.com/cap/
Kirito: 本文的原作者是 Martin Kleppmann, 著有《Designing Data-Intensive Applications》一书,无论是这本书还是这篇文章,都能站在一个独特的视角去阐释那些可能被大多数人误解的理念,让读者醍醐灌顶。在此之前我就隐约对文中提到的一些 CAP 误解嗤之以鼻,这篇文章让我更加确信了之前零碎的认知,不夸张地讲,这应该是我看过的最通俗也是最深刻的 CAP 科普文。
在 Jeff Hodges 精彩的博客文章给年轻人关于分布式系统的笔记中,他建议我们用 CAP 定理来评论系统。很多人都听取了这个建议,描述他们的系统为"CP" (有一致性但在网络分区的时候不可用),“AP”(可用但是在网络分区的时候不一致) 或者有时候 "CA" (说明"我还没有读过 Coda 的五年前的文章")。
我同意 Jeff 的所有观点。唯独他关于 CAP 定理的观点,我必须表示不同意。CAP 定理本身太简单化而且被广泛的误解,以至于在描述系统上没有太多用处。因此我请求我们不要再引用 CAP 定理,不要再讨论 CAP 定理。取而代之,我们应该用更精确的术语来理解我们系统的权衡。
(没错,我意识到很讽刺的是我不希望别人再讨论这个话题,但我却正在一篇关于这个话题的博客文章。但是至少这样以后别人问我为什么不喜欢讨论 CAP 定理的时候,我可以把这篇文章的链接给他。还有,抱歉这篇文章有些吐槽,但是至少这个吐槽有文献引用。)
CAP 用的是非常精确的定义
如果你想引用 CAP 作为一个定理(而不是一个模糊的,用来做数据库市场营销的概念),你需要用非常精确的定义。数学要求精确。只有当你的用词和定理的证明中的定义是一样的时候,这个证明才有意义。CAP 的证明用的是非常具体的定义。
- 一致性(Consistency) 在CAP中是可线性化的意思(linearizability)。而这个是非常特殊(而且非常强)的一致性。尤其是虽然 ACID 中的 C 也是一致性(Consistency),但是和这里的一致性没有任何关系。我会在后面解释可线性化是什么意思。
- 可用性(Availability)在 CAP 中是定义为"每一个请求(request)如果被一个工作中的[数据库]节点收到,那一定要返回[非错误]的结果"。注意到,这里一部分节点可以处理这个请求是不充分的。任意一个工作中的节点都要可以处理这个请求。所以很多自称"高度可用"的系统通常并没有满足这里的可用性的定义。
- 分区容错(Partition Tolerance)基本上就是说通信是在异步的网络中。信息是可能延迟送达或者被丢失的。互联网还有我们所有的数据中心都有这个属性。所以我们在这件事上并没有选择。
还有就是注意到 CAP 并没有描述任意一个老的系统,而是一个非常特殊的系统:
- CAP 系统的模型是一个只能读写单个数据的寄存器。这就是全部。CAP 没有提到任何关于关系到多个事物(object)的事务(transaction)。他们根本就不在这个定理的范围之内,除非你可以把这些问题约化到一个单个寄存器的问题。
- CAP 定理只考虑了网络分区这一种故障情况(比如节点们还在运行,但是他们之间的网络已经不工作了)。这种故障绝对会发生,但是这不是唯一会出故障的地方。节点可以整个崩溃(crash)或者重启,你可能没有足够的磁盘空间,你可能会遇到一个软件故障(bug),等等。在建分布式系统的时候,你需要考虑到更多得多的问题。如果太关注 CAP 就容易导致忽略了其他重要的问题。
- 还有 CAP 根本没有提到延迟(latency)。而常常人们其实对关心延迟比可用性更多。事实上,满足 CAP 可用性的系统可以花任意长的时间来回复一个请求,而且同时保持可用性这个属性。我来冒险说一句,我猜如果你的系统要花两分钟来加载一个页面,你的用户是不会称它是“可用的”。
如果你的用词是符合 CAP 证明中的精确定义的,那么它对你来说是适用的。但是如果你的一致性还有可用性是有其他意思的,那么你不能期待 CAP 对你还是适用的。当然,这并不意味着你通过重新定义一些词汇就可以做到一些不可能的事情!这只是说你不能靠 CAP 来给你提供指导方向,而且你不能通过 CAP 来为你的观点来辩解。
如果 CAP 定理不适用,那么这就意味着你必须自己来考虑取舍。你必须根据你自己对一致性还有可用性的定义来思考这些属性,而且你能证明自己的定理就更好了。但是请不要称它为 CAP 定理,因为这个名字已经被用了。
可线性化
如果你对可线性化不是很熟悉(也就是 CAP 中的一致性),那么让我来简短地解释一下。正式的定义不是特别直观,但是关键的思想用非正式的描述就是:
如果B操作在成功完成 A 操作之后,那么整个系统对 B 操作来说必须表现为 A 操作已经完成了或者更新的状态。
为了可以解释的更清楚一些,让我们来看一个例子。在这个例子中的系统并不是可线性化的。 看下面这个图(我还没有发行的书的预览):
这张图展示了 Alice 还有 Bob, 他们在同一个房间,都在用他们的手机查询 2014 年世界杯的决赛结果。就在最终结果刚发布之后,Alice 刷新了页面,看到了宣布冠军的消息,而且很兴奋地告诉了 Bob。Bob 马上也重新加载了他手机上的页面,但是他的请求被送到了一个数据库的拷贝,还没有拿到最新的数据,结果他的手机上显示决赛还正在进行。
如果 Alice 和 Bob 同时刷新,拿到了不一样的结果,并不会太让人意外。因为他们不知道具体服务器到底是先处理了他们中哪一个请求。但是Bob知道他刷新页面是在 Alice 告诉了他最终结果之后的。所以他预期他查询的结果一定比 Alice 的更新。事实是,他却拿到了旧的结果。这就违反了可线性化。
只有 Bob 通过另外一个沟通渠道从 Alice 那里知道了结果, Bob 才能知道他的请求一定在 Alice 之后。如果 Bob 没有从 Alice 那里听到比赛已经结束了,他就不会知道他看到的结果是旧的。
如果你在建一个数据库,你不知道用户们会有什么另外的沟通渠道。所以,如果你想提供可线性化(CAP 的一致性),你就需要让你的数据库看起来就好像只有一个拷贝,虽然实际上可能有多个备份在多个地方。
这是一个非常昂贵的属性,因为它要求你做很多协调工作。甚至你电脑上的 CPU 都不提供本地内存的可线性化访问! 在现代的 CPU 上,你需要用 memory barrier 指令来达到可线性化访问。甚至测试一个系统是不是可线性化的也是很困难的。
CAP 可用性
让我们来简短的讨论一下为什么在网络分区的情况下,我们要放弃可用性和一致性中的一个。
举个例子,你的数据库有两个拷贝在两个不同的数据中心。具体怎么做备份并不重要,可以是 single-master,或者多个 leader,或者基于 quorum 的备份(Dynamo 使用的方式)。要求是当数据被写到一个数据中心的时候,他也一定要被写到另一个数据中心。假设 client 只连接到其中一个数据中心,而且连接两个数据中心的网络故障了。
那么现在假设网络中断了,这就是我们所说的网络分区的意思。接下来怎么样呢?
显然你有两个选择:
- 你的应用还是被允许写到数据库,所以两边的数据库还是完全可用的。但是一旦两个数据库之间的网络中断了,任何一个数据中心的写操作就不会在另一个数据中心出现。这就违反了可线性化(用之前的例子,Alice 可能链接到了一号数据中心,而 Bob 连接到了二号数据中心)。
- 如果你不想失去可线性化,你就必须保证你的读写操作都在同一个数据中心,你可能叫这它 leader。另一个数据中心,因为网络故障不能被更新,就必须停止接收读写操作,直到网络恢复,两边数据库又同步了之后。所以虽然非 leader 的数据库在正常运行着,但是他却不能处理请求,这就违反了 CAP 的可用性定义。
(而这个,其实就是 CAP 定理的证明。这就是全部了。这里的例子用到了两个数据中心,但是对于一个数据中心内的网络故障也是同样适用的。我只是觉得用两个数据中心这样更容易考虑这个问题。)
注意到上面第二点,就算它违反了 CAP 的可用性,但我们还是在成功地处理着请求。所以当一个系统选择了可线性化(也就是说不是 CAP 可用的),这并不一定意味着网络分区一定会造成应用停运。如果你可以把用户的流量转移到leader数据库,那么用户根本就不会注意到任何问题。
实际应用中的可用性和 CAP 可用性并不相同。你应用的可用性多数是通过 SLA 来衡量的(比如 99.9% 的正确的请求一定要在一秒钟之内返回成功),但是一个系统无论是否满足 CAP 可用性其实都可以满足这样的 SLA。
实际操作中,跨多个数据中心的系统经常是通过异步备份(asynchronous replication)的,所以不是可线性化的。但是做出这个选择的原因经常是因为远距离网络的延迟,而不是仅仅为了处理数据中心的网络故障。
很多系统既不是可线性化的也不是 CAP 可用的
在 CAP 对可用性还有一致性严格的定义下,系统们表现怎么样?
拿任意一个 single master 的有备份的数据库作为一个例子。这也是标准的数据库设置。在这种情况下,如果用户不能访问 leader,就不能写到数据库。虽然他还能从 follower 那里读到数据,但是他不能写任何数据就说明它不是 CAP 可用的。更不要说这种设置还常常声称自己是“高可用的(high availablity)”。
如果以上这种设置不是 CAP 可用的,那是不是就是说他满足 CP(一致)?等一下。如果你是从 follower 那里读到的数据,因为备份是异步的,所以你可能读到旧的数据。所以你的读操作不是可线性化的,所以不满足 CAP 中的一致性。
而且支持 snapshot isolation/MVCC 的数据库是故意做成不可线性化的。否则会降低数据库的并发性。比如 PostgreSQL 的 SSI 提供的是可串行化而不是可线性化,Oracle 两者都不支持。仅仅因为数据库标榜自己是 ACID 并不意味着它就满足 CAP 中的一致性。
所以这些系统既不是 CAP 一致的,也不是 CAP 可用的。他们既不是 CP 也不是 AP,他们只是 P,不管这是什么意思。(是的,“三选二”也允许你只从三个中选一个,甚至一个都不选!)
那 NoSQL 怎么样的?拿 MongoDB 作为一个例子:每一个 shard 都只有一个 leader(至少只要他不在 split-brain 的模式下,它应该是这样的),根据以上的论证,那就说明他不是 CAP 可用的。而且 Kyle 最近发现,设置了最强的一致性,他还是允许非一致性的读操作,所以它也不是 CAP 一致的。
那像 Riak, Cassandra 还有 Voldemort 这些声称是 AP 的高可用的 Dynamo 的继承者们又怎么样呢?这取决于你的设置。如果你接受读写只访问一个拷贝(R=W=1),那么这确实是 CAP 可用的。但是如果你要求 quorum 读写(R W>N),而且你有网络分区,那么那些被分在少部分节点的用户就不能达到 quorum,所以 quorum 操作不是 CAP 可用的(至少暂时是不可用的,直到你在少部分的分区内加入了更多的节点)。
你有时候会看到人们声称 quorum 读写可以保证可线性化,但是我觉得依赖这样的声明是不明智的。因为在一些复杂的情况下,read repair 操作和 sloppy quorum 同时发生,就有可能会重写已经被删除了的数据。或者当备份数(replicas)已经低于原来的 W 值(违反了 quorum 的条件),或者当备份数被加到了高于原来的 N 值(还是违反了 quorum 的条件),这些都可以导致不可线性化的访问结果。
这些都不是差的系统:他们在实际运用中都很成功。但是目前为止,我们还是不能严格把他们分类为 AP 或者 CP,要么是因为取决于具体的设定,或者是因为这个系统一致性和可用性都不满足。
案例分析:ZooKeeper
那 ZooKeeper 又怎么样呢?他用了 consensus 算法,所以人们一般认为他是很清楚的选择了一致性而放弃了可用性(也就是CP 系统)。
但是如果你阅读 ZooKeeper 的文档,他们很清楚的说了 ZooKeeper 的默认设置不提供可线性化的读操作。每一个连接到一个服务器的客户端,当你要读的时候,即使别的节点有更新的数据,你只能看到那个服务器本地的数据。这样读操作就比需要收集 quorum 或者访问 leader 要更快。但这也说明 ZooKeeper 默认不满足 CAP 的一致性定义。
做可线性化的读操作在 ZooKeeper 中是支持的。你需要在读操作之前发一个 sync 命令。但这不是默认的设置,因为这样读操作会更慢。人们有时候会用 sync 命令,但一般不会是所有的读操作都用。
那 ZooKeeper 的可用性呢?他要求达到大多数 quorum,来达到共识,才能处理一个写操作。如果你有网络分区,一边有大多数节点,一边有少部分节点。那么拥有大多数节点的分区还可以继续工作,但是少部分节点的分区就算节点们都正常工作着,还是不能处理写操作。所以 ZooKeeper 的写操作在网络分区的情况下,不满足 CAP 的可用性(即使拥有大多数节点的分区还是可以处理写操作的)。
更有意思的是,ZooKeeper 3.4.0 还加入了一个只读的模式。在这个模式下,少部分节点的分区还可以继续处理读操作 -- 不需要 quorum! 这个读操作是满足 CAP 可用性的。所以 ZooKeeper 默认设置既不是一致的(CP)也不是可用的(AP),只是"P"。但是你有选择通过用 sync 命令来让它成为 CP。并且在正确的设置下,读操作(不包括写)其实是 CAP 可用的。
这让人不是很舒服。如果就因为 ZooKeeper 的默认设置不是可线性化的就称他为不一致,那就歪曲了他的功能。他其实可以提供非常强的一致性!他支持 atomic broadcast(这个可以约化为共识问题)以及每个 session 的 causal consistency -- 这比 read your writes, monotonic reads 还有 consistent prefix reads 在一起都要强。他的文档上说 ZooKeeper 提供可串行化的一致性,但这其实是过于谦虚了,因为他其实可以提供更强的一致性。
根据 ZooKeeper 的例子,你就会发现就算这系统在网络分区的时候既不是 CP 也不是 AP(甚至在默认设置下,就算没有网络分区,也不是可线性化的),但他还是很合理的。(我猜 ZK 在 Abadi 的 PACELC 的框架下是 PC/EL,但我不觉得这比 CAP 更有启发性。)
CP/AP:一个伪二分法
事实上我们都没有成功地把一个数据库无歧义地分类为 AP 或者 CP。这应该告诉我们 CP/AP 根本就不是合适的用来描述系统的标签。
我相信我们应该不要再把数据库归类为 AP 或者 CP 了,因为
- 在同一个软件内,你可能有多个一致性属性的选择
- 很多系统在 CAP 的定义下,既不是一致也不可用。然而我从来没有听到别人称这些系统为"P",可能是因为这样不太好看。但这并不差,他很可能是完全合理的设计,他只是不在 CP/AP 这两个分类中。
- 虽然大部分软件都不在 CP/AP 这两类中,但人们还是强行把软件分为这两类。这就导致了,为了适用,不可避免地改变对“一致性”或者“可用性”的定义。不幸的是,如果用词的定义改变了,CAP 定理自己也不适用了,那 CP/AP 区分也就完全没有意义了。
- 把系统分为这两类,导致了很多细节被忽略。在考虑分布式系统设计的时候,会有很多关于容错,延迟,简单模型,运行成本,等等的考虑。把那么多细节编码到一个比特的信息,显然是不可能的。比如说虽然 ZooKeeper 有一个 AP 的只读模式,但这个模式也提供对所有写操作的 total ordering。这比 Riak 或者 Cassandra 这些 AP 系统提供的保障要强得多。所以简单地把他们都归为 AP 一个类别就显得很不合理。
- 甚至 Eric Brewer 承认 CAP 是一个容易误导人的而且过于简化的模型。在 2000 年,CAP 的意义在于让大家开始讨论关于分布式系统的取舍。他在这方面做得很好。但是他不是用来作为一个正式的突破性的结果,也不是一个严格的数据系统的分类方式。15 年之后,我们已经有了多得多的有不一样一致性和容错模型的系统。CAP 已经完成了他自己的使命,现在是时候不要在纠结了。
学会独立思考
如果 CP 和 AP 用来描述和评论系统是不合适的,那么我们应该用什么呢?我不认为有一个唯一的答案。很多人花了很多心思考虑这些问题,也提出了术语和模型来帮助我们理解这些问题。想要学习这些思想,你就需要更深入自己阅读文献。
- 一个很好的起点就是 Doug Terry 的论文。其中他用棒球来解释了各种不一样的最终一致性。可读性很强,而且就算对像我这样不是美国人而且完全不懂棒球也解释的很清晰。
- 如果你对 transaction 的 isolation 模型有兴趣(这和分布式系统的一致性不一样,但是相关),我的小项目 Hermitage 你可以看一下。
- 这篇论文讨论了分布式系统的一致性和 transaction 的 isolation 以及可用性之间的关系。(这篇论文也描述了不同一致性之间的分级。Kyle Kingsbury 很喜欢给别人讲这个。)
- 当你读到过这些了以后,你应该已经准备好深入阅读论文。我在这篇文章中加入了很多对文献的引用。去看一下,很多专家已经帮你把很多问题都已经解决了。
- 作为最后的手段,如果你不想读论文原文,我建议你看一下我的书。这本书用通俗易懂的方式总结了大多数重要的思想。(你看,我已经竟可能的让这篇文章看上去不是用来推销我的书的。)
- 如果你想学跟多关于怎么正确使用 ZooKeeper,Flavio Junqueira 还有 Benjamin Reed 的书是非常不错的。
不管你选择哪一种学习方式,我都鼓励你保持好奇心和耐心,因为这不是容易的学科。但是这是有回报的,因为你学会如果考虑取舍,进而搞清楚什么样的架构对于你的应用是最合适的。但是不管你做什么,请不要再说 CP 还有 AP 了,因为根本不合理。
谢谢 Kyle Kingsbury 还有 Camille Fournier 对于这篇文章初稿的评论。当然,所有的错误还有不受欢迎的观点都是我本人的。