2

MongoDB案例分享:如何使用oplog恢复数据

 2 years ago
source link: https://mongoing.com/archives/81744
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近跟数据恢复杠上了,这不又来一例。关于备份恢复的问题其实我在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。这里有两种办法:

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较多时该办法可能会耗时较长。

local.oplog.rs中查询。这种查询方法通常会比方案1快,但需要在原始系统上运行查询,可能造成一定的负担。如果系统本身压力已经较大,则要注意避开业务高峰期。另外也可以在从节点上执行查询以避开压力最大的主节点。这里要注意的是每个节点上保存的oplog可能不一样多,但一定是一致的。例如,某个节点上的oplog有1,2,3,4,5共计5条,其他节点上可能只有:

  • 2,3,4,5
  • 3,4,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认证专家,曾经为不同行业的各类大型客户提供过培训、性能调优、架构设计等各类技术及咨询服务,颇得广大客户信任。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK