在之前的文章中,介绍了关于master-slave模式下的主从端代码的执行流程,因为当时篇幅所限,未对oplog的数据结构以及mongodb的 local数据库作过多阐述,而这可能会让不知道其内容的朋友看代码时云里雾里找不到头绪,今天我专门用一篇文章来大致解释一下(这些内容可能会在后面章 节中有所涉及)。
首先了解一个local数据库:
在mongod中,出于特殊目的(复制机制),保留性使用了local数据库。当使用认证机制时,对local数据库等同于认证admin数据库。
当使用复制集(Replica sets)模式时,其会使用下面的local数据库:
local.oplog.rs 一个capped collection集合.可在命令行下使用--oplogSize 选项设置该集合大小尺寸.
local.replset.minvalid 通 常在复制集内使用,用于跟踪同步状态(sync status)
主从复制模式(Master/Slave):
o local.oplog.$main 存 储"oplog"信息
o local.slaves 存储在master结点上相应slave结点的同步情况(比如 syncTo时间戳等)
* Slave
o local.sources 从结点所要链接的master结点 信息(可通过--source配置参数指定)
* Other
o local.me 未知待查:)
o local.pair.* (replica pairs 选项,目前已不推荐使用)
除了了解local之外,还有oplog的数据结构(存储在local.oplog.$main)要解释一下:
{ ts : ..., op: ..., ns: ..., o: ... o2: ... }
上面就是一条oplog信息,复制机制就是通过这些信息来进行节点间的数据同步并维护数据一致性的,其中:
这个值很重要,在选举(如master宕机时)新primary时,会选择ts最大的那个secondary作为新primary。
op:1 字节的操作类型,例如i表示insert,d表示delete。
ns:操作所在的namespace。
o:操作所对应的 document,即当前操作的内容(比如更新操作时要更新的的字段和值)
o2: 在执行更新操作时的where条件,仅限于 update时才有该属性
其中op,可以是如下几种情形之一:
"u": update
"d": delete
"c": db cmd
"db": 声明当前数据库 (其中ns 被设置成为=>数据库名称+ '.')
"n": no op,即空操作,其会定期执行以确保时效 性
了解了这些内容之后,大家通过下面方式来验证一下上面的这些内容:
1.本地构造复制集
2.使用mongo连接到上面结点并初始化
MongoDB shell version: ...
connecting to: test
> rs.initiate()
3.查看oplog复制集信息
switched to db local
> db.oplog.rs.find()
{"ts" : { "t" : 1306207268000, "i" : 1 }, "h" : NumberLong(0), "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } }
4.开始添加记录
switched to db test
> db.foo.insert({x:1})
> db.foo.update({x:1}, {$set : {y:1}})
> db.foo.update({x:2}, {$set : {y:1}}, true)
> db.foo.remove({x:1})
5.查看上面操作生成的oplog信息:
switched to db local
> db.oplog.rs.find()
{ "ts" : { "t" : 1306207268000, "i" : 1 }, "h" : NumberLong(0), "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } }
{ "ts" : { "t" : 1306207310000, "i" : 1 }, "h" : NumberLong("3138280161636515857"), "op" : "i", "ns" : "test.foo", "o" : { "_id" : ObjectId("4ddb244d2d0d00000000551a"), "x" : 1 } }
{ "ts" : { "t" : 1306207314000, "i" : 1 }, "h" : NumberLong("772196482295043060"), "op" : "u", "ns" : "test.foo", "o2" : { "_id" : ObjectId("4ddb244d2d0d00000000551a") }, "o" : { "$set" : { "y" : 1 } } }
{ "ts" : { "t" : 1306207317000, "i" : 1 }, "h" : NumberLong("7272647888810218413"), "op" : "i", "ns" : "test.foo", "o" : { "_id" : ObjectId("4ddb2455be10000000002f6a"), "x" : 2, "y" : 1 } }
{ "ts" : { "t" : 1306207325000, "i" : 1 }, "h" : NumberLong("3083832920223263240"), "op" : "d", "ns" : "test.foo", "b" : true, "o" : { "_id" : ObjectId("4ddb244d2d0d00000000551a") } }
注:其它常用命令:db.oplog.$main.help() db.printReplicationInfo();
知道上面这些内容之后,我们来大致看一下mongod在源码层面上是如何存储oplog的,先请看下面文件:
oplog.cpp :顾名思义,它的作用就是存储和读取oplog,其主要 包括方法:
createOplog() :初始 化本地local.oplog.$main 集合信息,包括大小尺寸等
_logOpOld() : 用于在master/slave模式下向local.oplog.$main 添加oplog信息
_logOpObjRS() : 用于在replset模式下向local.oplog.rs模式下添加oplog 信息
append_O_Obj() :向已存在的obj对象后追加新的对象元素信息(主要用于更新类型的oplog)
pretouchN() 及pretouchOperation() : 某些情况下MongoDB会锁住数据库。如果此时正有数百个请求,则它们会堆积起来,造成许多问题。这里使用下面的优化方式来避免锁定:每次更新前,先查 询记录。查询操作会将对象放入内存,于是更新则会尽可能的迅速。在"主/从"部署方案中,从节点可以使用“-pretouch”参数运行,这也可以得到相 同的效果。
上面的newRepl()及oldRepl()主要是实现函数绑定,它会将oplog.cpp文件中的_logOpRS和_logOpOld方法加以绑定 (本人猜测可能因为1.6版之后引入了replset,造成方法名称的重新命名和分配,导致这里用这种方式对老的方法进行“过渡”)。
接着上面方面会调用oplog.cpp文件的createOplog()来构造master的“local.oplog.$main”集合,如下:
在上面初始化及函数指针绑定任务完成了,就可以在数据同步时,根据oplog集合信息来进行同步了,如下:
上面方法中出现了预取操作,该技术主要是出现运行效率的考虑,下面是pretouchOperation方法的具体实现(pretouch方法实现与其 大同小异,感兴趣的朋友可自行阅读):
说到这里只是获取oplog的操作,那mongod进行cud操作时,log是如何记录到日志集合中的呢?还记得上面提到的函数绑定吗?我们来看一下 oplog.cpp中的下面方法:
上面的logOp方法几乎出现在了所有cud操作场景中,比如在update操作时:
这里以_logOpOld()方法为例,来看一下mongod是如何向local.oplog.$main集合中添加oplog日志的:
好了,关于oplog的主要内容介绍的差不多了,感兴趣的朋友可以下载相关源码做进一步的分析。
参考链接:
http://www.snailinaturtleneck.com/blog/2010/10/14/getting-to-know-your-oplog/
http://www.snailinaturtleneck.com/blog/2010/08/02/replica-sets-part-2-what-are-replica-sets/
原文链接:
作者: daizhj, 代震军
微博: http://t.sina.com.cn/daizhj
Tags: mongodb,c++,Replica,master-slave,oplog,local database