谈谈 ACID、CAP 和 BASE

在《从 Paxos 到 Zookeeper:分布式一致性原理和实践》里有一章”从ACID到CAP/BASE”,作者从单机事务的 ACID 讲到分布式事务,然后突然扯到CAP/BASE 理论。那么 ACID 和 CAP 有什么关系,为什么会放在一起讨论,作者并没有表述清楚。本文顺着这二者的关系进行讨论。

背景知识

ACID 原则

ACID 是指数据库管理系统在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

CAP 理论

来自Berkerly的Eric Brewer教授提出了一个著名的CAP理论:一致性(Consistency),可用性(Availability)以及分区容忍性(Partition tolerance)三者不能同时满足。

  • 一致性:对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致;
  • 可用性:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
  • 分区容忍性:当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

BASE 理论

Eric Brewer在1997发表的论文 Cluster-Based Scalable Network Services 中第一次提出 BASE 的概念;eBay的架构师Dan Pritchett 在 2008 年发表文章 BASE: An AcidAlternative 中第一次明确提出的 BASE 理论。

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写。

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性:

  • 响应时间上的损失
  • 功能上的损失(降级页面)

弱状态

弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

为什么要谈 ACID

BASE 和 CAP 的提出就是体系架构从单机到分布式的大背景下。

BASE: An AcidAlternative 文章的背景是讨论数据库分片对分布式事务的需求。文章使用 2PC (两阶段提交)来提供跨越多个数据库实例的ACID保证。但是引入 2PC 的直接影响就是可用性下降。假设数据分布在两个数据库实例上,每个数据库实例的可用性是 99.9%,那么数据库分片后可用性为:

作为商用软件,可用性下降是不可容忍的。因此,作者才引出了 CAP 理论,进而提出 BASE,通过放松 ACID 的严格一致性,获得系统可用性和可扩展性的提升。

我认为, 目前所有在讨论 CAP 的时候带上 ACID,一方面想说明在分布式环境下,必须在数据一致性和可用性之间做出取舍;另一方面,可能就是想单纯装一下。

CAP 理论的”三选二”

我们把 C、A、P 两两组合起来,可以得到关注点不同的系统:

  • CA:这样的系统关注一致性和可用性,它需要非常严格的全体一致性协议,比如上文提到的”两段提交”(2PC)。CA 系统不能容忍网络错误或者节点错误,一旦出现这样的问题,整个系统就会拒绝写请求,因为它并不知道是对面的那个节点宕机还是网络错误。唯一安全的做法就是把自己变成只读的。
  • CP:这样的系统关注一致性和分区容忍性。它关注的是系统里大多数人的一致性协议,比如:Paxos 算法 (Quorum 类的算法)。这样的系统只需要保证大多数节点数据一直,而少数的节点会在没有同步到最新版数据时编程不可用的状态。这样能够提供一部分的可用性。
  • AP:这样的系统关心可用性和分区容忍性。因此,这样的系统不能达成一致性,需要给出数据冲突,给出数据冲突就需要维护数据版本(Dynamo)。

CAP 的误解

现在很多人在进行分布式架构设计时言必谈 CAP,但是还是有很多人对 CAP 理论有误解,连 CAP 理论的作者都直言 CAP 理论的”三选二”约束一直存在着误导性。

这个约束过分简单化了各性质之间的相互关系。我们有必要辨析其中的细节。因此,我们对自己提出两个问题:

  1. P是必选项吗?
  2. CA一定要二选一吗?

P 是必选项吗

在分布式系统中,分区是由于网络问题或节点宕机导致的。这就导致程序员们就直面了一种状况:分区不会频繁出现,但是一定会出现。因此分布式系统的分区容忍性是必选项。

对于分布式系统工程实践,CAP理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性

CA 何如取舍

但是由于分区很少发生,那么在系统不存在分区的情况下牺牲 C 或 A 我们都会觉得很心疼,怎么办呢?

CAP定理证明中的一致性指线性一致性,即强一致性。强一致性要求多节点组成的被调要能像单节点一样运作、操作具备原子性,数据在时间、时序上都有要求。如果放宽这些要求,还有其他一致性类型:

  • 序列一致性(sequential consistency):不要求时序一致,A 操作先于 B 操作,在 B 操作后如果所有调用端读操作得到 A 操作的结果,满足序列一致性;
  • 最终一致性(eventual consistency):放宽对时间的要求,在被调完成操作响应后的某个时间点,被调多个节点的数据最终达成一致。

可用性在 CAP 定理里指所有读写操作必须要能终止,实际应用中从主调、被调两个不同的视角,可用性具有不同的含义。当网络分区出现时,主调可以只支持读操作,通过牺牲部分可用性达成数据一致。

工程实践中,较常见的做法是通过异步拷贝副本(asynchronous replication)、Quorum/NRW,实现在调用端看来数据强一致、被调端最终一致,在调用端看来服务可用、被调端允许部分节点不可用(或被网络分隔)的效果。

CAP 理论的这三种性质都可以在程度上衡量,并不是非黑即白的有或无。可用性显然是在 0% 到 100% 之间连续变化的,一致性分很多级别,连分区也可以细分为不同含义,如系统内的不同部分对于是否存在分区可以有不一样的认知。CAP 实践应将目标定为针对具体的应用,在合理范围内最大化数据一致性和可用性。

这样的思路延伸为如何规划分区期间的操作和分区之后的恢复,从而启架构师和程序员加深对 CAP 的认识,跳出由于 CAP 理论的表述而产生的思维局限。

无法忽略的网络延迟

CAP 理论的经典解释,是忽略网络延迟的,但在实际中延迟和分区紧密相关。CAP 从理论到实践落地的场景是如何在出现分区时对待已发生的操作:

  • 降低系统的可用性取消操作

  • 冒着系统损失一致性的风险继续操作

