MongoDB_数据库如何进行PIT恢复
MongoDB_数据库如何进行PIT恢复

MongoDB_数据库如何进行PIT恢复

下面是机器分布情况:

用途IP:PORT角色主机名MongoDB版本
生产主库172.18.3.167:27101Primarydb-test02社区版6.0.4
生产备库172.18.3.168:27101Secondarydb-test03社区版6.0.4
恢复机器172.18.3.166:27101db-test01社区版6.0.4

mongodump版本:100.7.0

mongodump version: 100.7.0
git version: 17946f45f5fabfcdd99f8960b9109f976d370631
Go version: go1.19.6
   os: linux
   arch: amd64
   compiler: gc

大致步骤

  1. 导出oplog,可以在主库或者备库导;
  2. 寻找oplog中误操作发生的时间戳;
  3. 全备+增备重放到误操作的前一刻;
  4. 将恢复的数据dump/restore到生产库;

模拟步骤

1. 首先我们往生产集群一个集合t1里插入部分测试数据。

ycf [direct: primary] mydb> for (var i=0; i < 1000; i++) { db.t1.insert({"a":i}) }
{
  acknowledged: true,
  insertedIds: { '0': ObjectId("65239d51fa195c51fa67afde") }
}

2. 检查下collection t1中的数据

总共1000条

3. 开始备份

直接采用物理备份,在备节点锁定之后直接拷贝物理文件

db.fsyncLock()

在备节点 172.18.3.168上直接将所有数据文件拷贝到恢复机器172.18.3.166上

