这篇 blog 已被翻译成 俄语, 日语 和 中文. 有关 CAP 问题的更多细节和其余选择方案的建议, 请阅读个人论文 A Critique of the CAP Theorem.html
Jeff Hodges 在他那篇杰出的 blog 文章 "Distributed Systems for Young Bloods" 中建议用 CAP 定理来评判一个系统. 这的确很深刻人心, 以致于这些人直接将他们的系统描述为 "CP" (consistent but not available under network partitions, 一致但在网络分区的状况下不可用), "AP" (available but not consistent under network partitions, 可用但在网络分区的状况下不一致), 或 "CA" (meaning "I still haven’t read Coda's post from almost 5 years Ago", 尚未读过 Coda 的5年前的那篇文章).git
我赞成 Jeff 的全部其余观点, 但关于 CAP 定理, 我持有不一样意见. CAP 定理过于精简, 且被普遍误解, 致使它对描述一个系统没有多大用处. 因此我恳请你们最好仍是放弃 CAP 定理并中止引用和讨论. 而且咱们要用更精确的术语来权衡系统.github
(我知道我写这篇 blog 是在讽刺大家, 但至少这样我就有了个 URL, 当别人问为何我不喜欢他们谈论 CAP 定理的时候我就能够甩给他们. 另外我很抱歉这篇文章像是在对大家吼, 但起码我吼的有理有据(lots of literature references)).web
若是想将 CAP 称为一个定理 (而不是数据库软文中模糊的缺少支撑的概念) 那就必需要精确. 数学是严谨的科学. 在证实过程中使用的词语必须含义相同, 证实才能成立. 但在证实中, 这些定义都是一些特殊场景, 例如:算法
并且还要注意 CAP 定理不只描述的是个旧的系统, 并且是一个很是特定的系统:mongodb
若是你使用的相关定义与上面描述的关于 CAP 的定义精确相符, 则 CAP 定理适合你. 不过若是使用其余的一致性或可用性的概念, 就不能期待 CAP 定理仍然适用. 固然这并不意味着从新定义一些词汇就超脱现实, 而是说不能把 CAP 定理做为指导, 并且不要用 CAP 定理来证实你的观点.数据库
若是CAP定理不适用, 那就意味着你必须本身去考虑系统中的权衡条件. 你能够本身定义一致性和可用性, 而且证实你的定理. 但不要叫 CAP 定理了, 由于已经被占用了.apache
若是你不熟悉 线性一致性 (Linearizability, 即 CAP 意义上的 "一致性, consistency"). 简要的解释一下, 正式的定义并非十分的直截了当, 不过定义的核心简单来说是:编程
若是操做 A 成功完成, 进行操做 B, 那么操做 B 必须观察到系统处于操做 A 完成, 或更新完毕的统一状态.
为了更具体, 考虑一个非线性化的系统, 见下图 (这个图出自我未发布的书中):bash
该图显示了位于同一房间的 Alice 和 Bob, 他俩都在刷手机看 2014 足球世界杯的决赛结果. 在最终比分公布后, Alice 刷新页面, 兴奋地告诉 Bob 获胜者已经公布了. Bob 半信半疑的刷新本身手机上的页面, 可是他的请求被转发到了一个滞后的数据库副本 (database replica), 因此他的手机上显示比赛仍在继续.
假如 Alice 和 Bob 同时从新加载, 那么他们获得了两个不一样的查询结果也是有可能的, 由于他们不知道服务器处理他们各自的请求的确切时间. 然而, Bob 是在听到 Alice 惊叹最终比分后才点击了从新加载按钮 (发起他的查询), 所以他但愿他的查询结果至少与 Alice 的同样, 然而却获得了旧的查询结果, 这一事实违反了线性一致性 (Linearizability).
Bob 的请求严格地发生在 Alice 的请求以后 (即它们不是并发的), 是由于 Bob 经过一个单独的通讯通道 (本例中是现实世界的声音) 听到 Alice 的查询结果. 若是 Bob 没有从 Alice 那里据说比赛结束了, 他就不会知道他的查询结果已通过期了.
若是你正在构建一个数据库, 是没法知道客户端可能有哪些其余通讯渠道 (backchannel) 的. 所以, 若是但愿在数据库中提供线性语义 (linearizable semantics 即CAP中的consistency), 就须要让数据看起来只有一个副本, 即便数据可能在多个位置有多个副本 (replicas, caches).
实现这样的保障很是困难, 由于它须要大量的协调. 甚至计算机中的 CPU 都不能提供对本地内存的线性访问! 在现代 CPU 上须要使用显式内存屏障指令 (explicit memory barrier instruction) 才能线性访问. 甚至测试一个系统是否能提供线性访问也是很棘手的.
让咱们简单地讨论下在网络分区 (network partition) 状况下放弃线性一致性 (linearizability) 或可用性 (availability) 的必要性.
假设在两个不一样的数据中心中都有数据库的副本 (replicas). 数据复制的确切方法目前并不重要 - 它多是单主 (主/从, master/slave), 多主 (主/主, master/master) 或基于 quorum (即节点选举制) 的副本 (相似 Amazon Dynamo). 副本的要求是, 在一个数据中心中写入数据的同时, 都必须将数据写入另外一个数据中心中的副本. 假设客户端只链接到一个数据中心, 那么当数据复制的时候就必须依赖两个数据中心之间的网络连接.
如今假设网络链接中断 - 这就是网络分区 (network partition) 的意思. 猜猜会发生什么?
很明显你只能二选一:
(顺便说这基本就是对CAP定理的证实了. 这就是他的所有内容. 本例中使用了2个数据中心, 但这也一样适用于单一数据中心中的网路问题. 只是想象成2个数据中心时比较好理解.)
注意, 在选项2中的理论上"不可用" (unavailable) 状况下, 咱们仍然在一个数据中心中愉快地处理着请求. 所以若是系统选择了线性一致性 (linearizability, 即它不是cap可用的), 这并不必定意味着网络分区会自动致使应用程序停机. 若是您能够将全部客户端请求转移到 leader 数据中心, 那么客户端实际上根本不会感受到停机时间.
在实践中, 可用性并不彻底与 CAP 可用性 (CAP-availability) 一致. 应用程序的可用性多是经过 SLA 来衡量的 (例如, 99.9% 的合法请求必须在1秒内成功返回响应), 可是这样的 SLA 指标 CAP 可用 (CAP-available) 和 CAP 不可用 (CAP-unavailable) 的系统都能知足.
在实践中, 多数据中心系统一般设计为异步复制, 所以是非线性的 (non-linearizable). 然而, 这种选择的缘由一般是因为广域网的延迟, 而不只仅是数据中心故障和网络故障容灾.
在 CAP定理 对一致性 (线性一致性) 和可用性的严格定义下, 系统如何运行?
例如, 以具备单 leader 的复制型数据库为例, 这是大多数关系型数据库中配置副本的标准方法. 在此配置中, 若是客户端与 leader 发生分区, 则没法写入数据库. 尽管或许能够在从数据库 (只读副本) 读取数据, 但不能写入的事实意味着任何单 leader 的配置都不是 CAP 可用的. 尽管这样的配置经常被营销为"高可用性".
若是单 leader 集群不算 CAP 可用, 那是否意味着是 "CP" 的? 没那么简单, 若是容许应用读取 follower 节点, 而且数据复制是异步的 (大多数数据库都是这样的),
那么当从 follower 进行读取时, follower 可能会稍微落后于 leader. 在这种状况下, 读取就不是线性可用的, 即不符合 CAP 一致性 (CAP-consistent).
此外, 具备 快照隔离(snapshot isolation)/MVCC的数据库是刻意设计成非线性的, 由于强制实现线性一致性会下降数据库的并发性能. 例如, PostgreSQL SSI 提供顺序一致性 (serializability), 但不提供线性一致性 (linearizability), 而 Oracle 这两个都不支持. 数据库称做"ACID"并不意味着它知足 CAP 定理中对一致性的定义.
因此这些系统不 CAP 一致也不 CAP 可用. 它们既不"CP"也不"AP", 只是"P", 不论"P"究竟是啥. ("三选二"的公式确实是能够直选一种的, 甚至三个都不选也能够!)
那么"NoSQL"呢? 以 MongoDB 为例: 每一个 shard 中有一个 leader (只要不是在裂脑模(split-brain)式下, 就应该是这样的), 所以上面的状况不符合 CAP, Kyle 最近展现 了即便在最高的一致性设置下, MongoDB 也容许非线性读取, 因此不是 CAP一致的.
还有相似 Dynamo 的衍生品: Riak, Cassandra, Voldemort. 这些一般被称做"AP"是由于针对高可用优化了么? 仍是取决于你的设置. 若是能接受单副本读写 (R=W=1), 那么它们确实是 CAP 可用的. 可是, 若是须要仲裁读写 (R+W>N), 而且遇到了网络分区, 节点较少的分区没法进行仲裁, 所以仲裁操做不是 CAP 可用的 (至少暂时不可用, 直到在节点较少的分区设置其余的数据库副本节点).
有人声称仲裁机制 (quorum) 的读写能够保证线性一致性 (linearizability), 但我认为依赖它不是个明智的选择, 好比一些特性的微妙组合: 松散仲裁 (sloppy quorums), 读修正 (read repair), 会导致棘手的边缘状况 (edge cases). 例如删除的数据又出现了, 副本数量小于本来的写节点数量 (W) (违反了仲裁机制), 副本节点数量增长超过了副本总数 (N) (仍是违反了仲裁机制). 这些状况都会致使结果不线性一致.
这些系统并非很糟糕, 你们一直在生产环境中正常使用. 然而到目前为止, 还不能严格地将它们分类为"AP"或"CP", 这要么是由于必需要进行特定的操做或配置, 要么是由于系统不知足 CAP 定理中对一致性或可用性的严格定义.
那么ZooKeeper如何? 它使用了一种共识算法, 所以人们一般将其视为选择一致性放弃可用性(即"CP系统")的明确案例.
可是, 若是查看ZooKeeper文档, 就会发现ZooKeeper在默认状况下不提供线性读取. 每一个客户端链接到一个服务器节点, 当进行读取时, 只能看到该节点上的数据, 不管你在另外一个节点上写入多少新数据. 这会比起每次读取都必须获取仲裁信息或每次读取都与主节点进行通讯要快得多, 但这也意味着 ZooKeeper 在默认状况下不知足CAP定理中对一致性的定义.
在ZooKeeper中能够经过在读取以前使用同步(sync)命令进行线性化读取, 但不是默认的, 由于这会带来性能损失. 确实有人会使用会使用同步, 但一般不是每次操做都用.
ZooKeeper 的可用性如何? ZooKeeper 须要达成多数仲裁才能达成一致, 也就是说, 才能处理写操做. 若是发生分区, 其中一侧是多数节点, 另外一侧是少数节点, 那么多数节点侧将继续可用, 可是少数节点侧就不能处理写操做, 即便节点都是正常 (up) 的. 所以, ZooKeeper 的写操做在分区状况下不是 CAP 可用的 (即便多数节点侧能够继续处理写操做).
有意思的是, ZooKeeper 3.4.0 添加了只读模式, 在这种模式下,分区状况下的少数节点侧能够继续提供读请求 - 并且不须要仲裁! 这个只读模式是 CAP 可用的. 所以, ZooKeeper 在默认状况下既不是 CAP一致的 (CP), 也不是 CAP 可用的 (AP). 它实际上就是个"P". 但若是须要, 能够经过调用sync让它变成CP的, 若是打开正确的选项, 对于读操做 (而不是写操做), 它其实是AP的.
但这使人恼火. 仅仅由于 ZooKeeper 在默认状况下不是线性化的, 就称做"不一致", 那就严重歪曲了它的特性. 实际上 ZooKeeper 提供了很是好的一致性! 它提供的 原子化广播 (可简化为共识机制) 结合了 session保障 (session guarantee) 的因果一致性 (causal consistency) , 这可比读取写入, 单调读取 (monotonic reads) 和一致前缀读取 (consistent prefix reads) 的组合更强大. 文档说 ZooKeeper 提供了顺序一致性, 可是这是低估了本身, 由于 ZooKeeper 的保证明际上比顺序一致性 (sequential consistency) 强不少.
正如ZooKeeper所展现的, 在存在分区的状况下, 拥有一个既不 CAP 一致也不 CAP 可用的系统是很是合理的, 并且在没有分区的状况下, 系统在默认甚至不是线性化的. (我想在 Abadi 的 PACELC 理论框架里, 这应该算是PC/EL的了, 但我以为仍是CAP比较有启发性.)
事实上咱们很难将一个数据存储系统明确地划分为"AP"或"CP", 这一事实证实了: 这些根本不是系统的正确描述方式.
我认为咱们不该将数据存储系统归类为"AP"或"CP", 由于:
若是CP和AP不适合描述和评价系统, 那么应该使用什么来替代呢? 我认为答案不是惟一的. 不少人都认真思考过这些问题, 并提出了术语和模型来帮助咱们理解问题. 要了解这些观点, 必须深刻研究文献.
不管你选择怎样的学习方式, 我鼓励你保持好奇心和耐心 - 这些东西来之不易. 可是这是有好处的,由于你学会了权衡利弊, 从而肯定哪一种体系结构最适合您的特定应用程序. 可是不管你作什么, 请中止谈论CP和AP, 由于它们没有任何意义.
感谢 Kyle Kingsbury 和 Camille Fournier 对本文草稿的注解. 固然, 任何错误或使人不快的意见都是个人锅.