3.MongoDB恢復探究:爲何oplogReplay參數只設置了日誌應用結束時間oplogLimit,而沒有設置開始時間?

(一)個人疑問html

在使用MySQL數據庫binlog日誌基於時間點恢復數據庫時,咱們必需要指定binlog的開始位置和結束位置,而在MongoDB裏面,若是使用oplog進行恢復,只有oplogLimit參數,該參數信息以下mongodb

--oplogLimit=<seconds>[:ordinal]          only include oplog entries before the provided Timestamp

oplogLimit參數定義了數據庫恢復到該時間點。也就是說,MongoDB只是設置了oplog的結束位置,沒有指定oplog的開始位置。那麼就存在問題了,如下圖爲例,我在T3時刻執行了全備份,在T4時刻數據庫發生了誤操做,當我執行恢復的時候,分爲2個步驟:數據庫

  • 階段1:使用徹底備份,將數據庫恢復到T3時刻;
  • 階段2:使用oplog日誌,將數據庫恢復到T4故障以前。T4故障以前的時間點由參數oplogLimit控制,可是:oplog的開始時間不是從T3時刻,而是T2時刻,這裏T2是oplog記錄的最先時間,該時間並不受咱們控制

補充:這裏的「不受咱們控制」是指在使用mongorestore重作oplog的時候,咱們沒辦法指定開始時間。可是若是想要把oplog的開始時間控制在T3時刻,仍是有辦法的:使用bsondump分析全備的最後一筆數據,在備份oplog的時候,用query選項過濾掉以前的數據便可然而,這並非咱們關心的,我所關心的,是爲何mongorestore不給出恢復操做的開始時間參數。ide

clipboard

說了那麼多,把問題明確一下:測試

mongorestore在恢復oplog的時候,只限定了日誌的結束位置,而沒有開始位置,這樣就會形成oplog恢復的開始位置不是T3,而是在T2,那麼就會存在T2~T3這段時間數據重複操做的問題,理論上會形成數據變化,爲何mongorestore不設定一個開始時間參數去避免重複操做的問題呢?ui

本次測試在mongodb 4.2 副本集環境下進行。spa


(二)問題探索3d

(2.1)oplog日誌格式解析unix

既然該問題可能會發生在重作oplog時,那麼咱們不妨先看一下oplog到底存儲了什麼信息。爲了查看oplog日誌保存了什麼信息,向test集合中插入1條數據:rest

db.test.insert({"empno":1,"ename":"lijiaman","age":22,"address":"yunnan,kungming"});

查看test集合的數據信息

db.test.find()
/* 1 */
{
    "_id" : ObjectId("5f30eb58bcefe5270574cd54"),
    "empno" : 1.0,
    "ename" : "lijiaman",
    "age" : 22.0,
    "address" : "yunnan,kungming"
}

使用下面查詢語句查看oplog日誌信息:

use local db.oplog.rs.find( { $and : [ {"ns" : "testdb.test"} ] } ).sort({ts:1})

結果以下:

/* 1 */
{
    "ts" : Timestamp(1597070283, 1),
    "op" : "i",
    "ns" : "lijiamandb.test",
    "o" : {
        "_id" : ObjectId("5f30eb58bcefe5270574cd54"),
        "empno" : 1.0,
        "ename" : "lijiaman",
        "age" : 22.0,
        "address" : "yunnan,kungming"
    }
}

oplog中各個字段的含義:

  • ts:數據寫的時間,括號裏面第1位數據表明時間戳,是自unix紀元以來的秒值,第2位表明在1s內訂購時間戳的序列數
  • op:操做類型,可選參數有:

       -- "i": insert

       --"u": update

       --"d": delete

       --"c": db cmd

       --"db":聲明當前數據庫 (其中ns 被設置成爲=>數據庫名稱+ '.')

       --"n": no op,即空操做,其會按期執行以確保時效性

  • ns:命名空間,一般是具體的集合
  • o:具體的寫入信息
  • o2: 在執行更新操做時的where條件,僅限於update時纔有該屬性


(2.2)文檔中的「_id」字段

在上面的插入文檔中,咱們發現每插入一個文檔,都會伴隨着產生一個「_id」字段,該字段是一個object類型,對於「_id」,須要知道:

  • "_id"是集合文檔的主鍵,每一個文檔(即每行記錄)都有一個惟一的"_id"值
  • "_id"會自動生成,也能夠手動指定,可是必須惟一且非空


通過測試,發如今執行文檔的DML操做時,會根據ID進行,咱們不妨來看看DML操做的文檔變化。

(1)插入文檔,查看文檔信息與oplog信息

use testdb

//插入文檔
db.mycol.insert({id:1,name:"a"})
db.mycol.insert({id:2,name:"b"})
db.mycol.insert({id:3,name:"c"})
db.mycol.insert({id:4,name:"d"})
db.mycol.insert({id:5,name:"e"})
db.mycol.insert({id:6,name:"f"})

rstest:PRIMARY> db.mycol.find()
{ "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"), "id" : 1, "name" : "a" }
{ "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1"), "id" : 2, "name" : "b" }
{ "_id" : ObjectId("5f3b471a6530eb8aa5bf88a2"), "id" : 3, "name" : "c" }
{ "_id" : ObjectId("5f3b471a6530eb8aa5bf88a3"), "id" : 4, "name" : "d" }
{ "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4"), "id" : 5, "name" : "e" }
{ "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5"), "id" : 6, "name" : "f" }

這裏記錄該集合文檔的變化,能夠發現,mongodb爲每條數據都分配了一個惟一且非空的」_id」:

clipboard

此時查看oplog,以下

