MongoDB案例分享:如何使用oplog恢復資料

MongoDB中文社群發表於2022-03-11


最近跟資料恢復槓上了,這不又來一例。關於備份恢復的問題其實我在6年多以前就寫過,其中大部分討論放在今天仍然適用。


1

案例介紹


某使用者使用了MongoDB 4.0,資料庫中的一個表因為 drop 操作導致資料全部丟失。但因為庫本身很小,而 oplog 空間足夠大,所以從建庫至今的所有操作都尚在 oplog 中沒有被回收。基於這種情況,雖然他們沒有全量備份,我們仍然可以通過完整重放 oplog 來找回所有丟失的資料。所以我們的操作是:

  1. 匯出 oplog

  2. 尋找 drop 發生的時間戳;

  3. 重放到 drop 前一刻;

  4. 將恢復的資料dump/restore到生產庫;

步驟4屬於基本操作就不詳細敘述了,主要來看前面3步。


2

恢復步驟

2.1 匯出oplog

這一步實際上特別簡單。 oplog 位於 local.oplog.rs 集合中,我們可以使用 mongodump 直接匯出,匯出節點可以是主節點或從節點。基本形式是:

mongodump --host <host>:<port> -d local -c oplog.rs -u <user> --authenticationDatabase <adb>

得到如下輸出:


> tree dump

dump
└── local
   ├── oplog.rs.bson
   └── oplog.rs.metadata.json

1 directory, 2 files

我們需要的就是 oplog.rs.bson


2.2 尋找截止時間戳

進行重放的關鍵是要先找出重放截止到哪條 oplog 。這裡有兩種辦法:

方案1

oplog.rs.bson 中搜尋關鍵字 drop


> bsondump dump/local/oplog.rs.bson 
| grep drop

{ "ts ":{ " $timestamp ":{ "t ":1646056645, "i ":1}}, "t ":{ " $numberLong ": "1 "}, "h ":{ " $numberLong ": "7307295890643732087 "}, "v ":{ " $numberInt ": "2 "}, "op ": "c ", "ns ": "test. $cmd ", "ui ":{ " $binary ":{ "base64 ": "9sakiEOMS2qjwBZ5O0mQjQ== ", "subType ": "04 "}}, "wall ":{ " $date ":{ " $numberLong ": "1646056645661 "}}, "o ":{ "drop ": "survey "}}

如果多次出現 drop 記錄,則要自己注意辨別哪條是你想要的那條。然後注意記錄中 {"t":1646056645,"i":1} 是我們要截止到的時間戳,後面將會用到這個資料。
另外注意如果
oplog 較多時該辦法可能會耗時較長。

方案2

local.oplog.rs 中查詢。這種查詢方法通常會比方案1快,但需要在原始系統上執行查詢,可能造成一定的負擔。如果系統本身壓力已經較大,則要注意避開業務高峰期。另外也可以在從節點上執行查詢以避開壓力最大的主節點。這裡要注意的是每個節點上儲存的 oplog 可能不一樣多,但一定是一致的。例如,某個節點上的 oplog 有1,2,3,4,5共計5條,其他節點上可能只有:

  • 2,3,4,5

  • 3,4,5

  • 4,5

  • 5

這種情況通常是由於從節點是後來加進叢集裡導致的。那麼想要查詢時,可以使用:


> 
use 
local

> db. oplog . rs . find ({ "o.drop": { $exists: true }}). sort ({ $natural: -1 }). limit ( 1 );
{ "ts" : Timestamp ( 1646056729 , 1 ), "t" : NumberLong ( 1 ), "h" : NumberLong ( "6882491835596436855" ), "v" : 2 , "op" : "c" , "ns" : "test.$cmd" , "ui" : UUID ( "a98cba5a-066b-46fe-92a9-d122386dba5d" ), "wall" : ISODate ( "2022-02-28T13:58:49.167Z" ), "o" : { "drop" : "survey" } }

同樣注意 Timestamp(1646056729, 1) 是我們將要用到的截止時間戳。


2.3 重放oplog

mongorestore 本身是用來恢復 bson 檔案的同時順便重放 oplog 的。現在我們沒有 bson 要恢復,只有 oplog 要重放,所以需要點小花招來欺騙 mongorestore ,那就是用一個空資料夾:

mkdir empty

