在混合逻辑时钟这篇博客里,我介绍了关于混合逻辑时钟的基本知识,本文介绍一下MongoDB里面的混合逻辑时钟,参考 Implementation of Cluster-wide Logical Clock and Causal Consistency in MongoDB 。最后,还会介绍如何根据这个混合逻辑时钟解决 MongoShake 里面move chunk的问题。
混合逻辑时钟是MongoDB在3.6版本开始推出的。
ClusterTime ClusterTime表示的就是节点的混合逻辑时钟(下面也叫“时钟”),其由
组成,
字段是32位的秒级粒度的机器的物理时钟,
是32位的自增计数,其遵循混合逻辑时钟里面所说的规则。
Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases 里面介绍的,时钟递增是通过“发送”,“接受”事件来触发的。而MongoDB里面消息的“发送”和“接受”并不会导致时钟的变化,只有“写”事件才会触发,也就是说只有用户的 写请求 才会导致时钟的增加,在论文里面讲的是“节点状态发生了变化”,所以读请求并不会导致时钟增加。
ClusterTime可以转换成OpTime写到oplog里面,OpTime的格式是:
,其多了一个
复制协议字段。
(也就是ClusterTime)对应oplog里面就是 ts
字段,
对应的是 t
字段:
{ "ts" : Timestamp(1571389994, 1), "t" : NumberLong(1), ... }
下面是ClusterTime时钟递增的伪代码,其实也就是对应混合逻辑时钟这篇博客里介绍的时钟递增的方法。
ClusterTime getNextClusterTime() { newCounter = 0; wallClockSecs = now(); // _clusterTime is a current local value of node’s ClusterTime currentSecs = _clusterTime.getSecs(); if (currentSecs > wallClockSecs) { newSecs = currentSecs; newCounter = _clusterTime.getCounter() + 1; } else { newSecs = wallClockSecs; } // 要么物理时钟增加,计数清0;要么计数递增。 _clusterTime = ClusterTime(newSecs, newCounter); return _clusterTime; }
能否只根据write majority和read majority实现因果一致性? 有一个直观感觉就是,假如我write的时候配置了concern majority,read的时候也配置concern majority不就可以read own write了吗?
答案是不行的,原因是read majority并不是广播读,而是读一个本地的RaftCommitPoint,这可能导致读到的是旧的快照,而不是上次write majority的结果。举个例子,有3个节点:P1(主),S2(从), S3(从)。write majority写了P1, S2,数据更新到最新,而read majority请求发到了S3,其还没有更新快照,这时候本地快照读就读到了老的数据。
so,答案就是还需要ClusterTime。
session内的因果一致性 首先,介绍一下客户端与服务器端交互的简单过程,客户端保存一份ClusterTime的值,MongoDB节点收到写请求触发时钟的递增,同时ClusterTime的值会回复给客户端,该值将会在客户端进行存储。
那如何实现read own write?客户端可以携带
afterClusterTime
参数,表示只读那些大于等于给定时钟的数据,如果当前结点快照位点还没有更新,则会block等待,直到位点更新为止。
我们可以看到,在上图的第5步,客户端携带了 {afterClusterTime: T2}
参数,这个时候该请求发到了secondary结点,而结点位点没有更新的话,会一直等到primary的T2位点传播到当前读的secondary,才把数据返回,这样也就实现了read own write。
通过这个 afterClusterTime
可以实现一个 等待 的逻辑,那么这里有个问题,假如各种原因,当前结点一直没有更新到T2怎么办?比如sharding情况,写的是shard1,但是读的是shard2,恰好shard2一直没有写流量,导致位点一直没有更新。MongoDB的解决方式就是通过添加noop实现:mongod定期(默认10s)写一个心跳请求(也叫noop)到oplog,这样如果读错节点,最多经过一个心跳周期,位点也会进行更新(这个例子会返回空数据),而不会导致一直block。
客户端攻击的风险 上面我们介绍了,客户端写可以携带ClusterTime给MongoDB,那假如客户端是一个恶意程序,携带了一个非常大的时间戳,比如最大的timestamp,那么服务端对其自增时肯定会出错,那么如何解决?
MongoDB通过对客户端增加签名机制来实现,客户端发送需要携带一个签名,MongoDB收到请求以后,一旦发现客户端携带的ClusterTime大于当前结点的ClusterTime,就会对这个签名进行验证,查看是否是正常的签名,不是的话就会拒绝掉这个请求。关于签名的细节可以参考 Implementation of Cluster-wide Logical Clock and Causal Consistency in MongoDB 中的A1.4小节。
逻辑时钟与物理时钟差距的限制 在某些极端情况下,逻辑时钟可能远远领先于物理时钟,假如逻辑时钟真的跑到了最大的时间戳,那么这个值就没办法更新了。为了限制这种情况的发生,MongoDB加了一个差值的阈值maxAcceptableLogicalClockDriftSecs,也就是逻辑时钟和物理时钟的差值不能超过这个阈值(默认1年)。这个差值的限制也是混合逻辑时钟的特性:逻辑时钟和物理时钟的差值有一个上确界。
MongoShake解决sharding的move chunk问题 a. 3.6以前版本move chunk的问题 对于sharding,MongoShake是直接拉取源端所有节点的oplog,然后进行并行回放的方式进行实现。也就是说,不同shard之间是没有交互的,在正常情况下,这个是ok的,但一旦发送move chunk,这个就有问题了,如下图所示,shard1上面写入了一条 {a:1}
,然后发生了move chunk,这条数据跑到了shard2,shard2后面又执行了一个更新操作,把 {a:1}
改成了 {a:3}
。 MongoShake对于不同的shard是并行拉取回放的,我们 并不能 保证shard1的这个update操作一定先于shard2的update写入,所以并发同步就有问题了。为了解决这个问题,MongoShake在v2.2代码里做了很多工作,在大体上可以解决这个问题,但是会有一些的corner case,导致对性能会有较大的影响,极端情况下正确性也会有问题。
b. 3.6以后的解决方式 在3.6以及之后的版本,我们可以根据逻辑时钟可以排序的特性(如果事件 a
happened before b
,那么 a
的时钟一定小于 b
,反之不成立)来解决move chunk的问题。
对于sharding,MongoShake可以拉取所有shard的oplog,剔除move chunk的报文(含fromMigate字段),然后对所有oplog进行排序即可。因为一旦发生move chunk,则必然有happened before的关系,从而排序可以解决因果一致性。但这个也是只对3.6以后才可以,3.6以前的ts字段并不是混合逻辑时钟,所以没办法排序。在MongoShake的v2.4版本,我们将会用这种方式解决move chunk的问题。不同shard的oplog拉取排序是一个归并排序的过程,对性能的影响代价也很小。
说明