/* 1 */
{
    "ts" : Timestamp(1597720346, 2),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "i",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "wall" : ISODate("2020-08-18T03:12:26.231Z"),
    "o" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"),
        "id" : 1.0,
        "name" : "a"
    }
}

/* 2 */
{
    "ts" : Timestamp(1597720346, 3),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "i",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "wall" : ISODate("2020-08-18T03:12:26.246Z"),
    "o" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1"),
        "id" : 2.0,
        "name" : "b"
    }
}

... 略 ...


(2)更新操做

rstest:PRIMARY> db.mycol.update({"id":1},{$set:{"name":"aa"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

這裏更新了1行數據,能夠看到,文檔id是沒有發生變化的clipboard

此時查看oplog,以下:

/* 7 */
{
    "ts" : Timestamp(1597720412, 1),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0")
    },
    "wall" : ISODate("2020-08-18T03:13:32.649Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "name" : "aa"
        }
    }
}

這裏值得咱們注意:上面咱們說到,oplog的」o2」參數是更新的where條件,咱們在執行更新的時候,指定的where條件是」id=1」,id是咱們本身定義的列,然而,在oplog裏面指定的where條件是

"_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"),很明顯,他們都指向了同一條數據。這樣,當咱們使用oplog進行數據恢復的時候,直接根據」_id」去作數據更新,即便再執行N遍,也不會致使數據更新出錯。


(3)再次更新操做

上面咱們是對某一條數據進行更新,而且在update中指出了更新後的數據,這裏再測試一下,我使用自增的方式更新數據。

// 每條數據的id在當前的基礎上加10
rstest:PRIMARY> db.mycol.update({},{$inc:{"id":10}},{multi:true}) WriteResult({ "nMatched" : 6, "nUpserted" : 0, "nModified" : 6 })

數據變化如圖,能夠看到,id雖然發生了變化,可是」_id」是沒有改變的。

clipboard

再來看oplog信息

/* 8 */
{
    "ts" : Timestamp(1597720424, 1),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0")
    },
    "wall" : ISODate("2020-08-18T03:13:44.398Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 11.0
        }
    }
}

/* 9 */
{
    "ts" : Timestamp(1597720424, 2),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1")
    },
    "wall" : ISODate("2020-08-18T03:13:44.399Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 12.0
        }
    }
}

/* 10 */
{
    "ts" : Timestamp(1597720424, 3),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a2")
    },
    "wall" : ISODate("2020-08-18T03:13:44.399Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 13.0
        }
    }
}

/* 11 */
{
    "ts" : Timestamp(1597720424, 4),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a3")
    },
    "wall" : ISODate("2020-08-18T03:13:44.400Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 14.0
        }
    }
}

/* 12 */
{
    "ts" : Timestamp(1597720424, 5),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4")
    },
    "wall" : ISODate("2020-08-18T03:13:44.400Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 15.0
        }
    }
}

/* 13 */
{
    "ts" : Timestamp(1597720424, 6),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "u",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "o2" : {
        "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5")
    },
    "wall" : ISODate("2020-08-18T03:13:44.400Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "id" : 16.0
        }
    }
}

這裏也很是值得咱們注意:o2記錄的是已經發生更改的文檔_id,o就比較有意思了,記錄的是發生變動以後的值。咱們能夠發現,若是咱們把上面自增更新的SQL執行每執行1次,id都會加10,可是,咱們重複執行N次oplog,並不會改變對應記錄的值。


(4)再來看看刪除操做

// 刪除id大於14的條目
rstest:PRIMARY> db.mycol.remove({"id":{"$gt":14}}) 
WriteResult({ "nRemoved" : 2 })

數據變化以下圖:

clipboard

再來看看oplog日誌:

/* 14 */
{
    "ts" : Timestamp(1597720485, 1),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "d",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "wall" : ISODate("2020-08-18T03:14:45.511Z"),
    "o" : {
        "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4")
    }
}

/* 15 */
{
    "ts" : Timestamp(1597720485, 2),
    "t" : NumberLong(11),
    "h" : NumberLong(0),
    "v" : 2,
    "op" : "d",
    "ns" : "testdb.mycol",
    "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
    "wall" : ISODate("2020-08-18T03:14:45.511Z"),
    "o" : {
        "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5")
    }
}

」op」:」d」選項記錄了該操做是執行刪除,具體刪除什麼數據,由o選項記錄,能夠看到,o記錄的是」_id」,也就是說,oplog中刪除操做是根據」_id」執行的。


(三)結論

能夠看到,在DML操做數據庫時,oplog時基於"_id"記錄文檔變化的。那麼,咱們來總結一下開頭提出的問題:未指定開始時間,oplog數據是否會重複操做呢?

  • 若是當前數據庫已經存在相同id的數據,那麼不會執行二次插入,主鍵衝突報錯;
  • 在作更新時,記錄的是更新文檔的"_id"以及發生變動後的數據,所以,若是再次執行,只會修改該條數據,哪怕執行N遍,效果也和執行一遍是同樣的,全部也就不怕重複操做單條數據了;
  • 在執行刪除操做時,記錄的是刪除的文檔"_id",一樣,執行N遍和執行一遍效果是同樣的,由於」_id」是惟一的。

所以,即便oplog從徹底備份以前開始應用,也不會形成數據的屢次變動。


【完】

相關文檔:

1.MongoDB 2.7主從複製(master –> slave)環境基於時間點的恢復  
2.MongoDB 4.2副本集環境基於時間點的恢復


3.MongoDB恢復探究:爲何oplogReplay參數只設置了日誌應用結束時間oplogLimit,而沒有設置開始時間?

相關文章
相關標籤/搜索