依靠多次尝试通信的方法来达到一致性(比如 Paxos 算法或者两阶段事务提交),仅仅是推迟了决策的时间,系统终究要做一个决定。无限期地尝试下去,本身就是选择一致性牺牲可用性的表现。

因此以实际效果而言,分区相当于对通信提出的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。这就从延迟的角度抓住了设计的核心问题:分区两侧是否在无通信的情况下继续其操作?

从这个实用的观察角度出发可以导出若干重要的推论:

  1. 分区并不是全体节点的一致见解,因为有些节点检测到了分区,有些可能没有;
  2. 检测到分区的节点即进入分区模式——这是优化 C 和 A 的核心环节。
  3. 系统设计者可以根据期望中的响应时间设置时限。但是需要注意:时限越短,系统进入分区模式越频繁,其中有些时候并不一定真的发生了分区的情况,可能只是网络变慢而已。

因此,有时候在跨区域的系统中放弃强一致性来避免保持数据一致所带来的高延迟是非常有意义的。

跳出 CAP

CAP 理论的这三种性质都可以在程度上衡量,并不是非黑即白的有或无。可用性显然是在 0% 到 100% 之间连续变化的,一致性分很多级别,连分区也可以细分为不同含义,如系统内的不同部分对于是否存在分区可以有不一样的认知。CAP 实践应将目标定为针对具体的应用,在合理范围内最大化数据一致性和可用性。

因为在分区没有出现的时候,我们完全不需要考虑分区容忍性,可以选择 CA;当分区出现之后,我们可以根据需求在 C 和 A 之间进行取舍。

因此,思路延伸为如何规划分区期间的操作和分区之后的恢复,从而启架构师和程序员加深对 CAP 的认识,跳出由于 CAP 理论的表述而产生的思维局限。例如,Oracle数据库的DataGuard复制组件包含三种模式:

  • 最大保护模式(Maximum Protection):即强同步复制模式,写操作要求主库先将操作日志(数据库的redo / undo日志)同步到至少一个备库才可以返回客户端成功。这种模式保证即使主库出现无法恢复的故障,比如硬盘损坏,也不会丢失数据;
  • 最大性能模式(Maximum Performance):即异步复制模式,写操作只需要在主库上执行成功就可以返回客户端成功,主库上的后台线程会将重做日志通过异步的方式复制到备库。这种方式保证了性能及可用性,但是可能丢失数据;
  • 最大可用性模式(Maximum Availability):上述两种模式的折衷。正常情况下相当于最大保护模式,如果主备之间的网络出现故障,切换为最大性能模式。

CAP 实践

管理分区

上文谈的,CAP 理论实践的关键是对分区情况进行管理。其关键是以非常明确、公开的方式去管理分区,不仅需要主动察觉分区的发生,还需要为分区期间所有可能受侵害的不变性约束预备专门的恢复过程和计划。管理分区有三个步骤:

  • 检测到分区开始;

  • 明确进入分区模式,限制某些操作;

  • 当通信恢复后启动分区恢复过程。

当系统进入到分区模式,它有两种可行的策略:一是限制部分操作,会削弱可用性;二是额外记录一些有利于后面分区恢复的操作信息。

系统可通过持续尝试恢复通信来察觉分区何时结束,当通信恢复后开始恢复一致性,以及补偿在系统分区期间程序产生的错误。

分区后的操作选择

决定限制哪些操作,主要取决于系统需要维持哪些不变性约束。
在给定了不变性约束条件之后,我们需要决定在分区模式下,是否坚持不触动某项不变性约束,或者通过分区恢复不变性约束。
举一个工作中的例子,某告警系统向 MongoDB 插入告警信息,告警信息具备唯一性。当发生分区时 MongoDB 产生脑裂,在互不感知的情况下可能系统可能会想两个数据库分区插入相同告警信息。
那么这时候我们有两个选择:

  1. 在感知到分区时不插入告警信息;
  2. 在分区时仍然插入告警,在分区结束后合并重复告警。
    因此,程序员应该清楚有哪些不变性约束、在分区期间必须维护哪些和哪些可以通过恢复保证不变性。
    特别的,在分区模式的讨论中,我们应将关注点放在有明确意义的原子操作而非单纯的读写,原因是操作的抽象级别越高对不变性约束的影响通常就越容易分析清楚。

分区恢复

到了某个时刻,通信恢复,分区结束。由于每一侧在分区期间都是可用的,其状态仍继续向前进展,但是分区会推迟某些操作并侵犯一些不变性约束。
在分区结束的时刻,当前状态不如历史记录有价值,因为通过历史记录,系统可以判断哪些操作违反了不变性约束,产生了何种外在的后果(如发送了响应给用户)。因此,系统分区模式下记录日志是有帮助的。
在分区恢复过程中,必须解决两个问题:

  1. 分区两侧的状态最终必须保持一致;
  2. 并且必须补偿分区期间产生的错误。

总结

当系统中存在分区,不应该盲目地牺牲一致性或可用性。运用以上讨论的方法,通过细致地管理分区期间的不变性约束,两方面的性质都可以取得最佳的表现。

对理论的讨论就到这里为止,但是从理论到实践的落地还有很多工作要做。引入 CAP 实践毕竟不像引入 ACID 事务那么简单,实施的时候需要对过去的策略进行全面的考虑,最佳的实施方案极大地依赖于具体服务的不变性约束和操作细节。多研究现有的优秀分布式系统,分析其设计理念和对 CAP 的实现,可以更快地成长。

参考

  1. CAP 理论十二年回顾:”规则”变了
  2. 再有人问你分布式事务,把这篇扔给他
  3. 分布式系统的事务处理
  4. CAP Confusion: Problems with ‘partition tolerance’