侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

MongoDB运维篇(4)之备份恢复

zze
zze
2020-06-20 / 0 评论 / 0 点赞 / 827 阅读 / 21438 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

工具介绍

针对 MongoDB 做备份恢复的工具常用的有如下两套:

  • mongoexport/mongoimport:导出/导入的是 JSON 格式或者是 CSV 格式;
  • mongodump/mongorestore:导出/导入的是 BSON 格式;

JSON 可读性强但体积较大,BSON 则是二进制文件,体积小但对人类几乎没有可读性。

在一些 MongoDB 版本之间,BSON 格式可能会随版本的不同而不同,所以不同版本之间用 mongodump/mongorestore 迁移可能会失败,具体要看版本之间的兼容性。
当无法使用 BSON 进行跨版本的数据迁移时,使用 JSON 格式即 mongoexport/mongoimport 则是一个可以考虑的方案。

使用 mongodump/mongorestore 进行跨版本迁移个人并不推荐,实在要做的话请查阅文档确认两个版本之间是否兼容(大部分时候是兼容的)。
注意:JSON 虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引、账户等其它基础信息。

应用场景总结:

mongoexport/mongoimportmongodump/mongorestore
异构平台迁移×
同平台,跨大版本迁移×
日常备份恢复×

:推荐方案,×:不推荐方案。

下面就来看一看这两套工具具体该怎么使用~~~

在开始之前最起码需要准备一个 MongoDB 实例,我这里准备的 MongoDB 实例中存在一个管理员用户 root,密码为 root123,认证库为 admin

然后准备测试集合并录入数据:

> use test
# 录入数据
> for(i=0;i<10000;i++){db.log.insert({"uid":i,"name":"mongodb","age":6,"date":new Date()})}

mongoexport/mongoimport

备份

mongoexport 使用说明如下:

$ mongoexport --help  
参数说明:
    -h:指明数据库宿主机的 IP
    -u:指明数据库的用户名
    -p:指明数据库的密码
    -d:指明数据库的名字
    -c:指明 collection 的名字
    -f:指明要导出那些列
    -o:指明到要导出的文件名
    -q:指明导出数据的过滤条件
    --authenticationDatabase:指定验证库

由于 mongoexport 导出的是 JSON 格式的文本文件,所以其只能针对具体库下的具体集合进行导出。


test 库的 log 集合导出为 JSON 格式文件:

$ mongoexport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log -o /tmp/test.log.json

检查导出的 JSON 文件:

