大约在五六年前,第一次接触到了当时已经是hot topic的NoSql。不过那个时候学的用的都是mysql,Nosql对于我而言还是新事物,并没有真正使用,只是不明觉厉。但是印象深刻的是这么一张图片(后来google到图片来自这里):
CAP理论
CAP理论起源
简而言之:BASE通过最终一致性来尽量保证服务的可用性。注意图中最后一句话“But I think it‘s a spectrum”,就是说ACID BASE只是一个度的问题,并不是对立的两个极端。
2002年,在Brewer's conjecture and the feasibility of consistent, available, partition-tolerant web services中,两位作者通过异步网络模型论证了CAP猜想,从而将Brewer的猜想升级成了理论(theorem)。但实话说,我也没有把文章读得很明白。
2009年的这篇文章brewers-cap-theorem,作者给出了一个比较简单的证明:
如上图所示,N1,N2两个节点存储同一份数据V,当前的状态是V0。在节点N1上运行的是安全可靠的写算法A,在节点N2运行的是同样可靠的读算法B,即N1节点负责写操作,N2节点负责读操作。N1节点写入的数据也会自动向N2同步,同步的消息称之为M。如果N1,N2之间出现分区,那么就没法保证消息M在一定的时间内到达N2。
从事务的角度来看这各问题
α这个事务由操作α1, α2组成,其中α1是写数据,α2是读数据。如果是单点,那么很容易保证α2能读到α1写入的数据。如果是分布式的情况的情况,除非能控制 α2的发生时间,否则无法保证 α2能读到 α1写入的数据,但任何的控制(比如阻塞,数据集中化等)要么破坏了分区容错性,要么损失了可用性。
另外,这边文章指出很多情况下 availability比consistency重要,比如对于facebook google这样的网站,短暂的不可用就会带来巨大的损失。
2010年的这篇文章brewers-cap-theorem-on-distributed-systems/,用了三个例子来阐述CAP,分别是example1:单点的mysql;example2:两个mysql,但不同的mysql存储不同的数据子集(类似sharding);example3:两个mysql,对A的一个insert操作,需要在B上执行成功才认为操作完成(类似复制集)。作者认为在example1和example2上 都能保证强一致性,但不能保证可用性;在example3这个例子,由于分区(partition)的存在,就需要在一致性与可用性之间权衡。
于我看来,讨论CAP理论最好是在“分布式存储系统”这个大前提下,可用性也不是说整体服务的可用性,而是分布式系统中某个子节点的可用性。因此感觉上文的例子并不是很恰当。
CAP理论发展
从收入目标以及合约规定来讲,系统可用性是首要目标,因而我们常规会使用缓存或者事后校核更新日志来优化系统的可用性。因此,当设计师选择可用性的时候,因为需要在分区结束后恢复被破坏的不变性约。实践中,大部分团体认为(位于单一地点的)数据中心内部是没有分区的,因此在单一数据中心之内可以选择CA;CAP理论出现之前,系统都默认这样的设计思路,包括传统数据库在内。分区期间,独立且能自我保证一致性的节点子集合可以继续执行操作,只是无法保证全局范围的不变性约束不受破坏。数据分片(sharding)就是这样的例子,设计师预先将数据划分到不同的分区节点,分区期间单个数据分片多半可以继续操作。相反,如果被分区的是内在关系密切的状态,或者有某些全局性的不变性约束非保持不可,那么最好的情况是只有分区一侧可以进行操作,最坏情况是操作完全不能进行。
上面摘录中下选线部分跟MongoDB的sharding情况就很相似,MongoDB的sharded cluste模式下,shard之间在正常情况下,是无需相互通信的。
在13年的文章中《better-explaining-cap-theorem》,作者指出“it is really just A vs C!”,因为
(1)可用性一般是在不同的机器之间通过数据的复制来实现
(2)一致性需要在允许读操作之间同时更新几个节点
(3)temporary partion,即几点之间的通信延迟是可能发生了,此时就需要在A 和 C之间权衡。但只有在发生分区的时候才需要考虑权衡。
在分布式系统中,网络分区一定会发生,因此“it is really just A vs C!”
MongoDB与CAP
write-concern:
{ w: <value>, j: <boolean>, wtimeout: <number> }
After the write operation returns with a
w: "majority"
acknowledgement to the client, the client can read the result of that write with a"majority"
readConcern
writeConcernMajorityJournalDefault
.这么一个选项,使得w,j在不同的组合下情况下不同:read-reference:
在前文已经讲解过,一个replica set由一个primary和多个secondary组成。primary接受写操作,因此数据一定是最新的,secondary通过oplog来同步写操作,因此数据有一定的延迟。对于时效性不是很敏感的查询业务,可以从secondary节点查询,以减轻集群的压力。
MongoDB指出在不同的情况下选用不同的read-reference,非常灵活。MongoDB driver支持一下几种read-reference:
primary:默认模式,一切读操作都路由到replica set的primary节点
primaryPreferred:正常情况下都是路由到primary节点,只有当primary节点不可用(failover)的时候,才路由到secondary节点。
secondary:一切读操作都路由到replica set的secondary节点
secondaryPreferred:正常情况下都是路由到secondary节点,只有当secondary节点不可用的时候,才路由到primary节点。
nearest:从延时最小的节点读取数据,不管是primary还是secondary。对于分布式应用且MongoDB是多数据中心部署,nearest能保证最好的data locality。
如果使用secondary或者secondaryPreferred,那么需要意识到:
(1) 因为延时,读取到的数据可能不是最新的,而且不同的secondary返回的数据还可能不一样;
(2) 对于默认开启了balancer的sharded collection,由于还未结束或者异常终止的chunk迁移,secondary返回的可能是有缺失或者多余的数据
(3) 在有多个secondary节点的情况下,选择哪一个secondary节点呢,简单来说是“closest”即平均延时最小的节点,具体参加Server Selection Algorithm
read-concern:
read concern是在MongoDB3.2中才加入的新特性,表示对于replica set(包括sharded cluster中使用复制集的shard)返回什么样的数据。不同的存储引擎对read-concern的支持情况也是不一样的
read concern有以下三个level:
local:默认值,返回当前节点的最新数据,当前节点取决于read reference。
majority:返回的是已经被确认写入到多数节点的最新数据。该选项的使用需要以下条件: WiredTiger存储引擎,且使用election protocol version 1
;启动MongoDB实例的时候指定 --enableMajorityReadConcern选项。
linearizable:3.4版本中引入,这里略过了,感兴趣的读者参考文档。
在文章中有这么一句话:
Regardless of the read concern level, the most recent data on a node may not reflect the most recent version of the data in the system.
就是说,即便使用了read concern:majority, 返回的也不一定是最新的数据,这个和NWR理论并不是一回事。究其根本原因,在于最终返回的数值只来源于一个MongoDB节点,该节点的选择取决于read reference。
在这篇文章中,对readconcern的引入的意义以及实现有详细介绍,在这里只引用核心部分:
readConcern
的初衷在于解决『脏读』的问题,比如用户从 MongoDB 的 primary 上读取了某一条数据,但这条数据并没有同步到大多数节点,然后 primary 就故障了,重新恢复后 这个primary 节点会将未同步到大多数节点的数据回滚掉,导致用户读到了『脏数据』。当指定 readConcern 级别为 majority 时,能保证用户读到的数据『已经写入到大多数节点』,而这样的数据肯定不会发生回滚,避免了脏读的问题。
一致性 or 可用性?
回顾一下CAP理论中对一致性 可用性的问题:
一致性,是指对于每一次读操作,要么都能够读到最新写入的数据,要么错误。
可用性,是指对于每一次请求,都能够得到一个及时的、非错的响应,但是不保证请求的结果是基于最新写入的数据。
前面也提到,本文对一致性 可用性的讨论是基于replica set的,是否是shared cluster并不影响。另外,讨论是基于单个客户端的情况,如果是多个客户端,似乎是隔离性的问题,不属于CAP理论范畴。基于对write concern、read concern、read reference的理解,我们可以得出以下结论。
- 默认情况(w:1、readconcern:local)如果read preference为primary,那么是可以读到最新的数据,强一致性;但如果此时primary故障,那么这个时候会返回错误,可用性得不到保证
- 默认情况(w:1、readconcern:local)如果read preference为secondary(secondaryPreferred、primaryPreferred),虽然可能读到过时的数据,但能够立刻得到数据,可用性比较好
- writeconern:majority保证写入的数据不会被回滚; readconcern:majority保证读到的一定是不会被回滚的数据
- 若(w:1、readconcern;majority)即使是从primary读取,也不能保证一定返回最新的数据,因此是弱一致性
- 若(w: majority、readcocern:majority),如果是从primary读取,那么一定能读到最新的数据,且这个数据一定不会被回滚,但此时写可用性就差一些;如果是从secondary读取,不能保证读到最新的数据,弱一致性。
回过来来看,MongoDB所说的高可用性是更普世意义上的可用性:通过数据的复制和自动failover,即使发生物理故障,整个集群还是能够在短时间内回复,继续工作,何况恢复也是自动的。在这个意义上,确实是高可用的。