lyhabc 发表于 2016-08-29 17:04

MongoDB 如何保证 oplog 顺序?

MongoDB 复制集里,主备节点间通过 oplog 来同步数据,Priamry 上写入数据时,会记录一条oplog,Secondary 从 Primary 节点拉取 oplog并重放,以保证最终存储相同的数据集。

oplog 主要特性

幂等性,每一条oplog,重放一次或多次,得到的结果是一样的;为实现幂等 mongodb 对很多操作进行来转换,比如将 insert 转换为 upsert、$inc 操作转换为 $set等等。
固定大小(capped collection),oplog 使用固定空间存储,当空间满了时,会自动删除最旧的文档。
oplog 按时间戳排序,并且在所有节点上顺序保持一致
本文主要介绍MongoDBD 如何保证 oplog 有序存储并读取,关于 oplog 扩展阅读

阿里云MongoDB数据库的自适应oplog管理
MongoDB 3.2删除优化策略
MongoDB Secondary同步慢问题分析
MongoDB Secondary同步慢问题分析(续)
MongoDB同步原理解析
并发写 oplog 时,如何加锁?

Primary 上写入文档时,首先对写入的 DB 加写意向锁,再对集合加写意向锁,然后调用底层引擎接口写入文档,对 local 数据库加写意向锁,对oplog.rs集合加写意向锁,写入 oplog。关于MongoDB 多层级意向锁的机制,参考官方文档。

Write1

DBLock("db1", MODE_IX);
CollectionLock("collection1", MODE_IX);
storageEngine.writeDocument(...);   
DBLock("local", MODEX_IX);
CollectionLock("oplog.rs", MODEX_IX);
storageEngine.writeOplog(...);
Write2

DBLock("db2", MODE_IX);
CollectionLock("collection2", MODE_IX);
storageEngine.writeDocument(...);   
DBLock("local", MODEX_IX);
CollectionLock("oplog.rs", MODEX_IX);
storageEngine.writeOplog(...);
如何保证Primar上oplog 顺序?

基于上述并发策略,在多个写并发的情况下,如何保证 oplog 顺序?

oplog是一个特殊的 capped collection,文档没有_id字段,但包含一个 ts(时间戳字段),所有 oplog 的文档按照 ts 顺序存储。如下是几条 oplog 的例子。

{ "ts" : Timestamp(1472117563, 1), "h" : NumberLong("2379337421696916806"), "v" : 2, "op" : "c", "ns" : "test.$cmd", "o" : { "create" : "sbtest" } }
{ "ts" : Timestamp(1472117563, 2), "h" : NumberLong("-3720974615875977602"), "v" : 2, "op" : "i", "ns" : "test.sbtest", "o" : { "_id" : ObjectId("57bebb3b082625de06020505"), "x" : "xkfjakfjdksakjf" } }
以 wiredtiger 为例,在写入 oplog 文档时,会以 oplog 的 ts 字段作为 key、文档内容作为 value,写入一条 KV 记录,wiredtiger 会保证存储(btree 或 lsm 的方式都能保证)的文档按 key 来排序,这样就解决『文档按 ts 字段顺序存储』的问题。但仍然存在并发乱序的问题,例如:

并发写入多条 oplog时,时间戳分别是ts1、ts2、ts3 (ts1 < ts2 < ts3 ),ts1、ts3先成功了,这时Secondary 拉取到这2条 oplog,然后 ts2才写成功,然后 Secondary 再拉取到ts2,也就是说 Secondary 看到的 oplog 顺序为ts1、ts3、ts2,就会出现 oplog 乱序的问题。

MongoDB(wiredtiger 引擎)的解决方案是通过在读取时进行限制,保证Secondary 节点看到一定是顺序的,具体实现机制如下:

写入 oplog前,会先加锁给 oplog 分配时间戳,并注册到未提交列表里

lock();
ts = getNextOpTime(); // 根据当前时间戳 + 计数器生成
_uncommittedRecordIds.insert(ts);
unlock();
正式写入 oplog,在写完后,将对应的 oplog 从未提交列表里移除

writeOplog(ts, oplogDocument);
lock();
_uncommittedRecordIds.erase(ts);
unlock();
在拉取 oplog 时

if (_uncommittedRecordIds.empty()) {
    // 所有 oplog 都可读
} else {
    // 只能到未提交列表最小值以前的 oplog
}
通过上述规则,最终保证Primary 上 oplog 按 ts 字段存储,并且 Secondary能按序读取所有 oplog。

如何保证 Secondary 上 oplog 顺序与Primary 一致?

Secondary 把 oplog 拉取到本地后,会多线程重放,最后在一个线程里将拉取到的 oplog原样写入本地的 local.oplog.rs集合,这样就保证 Secondary oplog 最终与 Primary 上完全相同。
页: [1]
查看完整版本: MongoDB 如何保证 oplog 顺序?