嘿,朋友,我是Agnes。看到标题里“实战”和“坑点”这两个词,我就知道你想听真话了。数据库迁移这事儿,听起来像是IT部门的背景工作,但一旦搞砸,那就是业务停摆、数据丢失的噩梦。今天咱们不整那些虚头巴脑的教科书定义,我就把你当成坐在我对面的运维工程师或者开发组长,咱们一边喝茶,一边把这事儿掰开揉碎了讲清楚。
我知道你现在的处境:老板下了死命令,“下周把生产库从阿里云迁到自建集群”或者“把老的副本集升级”。你心里慌得一比,因为你知道,迁移不是拷贝文件那么简单。尤其是MongoDB这种讲究Schema Free、文档嵌套、还有那个让人又爱又恨的ObjectId的系统,坑真的多。
咱们分几个阶段聊:先说说有哪些路可以走(方案对比),再聊聊怎么挑工具,最后重点说说那些让你半夜惊醒的坑。
一、 先搞清楚:你的数据到底长什么样?
在推荐工具之前,我得先让你反思一下你自家的数据。不同的数据结构,迁移策略天差地别。
1. 文档嵌套深度
你的文档是简单的 {"name": "张三", "age": 18} 这种扁平结构,还是像俄罗斯套娃一样,嵌套了五层子文档和数组?
- 扁平结构:随便移,工具随便挑。
- 深层嵌套:小心JSON序列化溢出,或者字段丢失。有些工具在解析深层嵌套时性能会掉得厉害。
2. 数据类型复杂性
MongoDB支持不少特殊类型:ObjectId、Timestamp、Binary、Code、RegExp,还有Decimal128。
- 如果你有大量Binary(比如存图片、文件)或者自定义的JavaScript代码(虽然官方不推荐了,但老库肯定有),普通的文本导出工具会直接报错或者乱码。
3. 索引和约束 迁移不仅仅是数据,还有索引。如果你的源库有几十个复合索引、文本索引、2dsphere索引,迁移完目标库索引数量对不上,查询性能会崩盘。
4. 业务停机容忍度 这是最关键的决策因素。
- 完美迁移:目标库就绪后,切断源库写入,同步残留数据,切换。
- 零停机迁移:需要双写或者实时同步,业务方几乎无感知。
二、 主流方案大乱斗:各有优劣,没有银弹
针对MongoDB,目前市面上主流的方案大致可以分为三类:官方/原生工具类、第三方专业工具类、自建脚本类。
方案一:官方原声武器 —— mongodump + mongorestore
这是最基础、最无脑,但也最容易出问题的方案。
原理:
- 使用
mongodump将源库的数据导出为BSON格式文件(按集合分文件)。 - 将这些文件传输到目标机器。
- 使用
mongorestore将文件导入目标库。
代码示例:
# 1. 全量导出(假设库名是 mydb)
mongodump --host source-host --port 27017 --db mydb --out /backup/migration
# 2. 传输备份文件到目标机
scp -r /backup/migration user@target-host:/backup/
# 3. 全量导入
mongorestore --host target-host --port 27017 --db mydb /backup/migration/mydb
优点:
- 自带:不用安装任何第三方软件,MongoDB官方维护,兼容性最好。
- 稳定:BSON格式是二进制标准,不会出现编码问题。
- 可中断:支持
--oplog参数,在dump过程中记录操作日志,为后续增量同步做准备。
缺点:
- 单线程瓶颈:默认
mongorestore的并发数很低(--numParallelCollections=1时),全量导入大型集合(比如几十GB的日志表)速度极慢。 - 缺乏增量:如果你中途挂了,或者你想做增量同步,纯 dump/restore 搞不定,必须配合
--oplog。 - 无进度条:对于几百GB的数据,你不知道它跑到哪了,只能干等。
方案二:高性能并发导入 —— mongorestore 的多核优化
如果你觉得原生工具太慢,别急,mongorestore 其实有并行选项,只是很多人没注意。
优化代码:
# 指定并行集合数为8,线程数为8
mongorestore --host target-host --db mydb \
--numParallelCollections=8 \
--numInsertionWorkersPerCollection=8 \
/backup/migration/mydb
解析:
--numParallelCollections:同时恢复几个集合。如果你的硬盘是SSD,这个值可以设大点(如16)。--numInsertionWorkersPerCollection:每个集合内部用几个线程插入。内存大的话可以设到4-8。
坑点提醒:
- 内存爆炸:这两个参数开太大,目标库的mongod进程会吃掉大量内存,导致OOM(Out of Memory)。记得先观察目标机的内存使用情况。
- 锁竞争:虽然MongoDB是文档级锁,但同一集合的并发写入还是会有一定竞争,提升幅度不是线性的。
方案三:第三方神器 —— MongoShake 和 Atlas Data Federation
这是目前业界迁移大型MongoDB集群最主流的开源方案,尤其是阿里的 MongoShake(现已被广泛适配到各类环境)。
原理: MongoShake 是一个基于 Change Data Capture (CDC) 的数据库同步工具。它不读数据文件,而是直接读取 MongoDB 的 Oplog(操作日志)。
- 全量阶段:扫描源库集合,分批写入目标库。
- 增量阶段:订阅Oplog,实时捕获源库的变化(Insert, Update, Remove),并在目标库回放。
核心优势:
- 真正的增量同步:在业务低峰期完成全量,在业务高峰期进行增量追赶,最后停机时,增量差距极小(可能只有几秒的数据)。
- 断点续传:如果同步中断,重启后会从最新的Oplog位置继续,不会重复导入。
- 过滤能力强:支持按集合、按正则表达式、按字段过滤要同步的数据。
配置示例(config.json):
{
"id": "mongo-shake-migration",
"address": "source-mongo-host:27017",
"auth_db": "admin",
"auth_user": "admin",
"auth_password": "password",
"target_address": "target-mongo-host:27017",
"auth_db": "admin",
"auth_user": "admin",
"auth_password": "password",
"filter": [
{"db": "mydb", "coll": "logs", "exclude": true}
],
"parallel": {
"collections": 10,
"workers": 50
}
}
启动命令:
./mongo.shake --conf=config.json
MongoShake 的坑:
- Oplog 依赖:源库必须开启 Oplog(默认开启),且Oplog保留时间要足够长。如果你全量同步花了3天,但Oplog只保留24小时,那中间产生的数据就丢了,同步会失败。
- 大事务支持:早期版本对大事务支持不好,如果遇到一个Update语句改了100万行,可能会阻塞。记得升级到最新版本。
- 字段类型转换:虽然MongoDB是Schema Free,但不同版本之间,
Timestamp、Date的精度可能略有差异,MongoShake需要正确处理这些转换。
方案四:云厂商工具 —— AWS DMS / 阿里云 DTS
如果你的源和目标其中一方是云数据库(比如RDS for MongoDB, Atlas, 阿里云PolarDB MongoDB版),直接用云厂商的工具是最省心的。
阿里云 DTS (Data Transmission Service):
- 界面化操作:点点鼠标就能配置,支持全量+增量。
- 监控完善:有详细的延迟监控、流量监控。
- 自动修复:遇到常见的网络抖动,有一定的自愈能力。
缺点:
- 收费:按量付费,数据量大时费用不低。
- 黑盒:出了错,你没法像MongoShake那样去改源码调试,只能找客服。
- 版本限制:源库和目标库的版本必须在DTS支持的范围内。
方案五:自建 Python/Go 脚本 —— 定制化最强
有些场景很特殊,比如源库是MongoDB 3.2,目标库是MongoDB 5.0,且你有自定义的字段清洗逻辑(比如把手机号脱敏)。这时候,MongoShake可能搞不定,你得自己写。
Python 示例(使用 pymongo + pymongo.cursor):
import pymongo
from bson import ObjectId
import json
# 连接源库和目标库
source_client = pymongo.MongoClient("mongodb://source_host:27017")
target_client = pymongo.MongoClient("mongodb://target_host:27017")
source_db = source_client["mydb"]
target_db = target_client["mydb"]
# 获取所有集合
collections = source_db.list_collection_names()
for coll_name in collections:
source_coll = source_db[coll_name]
target_coll = target_db[coll_name]
# 复制集合(含索引和选项,可选)
# 注意:这种简单复制不包含索引,需要额外处理
# 分批读取,避免内存溢出
batch_size = 1000
cursor = source_coll.find({}).batch_size(batch_size)
docs = []
for doc in cursor:
docs.append(doc)
if len(docs) >= batch_size:
# 使用 insert_many 提高性能
target_coll.insert_many(docs, ordered=False)
docs = []
print(f"Inserted {len(docs)} batches...")
# 处理剩余数据
if docs:
target_coll.insert_many(docs, ordered=False)
优点:完全可控,可以做任何数据清洗。 缺点:开发成本高,稳定性差,没有断点续传,没有增量同步能力(除非你额外实现复杂的逻辑)。一般不推荐用于生产级大规模迁移,除非有特殊清洗需求。
三、 几种方案的横向对比
为了让你更直观地选择,我整理了这个表格:
| 特性 | mongodump/mongorestore | MongoDBShake (CDC) | 云厂商 DTS/DMS | 自建脚本 |
|---|---|---|---|---|
| 实施难度 | 低 | 中 | 低 | 高 |
| 全量速度 | 慢(默认单线程) | 快(多线程并行) | 中 | 取决于代码质量 |
| 增量同步 | 需配合oplog,较复杂 | 原生支持,最稳定 | 原生支持 | 需自己写 |
| 断点续传 | 支持(文件级) | 支持(Oplog位点) | 支持 | 需自己实现 |
| 数据过滤 | 支持(导出时过滤) | 强大(支持表达式) | 支持 | 灵活 |
| 监控能力 | 弱 | 中(有API和日志) | 强(Web控制台) | 无 |
| 成本 | 免费 | 免费(开源) | 收费 | 人力成本 |
| 适用场景 | 小数据量、简单迁移 | 大数据量、在线迁移 | 云上迁移、怕麻烦 | 特殊清洗、异构迁移 |
四、 那些让你半夜惊醒的“坑”与解法
好了,方案选好了,接下来是重头戏。我在迁移现场踩过无数坑,总结出来,希望能帮你少熬几个通宵。
坑点1:Oplog 空间不足 —— 迁移未完成的头号杀手
现象:
MongoShake 全量同步跑了3天,第4天准备切换增量时,报错 oplog not found 或者同步断裂。
原因:
mongorestore 或 MongoShake 的全量阶段耗时太长,导致源库产生的新数据覆盖了旧Oplog。MongoDB 的 Oplog 是有大小限制的(默认占总磁盘空间的一定比例,或者固定大小)。
解法:
- 扩容Oplog:在迁移前,调整源库的
oplogSizeMB。
注意:这需要重启 mongod 或者切换主库,会影响业务,务必在低峰期操作。// 在源库上执行,假设要扩展到 100GB use local db.oplog.rs.drop() db.runCommand({ replSetResizeOplog: 1, size: 102400 // MB }) - 监控Oplog窗口:在迁移期间,实时监控
rs.printSecondaryReplicationInfo(),确保 Oplog 还能覆盖全量同步的时长。
坑点2:ObjectId 陷阱 —— 数据对不上
现象:
源库里有一些文档的 _id 字段,你以为是ObjectId,但实际上是字符串,或者是其他自定义ID。迁移后,应用层查询报错,或者数据重复。
原因:
MongoDB 的 _id 可以是任意类型。如果源库历史遗留问题多,_id 类型混乱,直接迁移可能导致目标库的索引冲突或应用层解析错误。
解法:
- 迁移前扫描:写个脚本统计源库各集合
_id的类型分布。db.myCollection.distinct("_id.$type") - 标准化:如果发现混合类型,考虑在迁移前统一格式,或者在迁移脚本中做映射。
- 注意大小写:MongoDB 的 ObjectId 是24位十六进制字符串,确保迁移过程中没有大小写转换问题(虽然 BSON 是二进制,通常没问题,但如果是 JSON 导出导入就有风险)。
坑点3:字段类型漂移 —— 数字变字符串
现象:
源库里有些字段存的是数字 123,有些存的是字符串 "123"。迁移工具通常会忠实地保留原样,但目标库如果做了严格的 Schema 校验(如 MongoDB 4.2+ 的 Validation Rule),可能会报错。
原因: MongoDB 本身是动态模式,但很多应用层代码依赖类型。迁移后,如果目标库开启了 Validation,或者应用层做了类型检查,就会炸。
解法:
- 迁移前测试:在测试环境先跑一遍,检查目标库的 Validation 规则。
- 数据清洗:使用 MongoShake 的
transform功能,或者在自建脚本中做类型转换。// MongoShake 配置示例:将特定字段转为字符串 "transform": [ {"db": "mydb", "coll": "orders", "field": "amount", "type": "string"} ]
坑点4:嵌套数组性能黑洞
现象: 全量同步时,某个集合特别慢,CPU 和内存飙升,但吞吐量很低。
原因:
这个集合里有很多文档包含巨大的嵌套数组(比如一个订单包含上千个商品明细)。mongodump 导出单个大文档时,内存占用会很高。mongorestore 导入时,也需要大量内存解析。
解法:
- 拆分大文档:如果可能,在迁移前对源库进行分片或重构,将大数组拆分成子集合。
- 调整工具参数:
mongodump:使用--batchSize参数,限制单次读取的数据量。- MongoShake:调整
parallel.workers和buffer_size。
- 增加目标库内存:确保目标库的
wiredTigerCacheSizeGB足够大。
坑点5:索引重建耗时
现象: 数据导完了,以为结束了。结果一查查询,慢得像蜗牛。
原因: 很多人只迁移数据,忘了迁移索引,或者索引创建顺序不对。在空表上创建索引非常快,但在已有数据的表上创建索引,尤其是复合索引,会消耗大量 I/O 和 CPU。
解法:
先迁移数据,再建索引:对于
mongodump/mongorestore,默认会先导入数据,再重建索引。这通常是最优策略。检查索引差异:迁移后,对比源库和目标库的索引。
# 在源库和目标库分别执行 db.myCollection.getIndexes()禁用索引重建:如果数据量巨大,可以先禁用索引,导入数据后,再逐个创建索引(使用
--noIndexRestore和后续的createIndex)。但这需要更多停机时间。
坑点6:时间戳和时区问题
现象: 数据迁移后,时间显示不对,晚了8小时或者快了1小时。
原因:
MongoDB 的 Date 类型在 BSON 中是 UTC 时间戳。但 mongodump 导出为 JSON 时,会转换成字符串,可能丢失时区信息,或者被目标库以本地时间解析。
解法:
- **
