再谈CAP理论:消除以往一些顽固的误解
引言
这篇文章是在看了两篇很好的文章翻译以后,发现以前对于CAP的理解有一些偏差,导致对BASE的理解也有问题,虚心读完两篇文章以后,对于以前的误会也消除了一些,当然是否是真的理解了还不清楚,毕竟没有看过CAP和BASE的paper,且缺少实际工业项目的磨练。这篇文章旨在讨论清楚几个以前对于CAP我没有意识到的点,这些导致了中间学习的时候产生了很多疑惑,如今看来就是基础理论理解的不够扎实导致的,所以如果能知道了这些细节,在学习的时候走的弯路也会少一些,这也是写下这篇文章的初衷。
这两篇文章分别是《CAP Twelve Years Later: How the “Rules” Have Changed》和《BASE: An Acid Alternative》,前者的作者有自己独到的写作风格,就是对于重点往往是一概而过,很多文中的金句都没有过多的解释,但是却让人感触颇多;后者打破了我往常对于BASE的理解,也就是BASE不仅是对CAP的扩展,同样是对ACID的妥协,这是以前我未曾想到的,却也是很重要的一部分。
下面对这些值得让人深思的话做一个简单的汇总:
《CAP Twelve Years Later: How the “Rules” Have Changed》
- 实际上只有“在分区存在的时呈现完美的数据一致性和可用性”这种很少见的情况是 CAP 理论不允许出现的。
这句话隐藏的意思是CAP理论不是简单的“三选二”,一直以来我们认为的“三选二”的公式一直存在着误导性,它会过分简单化各性质之间的相互关系。
因为我们的系统一般来说分区极少发生,那么在大多数时间里不存在分区的时候我们就没有理由牺牲CA其中一个,也就是说一般系统声称自己满足CP或者AP其实说的是在分区发生的时候如何选择,而且也并不是说完全的抛弃其中一个属性,只是在一定程度上的衡量罢了。比如说当我们需要注重程序的请求时延的时候,显然强一致性会导致程序的效率大幅下降,此时就可以选择降低对一致性的要求,以提高程序效率,此时我们并不是为了可用性,而是对满足需求所做出的取舍。况且一致性可以细分很多级别,可用性也可以从 0% 到 100% 之间连续变化,甚至于系统的不同部分对是否存在分区也可以有不同的认知。
- CAP关注的粒度是数据,而不是整个系统。
一个分布式系统的数据是多种多样的,对于不同的数据理应由不同的优先级,比如银行的存款数据优先级显然高于今天的排号数据,那么对于一个虚拟的银行管理系统来说,我们不可能对这个系统下AP或者CP的定义,而是对其中的某些数据分别下定义,那么为什么那么多的开源组件宣称自己是AP或者CP呢?因为其说的是在其中存储的数据,我们要做的就是把需要其声明的数据放到这个组件中。
- CAP 理论的经典解释,是忽略网络延迟的。
布鲁尔在定义一致性时,并没有将延迟考虑进去。也就是说,当事务提交时,数据能够瞬间复制到所有节点。这其实是令人感到诧异的结论,因为这揭示了在分布式中不存在服务器角度绝对的一致性,一切都是最终一致性,无非是时间的长短不一样罢了(当然这里讨论的服务器角度的一致性而不是用户角度的一致性)。
这也告诉我们,从现实来看,分区相当于对通信的时限要求,因为我们没办法准确判断分区,只能通过超时来判断是否出现分区,既然操作超时判断出现分区,我们假设这里有两种操作,一种是心跳包;一种是数据包用来达成一致。前者判断出现分区后没有什么需要的额外动作,后者则需要我们在CA之间做出抉择,这其实就是问题的核心所在,是在分区后在无通信的情况下继续提供服务,选择可用性,在恢复后合并冲突请求。还是选择一个分区提供服务或者不提供服务选择一致性。在不出现分区的时候根据实际需求选择CA,而在跨区域的系统,放弃强一致性来避免保持数据一致所带来的高延迟是非常有意义的,此时我们可以选择BASE,因为最终一致性的几个变种[6]在很多场合已经完全够用了。
在文中也举了Yahoo和Facebook的例子,其实究其本质都是最终一致性,但至少从文中简短的描述来看并不是严格的自读写一致性,因为它们都对用户的行为或者操作延迟做了假设,这也可以看出工程与学术的区别,学术研究会告诉你不要进赌场,因为最终你一定会输钱,而工程实践会告诉你如果你愿意承担一定的风险说不定可以小赚一笔!
- “一致性的作用范围”其实反映了这样一种观念,即在一定的边界内状态是一致的,但超出了边界就无从谈起。
对于这段话实际到现在我也比较迷惑,文中给出的例子是主分区内可以保证完备的一致性和可用性,而在分区外服务是不可用的,这其实就可以类比为一个quorum,当出现分区的时候,只有出现leader的那一方可以继续提供服务。
文中还举了一个例子,就是数据分片(sharding),当某些分片出现分区时,每个分片都可以单独提供服务;如果分区的内在联系比较紧密或者某些全局性的不变性约束非保持不可,那么此时就只有一个分区提供服务或者集群停止服务。
现在回到这句话上,我对其的理解就是出现分区时规定的一致性范围,也就是一种最终一致性,如果分区时选择全局的强一致性,那么全部的分片停止服务,如果允许更低级别的一致性,此时就有部分节点可以继续服务,在分区结束以后进行数据整合。
这里有一个很有意思的想法,就是对于离线模式的讨论,把离线模式看做一个长时间的分区,那么此时选择的就是A,这对于提升用户的满意度是很有用的,但是这也势必会造成数据不一致,所以在联上网的那一刻停止分区,进行数据同步。
- 牺牲并不等于什么都不做,需要为分区恢复后做准备。
这其实隐含了我们没有拒绝服务,而有些系统确实是选择拒绝服务的,比如Redis Cluster。
其实这一句话很好理解,毕竟这个系统我们希望它提供活性,也就是服务不能永远被拒绝,以文中样例举例,对于分区期间必须维持的不变性约束,设计师应当禁止或改动可能触犯该不变性约束的操作。也就是在分区期间一些操作是不能被执行的,这隐含了牺牲可用性,所以我们需要对这些操作做记录,在分区结束以后自动执行并恢复数据而不破坏全局的一致性。
如果牺牲了一致性,我们就会使得分区时每一个分区都是可用的,这保证了可用性,但是可能违反一些全局的不变性约束,此时在分区恢复以后就需要去恢复这些约束,文中给出了一种方法,就是它会回滚数据库到正确的时刻并按无歧义的、确定性的顺序重新执行所有的操作,最终使所有的节点达到相同的状态。这其实与Raft算法处理冲突类似,当然Raft中leader的日志就是权威,slave会回滚日志直到与主节点日志相同,并回滚后面的全部日志。
《BASE: An Acid Alternative》
我其实一直在把BASE与CAP作比较,其实看它们各自被提出的时间,BASE这个概念最早在1990年就被提出,而CAP最早则是1998年被提出,可见BASE初始就不是为了扩展CAP用的,而是作为ACID的补充。上一篇文章也有两句话证实了这个观点:
- 关于“数据一致性 VS 可用性”的第一回合争论,表现为 ACID 与 BASE 之争
- ACID 和 BASE 代表了两种截然相反的设计哲学,分处一致性 - 可用性分布图谱的两极。
显然在单机数据库上我们没有这些疑虑,因为单机事务已经很成熟了。而到了分布式则一切都不同了,我们要想继续支持ACID所要付出的代价是很大的,一般的2PC显然是很有问题的,2PC作为一个一致性算法,用来保证所有的进程在“事务要么提交要么失败退出”上达成一致。2PC是安全的,不会有坏的数据被写入到数据库,但是它的活性并不好:如果事务管理器在一个错误的点上失败,那么系统会阻塞。这是其中一个问题,2PC还有同步阻塞和数据不一致的问题存在。
换句话来说,实现ACID而使用的2PC导致可用性下降:
For example, if we assume each database has 99.9 percent availability, then the availability of the transaction becomes 99.8 percent, or an additional downtime of 43 minutes per month.
例如,我们假设每个数据库的可用性是3个9, 而在2PC实现的事务中,可用性降为99.8%,那么每个月的出问题时间就是43分钟。
为了可用性我们可以选择实现较为宽松的一致性,也就是我们所说的最终一致性,而这在很多情况已经完全可以满足我们的需求。而所谓的基本可用(Basically Available),也就是支持部分出错,但是整体还是工作的。文中所给的例子如下:
if users are partitioned across five database servers, BASE design encourages crafting operations in such a way that a user database failure impacts only the 20 percent of the users on that particular host. There is no magic involved, but this does lead to higher perceived availability of the system.
如果users被分到5个数据库,BASE的设计鼓励分开操作的方式,这样即使出错,也就只有20%的users会被影响,这个没什么神奇的做法,但这个确实可以实现高可用系统。
我们举一个现实的例子,如果一个事务包含修改购物车,库存修改,支付三个步骤,我们不希望购物车板块这样不那么重要的板块的崩溃影响其他核心板块的执行,此时可以选择先执行后两个,购物车在恢复以后在继续执行,最终一致性带来的是整个系统的高可用性。当然如何做也是一个问题,这一点可以参考文章,文章中提出引入一个消息队列解决整个问题。
还有很多朋友对于软状态(Soft state)的理解非常模糊,我尝试用文中的例子解释一下这个词语(可能需要阅读原文知道如何使用消息队列):在一个银行系统中用户A把一笔钱转移给了用户B,如果我们使用了BASE去做这件事,意味着A转账和B收款是两步,中间B操作被存入消息队列,那么可能有一个时间窗口这笔钱离开了A,B也没有收到,它被持久化到了消息队列中,这提高了系统的可用性,此时的状态就是软状态。显然这个窗口延迟很短的话用户是可以忍受的,甚至于根本没有发现,而这却大幅提升了系统的可用性。
此外BASE与CAP也是值得一提的,因为以前对BASE的误解颇深,我想不止是我一个人,因为我看到不少帖子都写到“BASE是基于CAP定理逐步演化而来的”,而根据上面的描述,显然这是有问题的一个说法,当然这种说法也有其道理,因为2008年BASE理论才被正式提出。当然这并不是一个有问题的结论,因为我们前面提到了CAP在不出现分区的时候可以选择CA,而往往因为效率不这么干,这就是BASE的用武之处,精确的描述就是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
当然对于这三个理论,目前我认为最客观的描述就是:“作为商用软件,可用性下降是不可容忍的。因此,作者才引出了 CAP 理论,即可用性和一致性无法兼得,但是我们大多数时候不需要完全牺牲掉一方,进而提出 BASE,根据具体需求在两者之间达成平衡,即通过放松 ACID 的严格一致性,获得系统可用性和可扩展性的提升”。
文中可能有些地方理解与实际有偏差,欢迎讨论指点!
参考: