目录

MongoDB oplog 分析

目录

MongoDB数据复制的基础是oplog,复制集的任何数据变更都会在primary节点local.oplog.rs集合下记录oplog,secondary节点从primary持续拉取oplog并在本地回放,实现主从节点的数据实时同步。

oplog包含下列键:

  • ts

    时间戳,由unix时间戳和自增计数构成,自增计数表示该操作为当前秒内的操作顺序

  • h

    唯一标识

  • v

  • op

    操作类型,其值是下列之一

    • i - insert
    • u - update
    • d - remove
    • c - command
    • n - no-op
  • ns

    namespace,格式是database.collection

  • o

    具体操作内容,对于i,表示要写入的文档;对于u,表示要执行的变更;对于d,表示要删除的文档,即_id;对于c,表示要执行的命令 udpate操作如果只更新部分field,o键的内容是*{ $set: { … } };如果是replace整个文档,o键的内容是{ _id: …, field0: … filed1: … }*

  • o2

    仅出现于update操作,用于指定要操作的目标文档,即_id

举个栗子,依次执行下列操作:

1
2
3
4
5
6
7
demo-rs:PRIMARY> use test
demo-rs:PRIMARY> test.coll.insert({a: 1})
demo-rs:PRIMARY> test.coll.createIndex({b: 1})
demo-rs:PRIMARY> test.coll.createIndex({c: 1}, {background: true})
demo-rs:PRIMARY> test.coll.dropIndex('b_1')
demo-rs:PRIMARY> test.coll.dropIndex('b_1')
demo-rs:PRIMARY> test.coll.drop()

然后观察对应的oplog,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
demo-rs:PRIMARY> use local
switched to db local
demo-rs:PRIMARY> db.oplog.rs.find().sort({$natural: -1})
{ "ts" : Timestamp(1482318735, 1), "h" : NumberLong("6964527823728342008"), "v" : 2, "op" : "c", "ns" : "test.$cmd", "o" : { "dropDatabase" : 1 } }
{ "ts" : Timestamp(1482318723, 1), "h" : NumberLong("-6125359811671901699"), "v" : 2, "op" : "c", "ns" : "test.$cmd", "o" : { "deleteIndexes" : "coll", "index" : "c_1" } }
{ "ts" : Timestamp(1482318662, 1), "h" : NumberLong("7320721234104966244"), "v" : 2, "op" : "c", "ns" : "test.$cmd", "o" : { "deleteIndexes" : "coll", "index" : "b_1" } }
{ "ts" : Timestamp(1482318543, 1), "h" : NumberLong("-7326267621257646623"), "v" : 2, "op" : "i", "ns" : "test.system.indexes", "o" : { "ns" : "test.coll", "key" : { "c" : 1 }, "name" : "c_1", "background" : true } }
{ "ts" : Timestamp(1482318520, 1), "h" : NumberLong("1480507733147525872"), "v" : 2, "op" : "i", "ns" : "test.system.indexes", "o" : { "ns" : "test.coll", "key" : { "b" : 1 }, "name" : "b_1" } }
{ "ts" : Timestamp(1482318510, 2), "h" : NumberLong("1964889382590225189"), "v" : 2, "op" : "i", "ns" : "test.coll", "o" : { "_id" : ObjectId("585a62aef6fe6ed88c29a233"), "a" : 1 } }
{ "ts" : Timestamp(1482318510, 1), "h" : NumberLong("2594632055235718257"), "v" : 2, "op" : "c", "ns" : "test.$cmd", "o" : { "create" : "coll" } }

可以看到,command的oplog类型不一定是c,比如建立索引,实际上是在目标数据库的system.indexes集合写入一条文档,类型是i。

有一点需要注意,对于类型是c的oplog,o键的值是有序的,比如

1
2
3
4
5
6
7
8
{
    "ts" : Timestamp(1482318662, 1),
    "h" : NumberLong("7320721234104966244"),
    "v" : 2,
    "op" : "c",
    "ns" : "test.$cmd",
    "o" : { "deleteIndexes" : "coll", "index" : "b_1" }
}

回放过程相当于执行db.runCommand({ "deleteIndexes" : "coll", "index" : "b_1" }),第一个键deleteIndexes表示操作类型是删除索引,第二个键index指定目标索引,二者要保证顺序。

在写同步工具时碰到过,pymongo将读取的文档存到一个dict,对于Python来说,dict是键序无关的,但对于MongoDB,键序不对将导致command执行失败,解决方法可参考 https://dzone.com/articles/pymongo-and-key-order


后记@20171229

如果oplog用于MongoDB间的数据同步,直接回放就行;如果MongoDB数据同步到其他数据库,update需要关注细节,特别是内嵌文档操作,字段名采用".“点分隔的扁平化表示。

以文档{ info: { a: 1, b: 2 } }为例,insert操作的olog的o字段即原始文档;update操作的oplog相对复杂,看下面几个情景:

  1. 修改a字段 => { info: { a: 100, b: 2 } }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $set: { 'info.a': 100 } } }

    注意 { op: 'u', o2: {_id: 'xxxx'}, o: { $set: { 'info': { 'a': 100 } } } }执行结果是{ info: { a: 100 } }

  2. 修改a&b字段 => { info: { a: 100, b: 200 } }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $set: { 'info.a': 100, 'info.b': 200 } } }

  3. 增加c字段 => { info: { a: 1, b: 2, c:3 } }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $set: { 'info.c': 3 } } }

    注意 { op: 'u', o2: {_id: 'xxxx'}, o: { $set: { 'info': { 'c': 3 } } } }执行结果是{ info: { c: 3 } }

  4. 删除a字段 => { info: { b: 2 } }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $unset: { 'info.a': true } } }

    info.a后面可以是任意值,不影响字段删除

    注意字段名扁平化,不要使用嵌套,下面的语义是删除info字段,*{ ‘a’: true }*相当于info的值

    { op: 'u', o2: {_id: 'xxxx'}, o: { $unset: { 'info': { 'a': true } } } }

    等同于

    { op: 'u', o2: {_id: 'xxxx'}, o: { $unset: { 'info': true } }

  5. 删除a&b字段 => { info: { } }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $unset: { 'info.a': true, 'info.b': true } } }

  6. 删除info字段 => { }

    oplog为{ op: 'u', o2: {_id: 'xxxx'}, o: { $unset: { 'info': true } }

简单总结,对于update来说,$set和$unset对象的key(s)是目标操作字段。