热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MongoDB里面的混合逻辑时钟

在混合逻辑时钟这篇博客里,我介绍了关于混合逻辑时钟的基本知识,本文介绍一下MongoDB里面的混合逻辑时钟,参考ImplementationofCluster-wideLogica


在混合逻辑时钟这篇博客里,我介绍了关于混合逻辑时钟的基本知识,本文介绍一下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拉取排序是一个归并排序的过程,对性能的影响代价也很小。


说明




推荐阅读
author-avatar
137381372_e57647
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有