最近跟数据恢复杠上了,这不又来一例。对于备份复原的问题其实我在6年多以前就写过,其中大部分探讨放在明天依然实用。

案例介绍

某用户应用了MongoDB 4.0,数据库中的一个表因为drop操作导致数据全副失落。但因为库自身很小,而oplog空间足够大,所以从建库至今的所有操作都尚在oplog中没有被回收。基于这种状况,尽管他们没有全量备份,咱们依然能够通过残缺重放oplog来找回所有失落的数据。所以咱们的操作是:

导出oplog;

寻找drop产生的工夫戳;

重放到drop前一刻;

将复原的数据dump/restore到生产库;

步骤4属于基本操作就不具体叙述了,次要来看后面3步。

复原步骤

2.1 导出oplog
这一步实际上特地简略。oplog位于local.oplog.rs汇合中,咱们能够应用mongodump间接导出,导出节点能够是主节点或从节点。根本模式是:

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

失去如下输入:

> tree dumpdump└── 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 emptymongorestore --host <host>:<port> -u <user> --authenticationDatabase <adb> \  --oplogReplay \  --oplogFile dump/local/oplog.rs.bson \  --oplogLimit 1646056729:1 \  empty/

留神:这里应该在一个新的实例上实现重放操作。
重放实现后,你就领有了一份截止到drop操作前的残缺数据。

改良计划

下面的步骤尽管能够实现工作,但有些节约。因为失落的只有一个表,咱们却复原了整个数据库,耗费了不必要的工夫。有没有方法只复原失落的那一个表呢?从原理来讲是能够办到的,那就是只重放这个表上的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"}]}'

结束语

这个案例是个很极其的状况,所以不要想着抄作业,你简直肯定不会遇到雷同的场景。但复原的原理却是相通的,无论何种备份复原都是“全量”+“增量”的做法,只有你了解了原理,剩下的就是入手尝试而已。

对于作者:张耀星

MongoDB中文社区常委会委员,论坛联席主席。

MongoDB公司北亚区首席技术咨询服务参谋。在MongoDB的开发、利用和咨询服务方面,领有多年的丰盛实践经验。

作为MongoDB认证专家,已经为不同行业的各类大型客户提供过培训、性能调优、架构设计等各类技术及咨询服务,颇得广大客户信赖。