本文是《如何学习分布式系统》中,关于一致性模型的相关介绍。
我们已经知道,在zookeeper中,写请求转发给leader,读请求follower自己处理,没有sync的情况下达不到linearizability。Mongodb呢?官方文档是这么说的,“MongoDB is consistent by default: reads and writes are issued to the primary member of a replica set. ”
这句话误导了我很多年,让我一直以为默认情况下,读写操作都发给primary,这总应该linearizable了吧?结果得知真相之后让我眼泪掉下来。
Write Concern,Read Concern和Read Preference
Mongodb能够提供灵活的一致性,原因在于它可以设置不同的Write Concern,Read Concern和Read Preference。
Write Concern决定了对数据库写入操作的确认级别,比如是否大多数节点都写入成功了才向客户端返回确认。
Read Concern决定了读取操作的返回数据的隔离级别,比如是否将大多数节点都希尔成功的数据返回给读操作。
Read Preference决定了从哪个节点进行读取操作。
默认情况
在mongodb 4.0版本中,默认的Read Preference是primary,也就是读写操作都发送给primary。
默认的Write Concern是w:1,对于副本集来说,就是只要primary确认写入就可以了。所以你以为你写入成功了,但是primary没有把数据分发出去之前就挂掉了,那么你以为写入成功的数据,就消失得无影无踪了。默认的Read Concern呢?居然是"local"!所以你从primary读到了一个数据x,在x还没被分发时primary挂了,新的primary没有x的信息,那么再从新的primary读数据时,x就好像从来没存在过一样。
所以,默认情况下,mongodb的一致性和Linearizability差得远呢。
Majority
那把Write Concern和Read Concern都设置为"majority"会怎么样呢?
Majority的Write Concern指的是数据分发给副本集的大多数成员之后再确认。
Majority的Read Concern指的是读取到的数据是被副本集大多数成员确认的。
想象以下情况:
- N1是副本集的primary,N2和N3是两个secondary。客户端S1读写请求都发给N1。
- N1突然发生了网络隔离,不能和N2,N3通信了。
- N2被选举为primary,但是N1对此一无所知,仍然认为自己是primary,所以此时副本集里实际上有两个primary。
- 客户端S2以majority的Write Concern写入新的数据到N2。
- N2将数据写入N3后,向S2返回写入确认。
- 客户端S1以majority的Read Concern从N1读取数据。
很容易看出,这种情况下是没有linearizability的。
其实,Mongodb还有一种更糟的情况,N1和N2同时作为primary的时候,S1有可能写请求发给N2,读请求发给N1。这样它连自己写入的数据都读不出来。因为mongodb的driver一般都需要连接池,而mongodb又有Read Preference这种实现读写分离的选项,所以实现起来就很麻烦。driver除了官方的以外,还有开源社区维护的,没注意到这些细节的话,很容易出现这种问题。
而且早期的mongodb中,根本没给你检测stale primary的机会。。。
https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#using-setversion-and-electionid-to-detect-stale-primaries
Linearizable Read Concern
Linearizable Read Concern是mongodb 3.4中出现的,为了解决上面的问题。有了linearizable的Read Concern,终于可以读取副本集中最新的数据啦。Linearizable Read Concern有一些使用上的限制:
- 只能用在primary的read操作上
- 查询条件必须唯一的确定一个document
linearizable Read Concern的实现原理是在read操作之前添加了一个空的写操作,如果当前的primary是stale的,空的写操作就会失败,从而被检查出来。实际上和zookeeper的sync如出一辙殊途同归。
Causal Consistency
上面的例子中,client的读写都发给了primary,不过总让secondary闲着也不太好,不如搞个读写分离吧。
不过一开始的读写分离是容易出现问题的:
- N1是副本集的primary,N2和N3是两个secondary。
- 客户端S设置Read Preference,可以让读请求发给secondary,写请求发给primary。
- S在N1上以majority的Write Concern写入订单数据x。
- N1把x分发给了N2之后,判定超过半数了,返回成功。
- S在N3上读取x,发现没有这个订单。
这真是非常尴尬,自己写的数据,自己都看不到。所以mongodb 3.6增加了Causal Consistency一致性。
为了实现它,mongodb每个节点都需要能获取正确的时间。时钟是分布式系统非常重要的组成部分,在其他文章中我们会着重介绍。
现在mongodb这样工作:
- N1是副本集的primary,N2和N3是两个secondary。
- 客户端S设置Read Preference,可以让读请求发给secondary,写请求发给primary。
- S在N1上以majority的Write Concern写入订单数据x,记录时间T。
- N1把x分发给了N2之后,判定超过半数了,返回成功。
- S在N3上读取x,读取操作带有T作为参数。
- N3没有时间T的操作,开始等待。
- N3从N1得到x的数据,意味着N3拥有了T时间的数据。
- N3将数据返回给S。
为了达到Causal Consistency,mongodb的客户端需要额外进行一些操作,例如开启client session,同时也有相应的一些限制,例如同一时刻只能有一个线程在client session中执行操作。
更多相关内容,请参考系列文章《如何学习分布式系统》。