mongorestore --host <host >: <port > -u <user > --authenticationDatabase <adb > \
 --oplogReplay \
 --oplogFile dump/local/oplog.rs.bson \
 --oplogLimit 1646056729:1 \
 empty/

注意:這裡應該在一個新的例項上完成重放操作。
重放完成後,你就擁有了一份截止到
drop 操作前的完整資料。


3

改進方案

上面的步驟雖然可以完成任務,但有些浪費。因為丟失的只有一個表,我們卻恢復了整個資料庫,消耗了不必要的時間。有沒有辦法只恢復丟失的那一個表呢?從原理來講是可以辦到的,那就是隻重放這個表上的 oplog ,那麼只需要在匯出 oplog 的時候做個過濾就可以辦到了:

mongodump --host <host>:<port> -d local -c oplog.rs -u <user> --authenticationDatabase <adb> -q '{"ns": "test.survey"}'


後續步驟沒有什麼差異,就不再贅述了。但是這樣的做法有個bug,那就是 事務。我一開始也栽在了這個問題上。事務會把多條操作放在一條 oplog 裡,以此來保證事務的原子性。比如如下事務操作:


var 
mongo 
= 
db.
getMongo
();

var session = mongo. startSession ();
session. startTransaction ();
var coll = session. getDatabase ( "test" ). getCollection ( "survey" );
coll. insertOne ({ y: 1 });
coll. insertOne ({ y: 2 });
coll. insertOne ({ y: 3 });
session. commitTransaction ();


其產生的 oplog 是這樣的:


{

    "ts": Timestamp ( 1646057834 , 1 ),
    "t": NumberLong ( 1 ),
    "h": NumberLong ( "-2362908976881142089" ),
    "v": 2 ,
    "op": "c" ,
    "ns": "admin.$cmd" ,
    "wall": ISODate ( "2022-02-28T14:17:14.189Z" ),
    "lsid": {
        "id": UUID ( "02ca1f7e-f451-4ec3-946f-cf307c0d03b7" ),
        "uid": BinData ( 0 , "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" )
    },
    "txnNumber": NumberLong ( 1 ),
    "stmtId": 0 ,
    "prevOpTime": {
        "ts": Timestamp ( 0 , 0 ),
        "t": NumberLong ( -1 )
    },
    "o": {
        "applyOps": [{
            "op": "i" ,
            "ns": "test.survey" ,
            "ui": UUID ( "04a8b634-4048-48a6-b358-9a879c1a20ed" ),
            "o": {
                "_id": ObjectId ( "621cd969a3a94c2e74b595c5" ),
                "y": 1
            }
        }, {
            "op": "i" ,
            "ns": "test.survey" ,
            "ui": UUID ( "04a8b634-4048-48a6-b358-9a879c1a20ed" ),
            "o": {
                "_id": ObjectId ( "621cd969a3a94c2e74b595c6" ),
                "y": 2
            }
        }, {
            "op": "i" ,
            "ns": "test.survey" ,
            "ui": UUID ( "04a8b634-4048-48a6-b358-9a879c1a20ed" ),
            "o": {
                "_id": ObjectId ( "621cd969a3a94c2e74b595c7" ),
                "y": 3
            }
        }]
    }
}


可見這裡的 {"ns": "admin.$cmd"} 並不在 test.survey 上,所以上面的過濾辦法會把事務產生的資料都排除在外,就會造成一部分資料丟失。解決辦法也很簡單,修改一下過濾條件:

mongodump --host <host>:<port> -d local -c oplog.rs -u <user> --authenticationDatabase <adb> -q '{"$or": [{"ns": "test.survey"}, {"o.applyOps.ns": "test.survey"}]}'



4

結束語


這個案例是個很極端的情況,所以不要想著抄作業,你幾乎一定不會遇到相同的場景。但恢復的原理卻是相通的,無論何種備份恢復都是“全量”+“增量”的做法,只要你理解了原理,剩下的就是動手嘗試而已。



關於作者: 張耀星


MongoDB中文社群常委會委員,論壇聯席主席。

MongoDB公司北亞區首席技術諮詢服務顧問。在MongoDB的開發、應用和諮詢服務方面,擁有多年的豐富實踐經驗。

作為MongoDB認證專家,曾經為不同行業的各類大型客戶提供過培訓、效能調優、架構設計等各類技術及諮詢服務,頗得廣大客戶信任。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69961190/viewspace-2869415/,如需轉載,請註明出處,否則將追究法律責任。

相關文章