$ tail -5 /tmp/test.log.json 
{"_id":{"$oid":"5eeb5f6f29f957b062ca2caa"},"uid":9995.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:55.171Z"}}
{"_id":{"$oid":"5eeb5f6f29f957b062ca2cab"},"uid":9996.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:55.171Z"}}
{"_id":{"$oid":"5eeb5f6f29f957b062ca2cac"},"uid":9997.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:55.176Z"}}
{"_id":{"$oid":"5eeb5f6f29f957b062ca2cad"},"uid":9998.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:55.176Z"}}
{"_id":{"$oid":"5eeb5f6f29f957b062ca2cae"},"uid":9999.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:55.176Z"}}

test 库的 log 集合导出为 CSV 格式文件:

# 使用 --type csv 指定导出为 CSV 格式文件,同时还必须使用 -f 选项指定导出的属性名称
$ mongoexport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log --type=csv -f uid,name,age,date -o /tmp/test.log.csv

检查导出的 CSV 文件:

$ tail -5 /tmp/test.log.csv
9995,mongodb,6,2020-06-18T12:34:55.171Z
9996,mongodb,6,2020-06-18T12:34:55.171Z
9997,mongodb,6,2020-06-18T12:34:55.176Z
9998,mongodb,6,2020-06-18T12:34:55.176Z
9999,mongodb,6,2020-06-18T12:34:55.176Z

恢复

mongoimport 使用说明如下:

$ mongoimport --help
参数说明:
    -h:指明数据库宿主机的IP
    -u:指明数据库的用户名
    -p:指明数据库的密码
    -d:指明数据库的名字
    -c:指明 collection 的名字
    -f:指明要导入那些列
    -j, --numInsertionWorkers=<number>:开启并发导入,值为一个数字,表示并发的线程数量,默认值为 1

mongoimport 可以把一个特定格式文件的内容导入到指定的 collection 中,该工具可以导入 JSON 格式数据,也可以导入 CSV 格式数据。


从 JSON 格式文件导入:

# 以将 /tmp/test.log.json 导入到 test 库下的 log_json 集合中为例:
$ mongoimport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log_json -j 2 /tmp/test.log.json

从 CSV 格式文件导入:

# 对于 CSV 格式的文件导入有两种情况,一种是含 header 的 CSV 文件,一种是不含 header 的 CSV 文件

# 先检查一下之前使用 mongoexport 导出的 CSV 文件,可以看到是含有 header 的 
$ head -2 /tmp/test.log.csv 
uid,name,age,date
0,mongodb,6,2020-06-18T12:34:52.335Z
# 此时执行导入操作则需要通过 --headerline 选项标识导入的 CSV 文件是包含 header 的,第一行不需要导入
# 以将 /tmp/test.log.csv 导入到 test 库下的 log_csv_withheader 集合中为例:
$ mongoimport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log_csv_withheader --type=csv --headerline --file  /tmp/test.log.csv

# 继续演示一下导入没有 header 的 CSV 文件,先将 /tmp/test.log.csv 的第一行删除
$ sed -i '1d' /tmp/test.log.csv
# 导入没有 header 的 CSV 文件时,需要通过 -f 选项指定每列对应在 doc 中的属性名
# 以将 /tmp/test.log.csv 导入到 test 库下的 log_csv_withoutheader 集合中为例:
$ mongoimport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log_csv_withoutheader --type=csv -f id,name,age,date --file /tmp/test.log.csv

案例

将 MySQL 中指定表数据迁移到 MongoDB 的集合中。

1、我这里就在 MongoDB 所在主机创建一个 MySQL 实例,root 用户密码为 123,下面在 test 库中执行下面 SQL 创建 user 表并录入 50000 条测试数据:

drop table if exists user;
create table user
(
    id     int auto_increment
        primary key,
    name   varchar(24)      not null,
    age    tinyint unsigned not null,
    gender char(1)          null check ( 'F' 'M' )
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

drop procedure if exists insert_user;
create procedure insert_user(count int)
begin
    declare i int default 1;
    while i <= count
        do
            -- 循环开始
            insert into user(name, age, gender)
            values (substring(MD5(RAND()), 1, 10), round(rand() * 100),
                    case round(rand() * 1000) % 2 when 0 then 'F' else 'M' end);
            set i = i + 1;
        end while; -- 循环结束
end;
-- 执行存储过程
call insert_user(50000);
-- 删除存储过程
drop procedure if exists insert_user;

2、导出 MySQL 中 test 库下的 user 表数据为 CSV 格式文件存放在 /tmp/test.user.csv

-- fields terminated by ',' 表示各列之间以 , 分隔
mysql> select * from test.user into outfile '/tmp/test.user.csv' fields terminated by ',';

【提示】MySQL 导出或导入 CSV 格式文件的方法如下:

-- 导出
select * from test_info   
into outfile '/tmp/test.csv'   
fields terminated by ','    -- 字段间以,号分隔
optionally enclosed by '"'   -- 字段用"号括起
escaped by '"'            -- 字段中使用的转义符为"
lines terminated by '\r\n';  -- \r\n结束

-- 导入
load data infile '/tmp/test.csv'   
into table test_info    
fields terminated by ','  
optionally enclosed by '"' 
escaped by '"'   
lines terminated by '\r\n'; 

-- terminated by ',':字段间以 , 分隔;
-- enclosed by '"':字段前后加上 "
-- escaped by '"':指定转义符为 "
-- lines terminated by '\r\n':指定行分隔符为 \r\n

3、检查导出文件的格式:

$ head -5 /tmp/test.user.csv 
1,1b1dd27721,18,M
2,71e8260808,1,M
3,e4ccd39ffa,85,F
4,f09f576d6d,95,M
5,d6088bba10,66,M

4、将通过 MySQL 导出的 CSV 文件导入到 MongoDB 中 test 库下的 user 集合中:

$ mongoimport -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c user --type=csv -f id,name,age,gender --file /data/test.user.csv

5、登入 MongoDB,检查数据是否正常被导入:

# 查看 user 集合记录总数
> db.user.count()
50000
# 检查数据是否正常
> db.user.find()
{ "_id" : ObjectId("5eeb7b4ab5a4e98dde68b136"), "id" : 1, "name" : "1b1dd27721", "age" : 18, "gender" : "M" }
{ "_id" : ObjectId("5eeb7b4ab5a4e98dde68b137"), "id" : 2, "name" : "71e8260808", "age" : 1, "gender" : "M" }
{ "_id" : ObjectId("5eeb7b4ab5a4e98dde68b138"), "id" : 3, "name" : "e4ccd39ffa", "age" : 85, "gender" : "F" }
...

mongodump/mongorestore

备份

mongodump 使用说明如下:

$ mongodump --help
参数说明:
    -h:指明数据库宿主机的IP
    -u:指明数据库的用户名
    -p:指明数据库的密码
    -d:指明数据库的名字
    -c:指明collection的名字
    -o:指明到要导出的文件名
    -q:指明导出数据的过滤条件
    --gzip:导出的同时进行压缩
    --j, --numInsertionWorkers=<number>:开启并发导入,值为一个数字,表示并发的线程数量,默认值为 1
    --oplog:备份的同时备份oplog

mongodump 能够在 MongoDB 运行时进行备份,它的工作原理是对运行的 MongoDB 做查询,然后将所有查到的文档写入磁盘。
但是存在的问题是使用 mongodump 产生的备份不一定是数据库的实时快照,如果我们在备份时对数据库进行了写入操作,则备份出来的文件可能不完全和 MongoDB 实时数据相等。另外在备份时可能会对其它客户端性能产生不利的影响。


全库备份:

# 全库备份 27017 实例
$ mongodump -uroot -proot123 --port 27017 --authenticationDatabase admin -o /tmp/mongo_backup
# 它的备份结果是一个目录,在备份的目标目录下每个库的数据单独存放在一个目录
$ tree /tmp/mongo_backup
/tmp/mongo_backup
├── admin
│   ├── system.users.bson
│   ├── system.users.metadata.json
│   ├── system.version.bson
│   └── system.version.metadata.json
└── test
    ├── log.bson
    ├── log_csv_withheader.bson
    ├── log_csv_withheader.metadata.json
    ├── log_csv_withoutheader.bson
    ├── log_csv_withoutheader.metadata.json
    ├── log_json.bson
    ├── log_json.metadata.json
    ├── log.metadata.json
    ├── user.bson
    └── user.metadata.json

2 directories, 14 files

通过上面的备份结果可以看到,最终的集合数据以二进制形式存放在 .bson 后缀文件中,这里我们可以使用 bsondump 来将该类二进制文件转为文本形式的 JSON 格式文件,如下:

# 转换后的文本直接输出到标准输出中,如果需要保存将其重定向到文件中即可。
$ bsondump /tmp/mongo_backup/test/log_json.bson | head -2
{"_id":{"$oid":"5eeb5f6c29f957b062ca05a0"},"uid":1.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:52.340Z"}}
{"_id":{"$oid":"5eeb5f6c29f957b062ca05a1"},"uid":2.0,"name":"mongodb","age":6.0,"date":{"$date":"2020-06-18T12:34:52.341Z"}}
2020-06-19T20:49:21.935+0800    492 objects found
2020-06-19T20:49:21.935+0800    write /dev/stdout: broken pipe

单库备份:

# 以备份 test 库为例
$ mongodump -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -o /tmp/mongo_backup_test
# 它的备份结果也是一个目录,库中的每个集合都有两个文件,分别保存数据和元数据
$ tree /tmp/mongo_backup_test
/tmp/mongo_backup_test
└── test
    ├── log.bson
    ├── log_csv_withheader.bson
    ├── log_csv_withheader.metadata.json
    ├── log_csv_withoutheader.bson
    ├── log_csv_withoutheader.metadata.json
    ├── log_json.bson
    ├── log_json.metadata.json
    ├── log.metadata.json
    ├── user.bson
    └── user.metadata.json

单集合备份:

# 以备份 test 库的 log 集合为例
$ mongodump -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log -o /tmp/mongodb_backup_test_log
# 它的备份结果也是依旧是目录,目录中有分别保存集合数据和元数据的两个文件
$ tree /tmp/mongodb_backup_test_log
/tmp/mongodb_backup_test_log
└── test
    ├── log.bson
    └── log.metadata.json

1 directory, 2 files

恢复

mongorestore 使用说明如下:

$ mongorestore --help
参数说明:
    -h:指明数据库宿主机的IP
    -u:指明数据库的用户名
    -p:指明数据库的密码
    -d:指明要恢复的数据库的名字
    --gzip:指明从压缩格式的文件恢复
    --drop:在恢复之前删除同名集合,不建议使用

为演示恢复操作,先登入 MongoDB 删除 test 库:

> use test
switched to db test
> db.dropDatabase()
{ "dropped" : "test", "ok" : 1 }

从全库备份中恢复 test 库:

# 也可以从单库备份中恢复,指定好 test 库对应的目录路径即可,在上面单库备份中 test 库的备份路径就是 /tmp/mongo_backup_test
$ mongorestore -uroot -proot123 --port 27017 --authenticationDatabase admin -d test  /tmp/mongo_backup/test

再来演示一下集合恢复操作,先登入 MongoDB 删除 test 库中的 log 集合:

> use test
switched to db test
> db.log.drop()
true

从单库备份中恢复 test 库中的 log 集合:

# 如果恢复的是压缩格式的文件,则需要指定 --gzip
$ mongorestore -uroot -proot123 --port 27017 --authenticationDatabase admin -d test -c log /tmp/mongo_backup_test/test/log.bson 

oplog

oplog 只有在 Replication Set 模式下才能使用,在 Replication Set 中 oplog 是一个定容集合(capped collection),它的默认大小是磁盘空间的 5%,可以在启动 mongod 实例时通过 --oplogSizeMB 参数修改,也可在对应 mongod 实例的配置文件中通过如下方式配置其大小:

replication:
 oplogSizeMB: 2048
 replSetName: my_repl

oplog 记录了是整个 mongod 实例在一段时间内数据库的所有变更(插入/更新/删除)操作。
当空间用完时新的记录自动覆盖最老的记录,其覆盖范围被称作 oplog 时间窗口。
需要注意的是,因为 oplog 是一个定容集合,所以时间窗口能覆盖的范围会因为单位时间内的更新次数不同而变化。

在使用 mongodump 备份的时候可以指定 --oplog 选项从而实现基于时间点的快照备份,同时会通过 oplog 去备份在备份过程中产生的修改操作,以 oplog.bson 的形式保存下来。

oplog 的数据保存在 local 库的 db.oplog.rs 中。

下面就来看看 oplog 具体的使用了,请先参考【复制集章节】准备一个复制集环境。


我们先来查看一下 oplog 的内容,为效果明显我们先登入复制集主实例录入一下数据用来生成 oplog:

$ mongo --port 28017 admin
MongoDB shell version v3.6.12
connecting to: mongodb://127.0.0.1:28017/admin?gssapiServiceName=mongodb
...
# 先录入一下数据用来生成 opLog
my_repl:PRIMARY> use test
my_repl:PRIMARY> db.test_collection.insert({id:1,name:"zze",gender:"M",age:"23"})

查看 oplog:

# 切换到 local 库
my_repl:PRIMARY> use local 
# 查看日志,可以找到上面录入数据的日志
my_repl:PRIMARY> db.oplog.rs.find().pretty()
...
{
        "ts" : Timestamp(1592575469, 3),
        "t" : NumberLong(1),
        "h" : NumberLong("2543376525806452639"),
        "v" : 2,
        "op" : "i",
        "ns" : "test.test_collection",
        "ui" : UUID("2d160eb7-d454-4e4d-8a3f-8f143c0fe342"),
        "wall" : ISODate("2020-06-19T14:04:29.480Z"),
        "o" : {
                "_id" : ObjectId("5eecc5edb96311ffece1fe28"),
                "id" : 1,
                "name" : "zze",
                "gender" : "M",
                "age" : "23"
        }
}
...
# 在 oplog 记录的文档属性中我们常关注的有如下几个:
#     ts:执行该操作的时间戳,Timestamp(1592575469, 3) 表示在 1592575469 这个时间戳表示的时间点一秒内执行的第 3 个操作;
#     op:操作类型,i 为插入操作,u 为更新操作,d 为删除操作,c 为库级别的操作,n 为提示信息;
#     ns:操作对象;
#     o:数据内容;

还可查看 oplog 当前的详细信息:

my_repl:PRIMARY> rs.printReplicationInfo()
configured oplog size:   2048MB
log length start to end: 82505secs (22.92hrs)
oplog first event time:  Fri Jun 19 2020 21:44:05 GMT+0800 (CST)
oplog last event time:   Fri Jun 19 2020 22:25:38 GMT+0800 (CST)
now:                     Fri Jun 19 2020 22:25:41 GMT+0800 (CST)

在上面的输出信息中我们主要关注的是下面两个属性:

  • configured oplog size:当前配置的 oplog 最大大小
  • log length start to end:按当前写入状态预估的下一次覆盖写入的时间,即 oplog 容量用尽的时间;

实际的备份策略需要根据这两个值合理的把控,以上述状态为例,即预估的下次覆盖写入的时间是 22.92 小时后,所以为保证数据不会丢失,我们应该在 22.92 小时后这个时间点到来之前执行一次备份操作,当然,也可调大 oplog 的容量以延长下一次覆盖写入的时间,具体还是自行斟酌啦~~


下面再演示一下使用基于 oplog 的热备和恢复操作。

模拟数据插入:

my_repl:PRIMARY> use test
my_repl:PRIMARY> for(var i = 1 ;i < 100; i++) {
    db.foo.insert({a:i});
}

oplog 配合 mongodump 实现热备:

$ mongodump --port 28017 --oplog -o /mongodb/backup

使用 mongodump 从基于 oplog 的热备文件恢复:

$ mongorestore --port 28017 --oplogReplay --drop /mongodb/backup

案例

背景:每天 0 点全备,oplog 恢复窗口为 48 小时。
某天,上午 10 点 my.t1 业务集合被误删除。
恢复思路:

  1. 停应用;
  2. 找测试库;
  3. 恢复昨天晚上全备;
  4. 截取全备之后到 my.t1 误删除时间点的 oplog,并恢复到测试库;
  5. 将误删除表导出,恢复到生产库;

1、准备 my.t1 集合的初始数据:

$ mongo --port 28017
my_repl:PRIMARY> use my
switched to db my
my_repl:PRIMARY> db.t1.insert({id:1,name:'zze',gender:'M'})
WriteResult({ "nInserted" : 1 })
my_repl:PRIMARY> db.t1.find()
{ "_id" : ObjectId("5eed819ecd43ea37ec79f3c3"), "id" : 1, "name" : "zze", "gender" : "M" }

2、执行全备操作:

$ mongodump --port 28017 --oplog -o /mongodb/all_backup
2020-06-20T11:27:27.960+0800    writing admin.system.version to 
2020-06-20T11:27:27.962+0800    done dumping admin.system.version (1 document)
2020-06-20T11:27:27.963+0800    writing my.t1 to 
2020-06-20T11:27:27.965+0800    done dumping my.t1 (1 document)
2020-06-20T11:27:27.966+0800    writing captured oplog to 
2020-06-20T11:27:27.967+0800            dumped 1 oplog entry

3、模拟全备后操作及误删除操作:

$ mongo --port 28017
my_repl:PRIMARY> use my
switched to db my
# 全备后新增数据了
my_repl:PRIMARY> db.t1.insert({id:2,name:'lcq',gender:'F'})
WriteResult({ "nInserted" : 1 })
my_repl:PRIMARY> db.t1.find()
{ "_id" : ObjectId("5eed819ecd43ea37ec79f3c3"), "id" : 1, "name" : "zze", "gender" : "M" }
{ "_id" : ObjectId("5eed828e619baf39f1f624e7"), "id" : 2, "name" : "lcq", "gender" : "F" }
# 执行了误删除操作
my_repl:PRIMARY> db.t1.drop()
true

4、备份现有的 oplog:

$ mongodump --port 28017 -d local -c oplog.rs -o /mongodb/all_backup
2020-06-20T11:31:44.325+0800    writing local.oplog.rs to 
2020-06-20T11:31:44.329+0800    done dumping local.oplog.rs (986 documents)

$ ls /mongodb/all_backup/local/
oplog.rs.bson  oplog.rs.metadata.json

5、登录到原库,找到误删除 my.t1 集合的 oplog 记录对应的时间戳:

$ mongo --port 28017
my_repl:PRIMARY> use local
my_repl:PRIMARY> db.oplog.rs.find({op:'c'}).pretty()
...
{
        "ts" : Timestamp(1592628079, 1),
        "t" : NumberLong(1),
        "h" : NumberLong("8871762747369564964"),
        "v" : 2,
        "op" : "c",
        "ns" : "my.$cmd",
        "ui" : UUID("537383dc-bd5a-4e27-a6d8-892ff050d26a"),
        "wall" : ISODate("2020-06-20T04:41:19.127Z"),
        "o" : {
                "drop" : "t1"
        }
}
...

可以看到误删除 my.t1 集合的 oplog 记录对应的时间戳为 Timestamp(1592628079, 1)

6、用后面备份的 oplog 文件替换全备的 oplog 文件:

$ mv /mongodb/all_backup/local/oplog.rs.bson /mongodb/all_backup/oplog.bson
$ rm -rf /mongodb/all_backup/local/

7、直接从全备恢复,跳过误删除的那个时间戳对应的记录:

$ mongorestore --port 28017 --oplogReplay --oplogLimit "1592628079:1"  --drop /mongodb/all_backup/
2020-06-20T11:45:10.304+0800    preparing collections to restore from
2020-06-20T11:45:10.305+0800    reading metadata for my.t1 from /mongodb/all_backup/my/t1.metadata.json
2020-06-20T11:45:10.311+0800    restoring my.t1 from /mongodb/all_backup/my/t1.bson
2020-06-20T11:45:10.335+0800    no indexes to restore
2020-06-20T11:45:10.335+0800    finished restoring my.t1 (1 document)
2020-06-20T11:45:10.335+0800    replaying oplog
2020-06-20T11:45:10.754+0800    done

8、登入复制集实例,检查 my 库下的 t1 集合数据是否已经恢复:

$ mongo --port 28017
...
my_repl:PRIMARY> use my
switched to db my
my_repl:PRIMARY> db.t1.find()
{ "_id" : ObjectId("5eed934638a6b25dc3bfd686"), "id" : 1, "name" : "zze", "gender" : "M" }
{ "_id" : ObjectId("5eed9364f8cf1b77f08db769"), "id" : 2, "name" : "lcq", "gender" : "F" }
0

评论区