问题描述
MongoDB的写请求写入Primary, secondary从Primary自动获取并且应用oplog来保持和主库的同步, MongoDB 允许用户从Primary 或者 secondary 读取数据(由客户端ReadPreference 决定)。但读数据可能存在以下问题:
- 用户从secondary读,但secondary还没有跟上Primary,导致读取了老数据
- 用户从primary读到数据,但在该数据复制到secondary 之前crash 了,导致用户”已经“读到的数据”丢失了“。
解决办法
MongoDB 引入了ReadConcern的概念, readConcern 的原型为:
readConcern: {
level: "[majority|local|linearizable|available|snapshot]",
afterOpTime: { ts: <timestamp>, term: <NumberLong> },
afterClusterTime: <timestamp>,
atClusterTime: <timestamp>
}
afterOpTime 已经Deprecated 了,可以忽略。
ClusterTime 暂时理解为一个时间点, 而这个时间点是什么含义取决于level 参数。
afterClusterTime 表示在该时间点之后。
atClusterTime 可能是4.2 的全局事务有关,目前driver 也不支持设置。
如果level 为local, ClusterTime 则表示applied oplog time。
如果level 为majority, ClusterTime 则表示大多数节点都达到的 applied oplog time。
为了解决上述问题1, 用户可以指定local + afterClusterTime, 表示用户要数据 ClusterTime 之后的数据(但依然可能不是最新数据),如果这个节点没有,服务器不要返回,一直等到有为止。
为了解决上述问题2, 用户可以指定level 为majority + afterClusterTime, 表示用户要在ClusterTime之后的数据,并且大多数节点都已经有这个数据了。 这时,可以保证即使原来有该数据的节点死掉,是新的primary肯定有这个数据,所有的secondary最终也会有这个数据。
内部实现
每一个MongoDB slave 都维护了一个 _lastAppliedOpTime 和 _lastCommittedOpTime。 (根据mongodb 的配置,_lastAppliedOpTime 有时应该是_durableOpTime,这里不做区分)
_lastAppliedOpTime 表示本节点applied 到了那个时间,
_lastCommittedOpTime 表示复制集内写入到majority节点的_lastAppliedOpTime。
_lastAppliedOpTime 的值比较容易维护,每次apply一批oplog 就会直接更新。_lastCommittedOpTime 更新比较困难。 主要有以下几种方式:
- 通过HeartBeat 更新。 MongoDB 中任何一个节点都会和别的节点进行HB, 在每一个HB 的过程中, 发送响应方都会包含自己的_lastAppliedOpTime。 每次处理HeartBeat response的时候, 会去更新 _lastCommittedOpTime。 但HB 的频率默认是2s, 效率比较低。
- 每次自己apply 一些oplog 后,借助于HB的结果, 也会改变_lastCommittedOpTime 的值。 所以本节点的_lastAppliedOpTime 比较快,但其他节点的信息还是有可能2s的间隔。
- MongoDB 的每一个slave 应用完一批oplog 后, 会立即将自己的_lastAppliedOpTime告诉primary, primary 需要这个信息对writeConcern == ”Majority“ 的客户进行相应。 所以Primary 有比较实时的全局的 _lastCommittedOpTime 信息。 所以各个slave 只要能够及时从primary 那里获得_lastCommittedOpTime 就可以了。 HB 显然不合适,因为间隔太长。 理论上可以放在
a). slave update 它的_lastAppliedOpTime 到primary 的时候, primary 返回primary 的_lastCommittedOpTime。
b). slave 每次向它的source节点请求oplog 的时候,source节点会返回它的_lastAppliedOpTime。
上述2种方式都比较及时, MongoDB 4.0 中采用了 b 方案。
因为最新的数据不一定是majority 的数据,而对于一个节点来说,用户要的可能是majority 的数据,所以存储引擎需要有一种保存历史数据的能力,目前只有wiredTiger才支持snapshot,所以如果要使用ReadConcern=="majority", 必须使用wiredTiger 引擎。
使用方法
由第一部分我们看到readConcern 需要设置level, afterClusterTime, atClusterTimeout,MongoDB driver 提供了相关的接口让用户自己设置。 其中的Level 用户使用readConcern 则必须指定。其他的则不是必须, driver 会设置默认值。
如果使用了 causally consistent, MongoDB driver会自动更新afterClusterTime 为上一条请求的response 的operationTime。