从阿里迁移到MongoDB Atlas踩过哪些坑 详解数据迁移工具怎么选 mongodump备份恢复完整方案 附5个常见问题解决方案
说真的,做数据库迁移这事儿,听着挺高大上,真干起来简直是一场修行。我最近帮一个项目组从阿里云的MongoDB集群迁移到MongoDB Atlas,整整折腾了一周,头发都掉了不少。今天把这过程中的坑、工具选择、备份恢复方案,还有几个让人头疼的问题,全部摊开来讲。
先说说我们为什么要迁移
事情是这样的,我们原先用的是阿里云的MongoDB托管服务,跑着大概120GB的数据量,业务主要是一些日志存储和关系型数据的补充查询。后来业务规模上来了,阿里云那边的弹性扩展总觉得差点意思,而且跨云灾备的需求也提上了日程。朋友推荐MongoDB Atlas,说是云原生的MongoDB,体验确实不错,就是——迁移成本不低。
选择迁移路径的时候,我们仔细对比了几个工具,下面我会详细讲每个工具的优缺点,你们自己掂量。
数据迁移工具怎么选,这事儿大有讲究
先说结论:没有最好的工具,只有最适合你场景的工具。我遇到的主流方案大概有这几种:
方案一:mongodump + mongorestore(最经典)
这是MongoDB官方自带的备份恢复工具,几乎所有人入门都会先想到这个。
# 第一步:从阿里云MongoDB导出数据
mongodump \
--host rs0/aliyun-mongodb.example.com:27017 \
--username admin \
--password 'your_password' \
--authenticationDatabase admin \
--out /data/backup/aliyun_dump
# 第二步:导入到MongoDB Atlas
mongorestore \
--host "cluster0.abc12.mongodb.net" \
--username myuser \
--password 'atlas_password' \
--authenticationDatabase admin \
--gzip \
/data/backup/aliyun_dump
优点:官方工具,稳定可靠,支持压缩,有详细的日志输出,遇到问题也容易排查。
缺点:对于大数据量(比如我们这种120GB级别),单线程备份恢复速度确实让人抓狂。我们实测下来,mongodump大约8MB/s,mongorestore大约6MB/s,换算一下,恢复完整数据要将近6个小时。更别提中间如果断网或者出错,还得从头再来。
方案二:MongoDB Atlas Data Federation + Atlas Import/Export Service
Atlas自带的云服务,可以通过控制台操作,也可以调用API。适合不想折腾命令行、喜欢可视化操作的同学。
我们当时试过Atlas的Import工具,把备份文件上传到S3,然后让Atlas从S3拉取。这个方案的亮点是不用维护中间跳板机,但缺点也很明显——上传120GB到S3本身就要很久,而且Atlas Import有单次任务大小限制,超过100GB的任务会被自动拆分。
# 通过Atlas CLI做导入操作(简化版)
mongoratlas import create \
--clusterName cluster0 \
--source aws:s3://my-bucket/backup-data/ \
--source.auth aws:iam \
--username myuser \
--password 'atlas_password' \
--compression gzip \
--output /tmp/import-job.log
方案三:Kangaroo迁移工具(我们最终选的)
Kangaroo是MongoDB官方开源的迁移工具,专门针对大规模数据迁移设计的。它支持并行读写、断点续传、增量同步,对网络不稳定的场景特别友好。
# 安装Kangaroo
pip3 install kangaroo
# 启动迁移任务
kangaroo \
--source-uri "mongodb://user:pass@aliyun-mongodb.example.com:27017/admin?replicaSet=rs0" \
--target-uri "mongodb://user:pass@cluster0.abc12.mongodb.net:27017/admin?authSource=admin" \
--workdir /data/kangaroo_work \
--parallelism 8 \
--batch-size 10000 \
--log-level INFO
# 启动后,Kangaroo会自动分片读取和写入,还支持断点续传
我们用了Kangaroo之后,实际吞吐量到了大概45MB/s,比mongodump/mongorestore快了7倍左右。最爽的是,中间有两次网络抖动,Kangaroo自动断点续传,没让我们重头再来。
工具选择总结表
| 工具 | 适用场景 | 速度 | 复杂度 | 推荐度 |
|---|---|---|---|---|
| mongodump/mongorestore | 小数据量(<10GB) | 慢 | 低 | ⭐⭐ |
| Atlas Import Service | 中等数据量,不愿维护基础设施 | 中等 | 低 | ⭐⭐⭐ |
| Kangaroo | 大数据量(>50GB),需要断点续传 | 快 | 中等 | ⭐⭐⭐⭐⭐ |
| 自研脚本 | 特殊需求 | 可控 | 高 | ⭐⭐ |
mongodump备份恢复完整方案
虽然最后我们选了Kangaroo,但mongodump还是得学,毕竟它是基础,很多场景下还是用得上。
全量备份方案
# 完整备份命令,包含所有数据库
mongodump \
--host rs0/aliyun-mongodb.example.com:27017 \
--username admin \
--password 'your_password' \
--authenticationDatabase admin \
--gzip \
--archive=/backup/full_archive_$(date +%Y%m%d).gz \
--oplog
# --oplog 参数很重要,它会在备份期间记录操作的oplog,
# 保证备份时刻的一致性,特别是对于正在写入的数据
单库备份(更灵活的方案)
# 只备份特定数据库
mongodump \
--host rs0/aliyun-mongodb.example.com:27017 \
--username admin \
--password 'your_password' \
--db my_production_db \
--gzip \
--out /backup/single_db/
# 只备份特定集合
mongodump \
--host rs0/aliyun-mongodb.example.com:27017 \
--username admin \
--password 'your_password' \
--db my_production_db \
--collection user_orders \
--gzip \
--out /backup/single_db/
恢复方案
# 从archive恢复(推荐,速度比目录结构快)
mongorestore \
--host "cluster0.abc12.mongodb.net" \
--username myuser \
--password 'atlas_password' \
--authenticationDatabase admin \
--gzip \
--archive=/backup/full_archive_20240101.gz \
--drop # 先删除现有数据再恢复,适合全量迁移
# 如果是增量恢复,配合--oplogReplay
mongorestore \
--host "cluster0.abc12.mongodb.net" \
--username myuser \
--password 'atlas_password' \
--authenticationDatabase admin \
--gzip \
--oplogReplay \
--archive=/backup/full_archive_20240101.gz
迁移前的准备清单
这个清单我们团队整理了好几版,最终版本如下:
- 源数据库信息收集:版本号、副本集架构、最大连接数、当前写入压力
- 目标环境确认:Atlas集群规格、网络连通性、白名单配置
- 数据量评估:
db.stats()和db.collection.stats()都要跑一遍 - 索引脚本准备:备份恢复后索引不会自动重建,需要单独处理
- Downtime窗口确认:和业务方对齐停机时间
// 评估数据量的脚本,在源库上执行
db.adminCommand({ serverStatus: 1 }).repl.members.forEach(function(m) {
printjson(m);
});
// 查看各个集合的大小和文档数
db.getCollectionNames().forEach(function(col) {
var stats = db.getCollection(col).stats();
print(col + ": docs=" + stats.count + " size=" + stats.size);
});
踩过的坑,一个一个来说
坑一:SSL连接问题
阿里云MongoDB默认开启了SSL,而Atlas集群也是强制SSL的。我们第一次迁移的时候,mongodump直接连不上,报SSL错误。
# 正确的连接方式,指定SSL参数
mongodump \
--host "ssl://rs0-0.aliyun.example.com:27017" \
--username admin \
--password 'your_password' \
--ssl \
--sslCAFile /path/to/ca-cert.pem \
--out /data/backup/
# 导入到Atlas时
mongorestore \
--host "cluster0.abc12.mongodb.net" \
--username myuser \
--password 'atlas_password' \
--ssl \
--sslAllowInvalidCertificates # 测试环境可用,生产环境建议验证证书
教训:连上去之前,先把SSL证书下载好,阿里云的ca-cert.pem在控制台能下。
坑二:oplog窗口不够大
阿里云MongoDB的oplog窗口我们测出来只有6小时。而我们全量备份花了8小时。这意味着什么?备份过程中产生的新数据,oplog里可能已经轮转掉了。
// 检查oplog大小和窗口
db.adminCommand({ replSetGetStatus: 1 }).optimeDate
db.oplog.rs.stats().maxSize
db.oplog.rs.find().sort({ts:-1}).limit(1).forEach(function(doc) {
print("最新的oplog时间: " + doc.ts.getHighLow());
})
解决方案是用binlog方式或者用Kangaroo这类支持增量同步的工具,它可以在主从切换的时候自动处理这类问题。
坑三:数据类型差异
MongoDB Atlas默认使用较新的驱动,对某些数据类型的处理逻辑和老版本的阿里云MongoDB略有差异。特别是OID和String的混淆问题。
// 比如你有一个字段存的是 ObjectId,但被错误地存成了String
db.user_profiles.find({
_id: { $type: "string" } // 这会找到那些错误存成字符串的ObjectId字段
}).forEach(function(doc) {
print(doc._id + " is a string, should be ObjectId");
});
// 修复脚本
db.user_profiles.find({
_id: { $type: "string" }
}).forEach(function(doc) {
db.user_profiles.updateOne(
{ _id: doc._id },
{ $set: { _id: ObjectId(doc._id) } }
);
});
我们在迁移前跑了一个类型扫描脚本,发现了大概2000条数据有问题,提前修复了。
坑四:权限模型差异
阿里云MongoDB的认证数据库和Atlas不同。阿里云通常用 admin 数据库做认证,但Atlas的IAM角色模型更严格。我们的业务账号在源库有 dbAdminAnyDatabase 角色,但导入Atlas时发现没有写入权限。
// Atlas上重新配置权限
db.grantRolesToUser("migrate_user", [
{ role: "readWriteAnyDatabase", db: "admin" },
{ role: "dbAdminAnyDatabase", db: "admin" }
])
// 验证权限
db.getUser("migrate_user").roles
坑五:时区问题
这个坑比较隐蔽。源数据库的时区设置是Asia/Shanghai,但Atlas集群默认用UTC。我们有一条日志记录,时间戳字段用的是UTC时间,但查询的时候用的是本地时间,导致迁移后查询结果偏差了8小时。
// 查询时间戳时注意时区转换
db.logs.find({
createdAt: {
$gte: new Date("2024-01-01T00:00:00+08:00"),
$lte: new Date("2024-01-31T23:59:59+08:00")
}
})
// 或者统一用UTC时间存储和查询,避免混淆
db.logs.find({
createdAt: {
$gte: ISODate("2024-01-01T00:00:00Z"),
$lte: ISODate("2024-01-31T23:59:59Z")
}
})
5个常见问题的解决方案
问题一:迁移过程中网络中断怎么办
这个问题几乎 every 项目都会遇到。我们那次迁移,第三天晚上阿里云机房闪断了一下,Kangaroo跑了半小时又断了。
解决方案:使用支持断点续传的工具。Kangaroo、Atlas Import Service 都支持。如果用mongodump,可以考虑分库分表备份,这样某一张表失败了只需要重跑那张表。
# 分集合备份策略,用脚本自动化
mongo --eval "var colls = db.getCollectionNames()" \
--host aliyun-mongodb.example.com | while read col; do
mongodump \
--host rs0/aliyun-mongodb.example.com:27017 \
--username admin \
--password 'your_password' \
--db my_db \
--collection "$col" \
--gzip \
--out "/backup/${col}/" \
&& echo "Successfully backed up $col" || echo "Failed to backup $col, retrying..."
done
问题二:迁移后数据不一致怎么校验
数据量大了之后,肉眼比对不可能。我们写了一个校验脚本:
// 校验脚本:对比源库和目标库的文档数
function compareCount(sourceUri, targetUri, collection) {
// 连接源库
var sourceDb = new Mongo(sourceUri).getDB("my_db");
var targetDb = new Mongo(targetUri).getDB("my_db");
var sourceCount = sourceDb[collection].countDocuments();
var targetCount = targetDb[collection].countDocuments();
print("Collection: " + collection);
print("Source count: " + sourceCount);
print("Target count: " + targetCount);
print("Match: " + (sourceCount === targetCount));
print("---");
}
// 遍历所有集合
db.getCollectionNames().forEach(function(col) {
compareCount(
"mongodb://user:pass@aliyun-mongodb.example.com:27017",
"mongodb://user:pass@cluster0.abc12.mongodb.net",
col
);
});
对于更精确的校验,可以用hash比对:
// Hash校验,抽样比对
function hashCompare(collection) {
var sourceDb = new Mongo("mongodb://user:pass@aliyun-mongodb.example.com:27017").getDB("my_db");
var targetDb = new Mongo("mongodb://user:pass@cluster0.abc12.mongodb.net").getDB("my_db");
// 取前1000条文档做hash比对
var sourceDocs = sourceDb[collection].find({}, {hash: 1}).limit(1000).toArray();
var targetDocs = targetDb[collection].find({}, {hash: 1}).limit(1000).toArray();
print("Source hash sample: " + JSON.stringify(sourceDocs.slice(0,5)));
print("Target hash sample: " + JSON.stringify(targetDocs.slice(0,5)));
}
问题三: Atlas集群性能不如预期
迁移完成后,我们发现查询性能比预期慢了大概30%。原因有几个:
- 集群规格选小了:我们一开始选了M10级别,其实M20才能扛住原有的写入压力
- 索引策略不对:源库的某些复合索引在Atlas上没有正确创建
- 连接池配置问题:应用层的连接池大小没调整
// 检查Atlas上的索引使用情况
db.my_collection.getIndexes()
db.my_collection.getIndexes().forEach(function(idx) {
printjson(idx);
})
// 检查慢查询日志
db.adminCommand({ profile: 2, slowms: 100 })
// 创建缺失的复合索引
db.user_orders.createIndex(
{ userId: 1, createdAt: -1 },
{ background: true } // 后台创建,不影响写入
)
建议:迁移前先做好性能基线测试,迁移后对比监控数据,发现问题及时调优。
问题四:应用连接字符串变了,怎么无缝切换
这个是个运维问题。我们最终的做法是:
- 先在Atlas上创建好集群
- 配置DNS记录,让新域名指向Atlas集群
- 应用层通过域名连接,切换时只需要更新DNS
- 旧集群保留一周,随时可以回滚
# DNS记录配置示例
# @ IN CNAME cluster0.abc12.mongodb.net
# 这样应用代码里的连接字符串不用改,只需要切换DNS指向
问题五:迁移完才发现数据丢了,怎么处理
这个问题虽然可怕,但我们必须面对。我们的预案是:
- 保留源库只读运行至少两周:即使迁移完成,源库不要立刻删除
- 增量同步:用Kangaroo的增量模式,确保最终一致
- 校验通过后,再关闭源库:所有校验脚本跑完,业务方确认没问题了,才下线源库
# Kangaroo增量同步命令
kangaroo \
--source-uri "mongodb://user:pass@aliyun-mongodb.example.com:27017/admin" \
--target-uri "mongodb://user:pass@cluster0.abc12.mongodb.net/admin" \
--workdir /data/kangaroo_work \
--mode incremental \
--parallelism 4
最后想说的一些话
说实话,这次迁移让我们团队成长了不少。数据库迁移这件事,说难也难,说简单也简单——难在细节,简单在流程。只要把每一步都准备好,坑都能避开。
如果你也在考虑迁移到MongoDB Atlas,我的建议是:先小规模试点,再全量迁移。拿一个非核心的业务库先练手,把流程摸熟,然后再动核心数据。这样即使出问题,影响也有限。
希望这篇文章能帮到你。有任何问题欢迎评论区交流,我们一起学习进步。