scp -r  /usr/local/data/mongodb/data/*   172.18.3.166:/usr/local/data/mongodb/data/

备份完成,解锁

db.fsyncUnlock()

4. 恢复机器172.18.3.166上启动实例

正常启动之后,此时相当于全备已经恢复完成了。

/usr/local/data/mongodb/bin/mongod -f /usr/local/data/mongodb/param/mongodb.conf

5. 继续往生产集群中插入一条记录,然后等待几秒后再不带条件的remove(remove({}))

db.t1.insert({"a":20000})
db.t1.countDocuments()
db.t1.remove({})
t1这张表全部没有了,我们假设最后remove({})是误操作,现在需求是要求数据恢复到误操作之前,总数据量是1001条数据

6. 先备份一次oplog(在主库和备库都可以)

mongodump  mongodb://admin:admin_pwd@127.0.0.1:27101 --authenticationDatabase admin -d local -c oplog.rs -o   /tmp/oplog/ 

#并且复制oplog文件到恢复机器上
scp -r  /tmp/oplog 172.18.3.166:/tmp/  

7. 找到误操作时间戳,并导出上一次备份到误操作前的oplog

 db.oplog.rs.find({"op" : "d", "ns" : "mydb.t1"}).sort({ts:1}).limit(3)

可以发现误操作的时间点是在(1696833877,1)。

8. 在恢复机器上开始恢复

因为该机器上刚才恢复的全备数据库已经有集群信息了,所以要做下特殊处理,修改一下副本集集群信息。如果重新初始化会看到如下信息:

conf = rs.conf()
delete conf.members[1]          #删除多余的member
conf.members[0].host = "172.18.3.166:27101"
rs.reconfig(conf, {force:1})   #强制重新配置副本集

开始应用oplog,做定点恢复,此时不能指定库名和集合名,会报错。

mongorestore mongodb://admin:admin_pwd@127.0.0.1:27101  --authenticationDatabase admin \
  --oplogReplay \
  --oplogFile /tmp/oplog/local/oplog.rs.bson \
  --oplogLimit 1696833877:1 
  
  
 
 
 
# --oplogReplay                         replay oplog for point-in-time restore
# --oplogLimit=<seconds>[:ordinal]      only include oplog entries before the provided Timestamp
# --oplogFile=<filename>                oplog file to use for replay of oplog

恢复成功,因为有些表记录已经存在了,所以恢复时会直接报记录重复错误,并且提示会有部分文档恢复失败,忽略即可

9. 查看恢复之后的数据

最后插入的{a: 20000}这条数据也存在了,t1集合总数据量1001,符合恢复预期

10. 将恢复之后的数据重新dump/restore到生产数据库

省略,自行操作。

可以优化的步骤

因为丢失的只有一个表,我们却恢复了整个数据库,消耗了不必要的时间, 因为我们导出oplog时候没有添加过滤条件

因此我们可以设置以下几个过滤条件:

  • 设置起止时间:开始时间在全备之前({“wall” : ISODate(‘2023-10-08T16:00:00.000Z’)}),结束时间截止为误操作时间({“ts”: Timestamp({t: 1696833877, i: 1 })})。
  • 只导出mydb.t1这个表的oplog({“ns”: “mydb.t1”})。
mongodump mongodb://admin:admin_pwd@127.0.0.1:27101  --authenticationDatabase admin \
-d local    \
-c oplog.rs  \
-q '{"$and": [{"ns": "mydb.t1"}, {"wall":{"$gte": { "$date": "2023-10-08T16:00:00.000Z"}}},  {"ts":{"$lt": {"$timestamp": {"t": 1696833877, "i": 1}}}}    ]}' \
-o  oplog/

这样过滤之后oplog里面的内容将会少很多,能节省大量的恢复时间。

上面的命令有一个bug,那就是有可能存在事务的情况,事务会将所有的操作都放在一个条oplog里面,但是在oplog中ns字段却不是mydb.t1。

列如事务如下:

var mongo = db.getMongo();
var session = mongo.startSession();
session.startTransaction();
var coll = session.getDatabase("mydb").getCollection("t1");
coll.insertOne({a: 50000});
coll.insertOne({a: 50001});
coll.insertOne({a: 50000});
session.commitTransaction();

产生的oplog如下:

 {
    lsid: {
      id: new UUID("926b0659-2dbf-4b7e-8384-d66956def791"),
      uid: Binary(Buffer.from("3b408cb48548b5037822c10eb0976b3cbf2cee3bf9c708796bf03941fbecd80f", "hex"), 0)
    },
    txnNumber: Long("1"),
    op: 'c',
    ns: 'admin.$cmd',
    o: {
      applyOps: [
        {
          op: 'i',
          ns: 'mydb.t1',
          ui: new UUID("6bab8d6f-2cf9-447e-ba03-e00cb5b7a738"),
          o: { _id: ObjectId("6524cc22436cd6fa89f1a824"), a: 50000 },
          o2: { _id: ObjectId("6524cc22436cd6fa89f1a824") }
        },
        {
          op: 'i',
          ns: 'mydb.t1',
          ui: new UUID("6bab8d6f-2cf9-447e-ba03-e00cb5b7a738"),
          o: { _id: ObjectId("6524cc22436cd6fa89f1a825"), a: 50001 },
          o2: { _id: ObjectId("6524cc22436cd6fa89f1a825") }
        },
        {
          op: 'i',
          ns: 'mydb.t1',
          ui: new UUID("6bab8d6f-2cf9-447e-ba03-e00cb5b7a738"),
          o: { _id: ObjectId("6524cc22436cd6fa89f1a826"), a: 50000 },
          o2: { _id: ObjectId("6524cc22436cd6fa89f1a826") }
        }
      ]
    },
    ts: Timestamp({ t: 1696910371, i: 1 }),
    t: Long("3"),
    v: Long("2"),
    wall: ISODate("2023-10-10T03:59:31.458Z"),
    prevOpTime: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") }
  }

所以还要加一个事务的过滤条件,最终命令如下:

mongodump mongodb://admin:admin_pwd@127.0.0.1:27101  --authenticationDatabase admin \
-d local    \
-c oplog.rs  \
-q '{"$and": [  {"$or":[{"o.applyOps.ns": "mydb.t1"},{"ns": "mydb.t1"}]},  {"wall":{"$gte": { "$date": "2023-10-08T16:00:00.000Z"}}},  {"ts":{"$lt": {"$timestamp": {"t": 1696833877, "i": 1}}}}  ]}'  \
-o  oplog/

恢复过程可能遇到的问题

1. mongodump导出oplog时常规写法使用wall或者ts字段无法过滤,ns字段能正常过滤。

mongodump mongodb://admin:admin_pwd@127.0.0.1:27101  --authenticationDatabase admin \
-d local    \
-c oplog.rs  \
-q '{"$and": [{"ns": "mydb.t1"},  {"wall":{"$gte": "ISODate('2023-10-08T16:00:00.000Z')"}}, {"ts":{"$lt": "Timestamp({'t': 1696833877, 'i': 1 })"}}  ] }' \
-o  oplog/ 

这里明明oplog有数据,但是却导出0 documents,这是因为写法不对,要做下相应的转换,使用$timestamp和$data操作符

{"wall":{"$gte": "ISODate('2023-10-08T16:00:00.000Z')"}}   换成  {"wall":{"$gte": { "$date": "2023-10-08T16:00:00.000Z"}}}

 {"ts":{"$lt": "Timestamp({'t': 1696833877, 'i': 1 })"}}  换成 {"ts":{"$lt": {"$timestamp": {"t": 1696833877, "i": 1}}}}

最终的导出命令命令如下(注意区别):

mongodump mongodb://admin:admin_pwd@127.0.0.1:27101  --authenticationDatabase admin \
-d local    \
-c oplog.rs  \
-q '{"$and": [  {"$or":[{"o.applyOps.ns": "mydb.t1"},{"ns": "mydb.t1"}]},  {"wall":{"$gte": { "$date": "2023-10-08T16:00:00.000Z"}}},  {"ts":{"$lt": {"$timestamp": {"t": 1696833877, "i": 1}}}}  ]}'  \
-o  oplog/