嘿,朋友。如果你正在盯着屏幕上的 MongoDB 迁移任务感到头大,或者正担心今晚又要通宵处理数据同步问题,那这篇指南就是为你准备的。别被“迁移”这两个字吓到了,其实它就像搬家:你不能把家具直接扔进卡车里不管不顾,你得打包、得分类、得确保易碎品没事,还得在新家把东西摆对位置。
我们这次不聊那些枯燥的理论定义,而是直接切入实战。我会带你走过从离线全量备份到在线增量同步,再到无感切换的全过程。我们会解决那些让人头疼的兼容性问题(比如从 3.6 升到 7.0),还会分享如何验证数据完整性,确保你交差时老板挑不出毛病。
准备好了吗?让我们开始这场“数字搬家”之旅。
第一阶段:知己知彼——环境评估与兼容性排查
在动手之前,最重要的一步是停下来看一眼。很多迁移失败不是因为工具不好,而是因为没搞清楚源端和目标端的差异。
1. 版本跨度检查
MongoDB 的版本升级是有严格限制的。如果你打算从 v4.4 直接跳到 v7.0,中间可能还需要经过 v5.0 或 v6.0 的过渡,或者使用特定的 --upgrade 标志。
- 关键动作:查询官方文档的 Compatibility Matrix。
- 避坑指南:如果你的应用使用了旧版驱动(如
pymongo<3.0),即使数据库迁移成功,应用也会连不上。记得提前升级客户端驱动。
2. 存储引擎与配置差异
源库如果是 MMAPV1(老古董了),必须升级到 WiredTiger。目标库通常也是 WiredTiger,但要注意参数配置:
journalCommitInterval:影响写入性能。wiredTigerCacheSizeGB:内存限制,确保目标服务器内存足够。
3. 数据特征分析
运行以下命令,看看你的数据长什么样:
// 统计各集合大小
db.stats()
// 查看最大文档大小和索引数量
db.getCollectionNames().forEach(function(collName) {
let coll = db[collName];
let stats = coll.stats();
print("Collection: " + collName + ", Count: " + stats.count + ", Indexes: " + stats.nindexes);
});
如果发现某个集合有几千万条记录,且包含大量嵌套文档,那么传统的 mongodump 可能会卡死内存。这时候我们需要更智能的工具。
第二阶段:基础保障——全量数据备份与验证
在开始任何在线迁移之前,必须进行一次完整的全量备份。这不是为了迁移本身,而是为了兜底。万一迁移搞砸了,这是你最后的救命稻草。
1. 使用 mongodump 进行全量导出
虽然 mongodump 简单粗暴,但在大数据量下它很慢。我们可以加一些优化参数:
# 基础命令
mongodump --uri="mongodb://user:pass@source_host:27017/mydb" \
--out=/backup/full_backup_$(date +%Y%m%d) \
--gzip \
--numInsertionWorkersPerCollection=8
# 解释:
# --gzip: 压缩存储,节省磁盘空间
# --numInsertionWorkersPerCollection: 并行写入线程数,根据 CPU 核心数调整
2. 验证备份完整性
备份完了不等于成功了。你需要验证备份是否可用。
# 创建一个临时本地实例加载备份
mongorestore --tmpDirectory /tmp/mongorestore_tmp \
--drop \
/backup/full_backup_YYYYMMDD
# 然后对比文档计数
mongo source_db --eval "db.my_collection.countDocuments()"
mongo target_db --eval "db.my_collection.countDocuments()"
如果两个数字一致,恭喜,你的“底牌”是稳的。
第三阶段:核心实战——在线迁移策略选择
这是最关键的部分。根据你的业务容忍度(RPO 和 RTO),有三种主流方案:
方案 A:基于 mongodump + mongorestore 的断点续传(适合中小数据量)
对于数据量在 TB 级别以下,且允许短暂停机的场景,可以使用 mongorestore 的 --resume 功能。但这通常用于恢复备份,而非实时迁移。更推荐的是使用 MongoDB Compass 或第三方 GUI 工具进行可视化的全量迁移,适合非技术背景的操作员。
方案 B:基于 rsync 的文件级复制(最快,但需停机)
如果你的 MongoDB 数据文件(.wt 文件)很大,直接复制文件比逐条插入快得多。
- 停止源服务:
mongosh --eval "db.adminCommand({ shutdown: 1 })" - 同步文件:
rsync -avzP /var/lib/mongodb/data/ user@target_server:/var/lib/mongodb/data/ - 启动目标服务:确保权限正确(
chown -R mongod:mongod /var/lib/mongodb)。
缺点:需要停机窗口,且无法处理增量数据。
方案 C:基于 CDC(变更数据捕获)的实时同步(推荐,零停机)
这是企业级迁移的标准做法。我们使用 MongoDB Atlas Data Federation 或开源工具如 Canal(针对 MySQL)、Debezium(支持 MongoDB Connector)。这里我们以 Debezium MongoDB Connector 为例,因为它能捕捉所有 OPLOG 变化。
1. 架构设计
[Source MongoDB] --> (OPLOG) --> [Debezium Connector] --> [Kafka] --> [Target MongoDB]
2. 配置 Debezium Connector
创建一个 JSON 配置文件 mongo-source.json:
{
"name": "mongo-source-connector",
"config": {
"connector.class": "io.debezium.connector.mongodb.MongoDbConnector",
"tasks.max": "1",
"mongodb.hosts": "rs0/source_host:27017",
"mongodb.name": "cluster0",
"topic.prefix": "mongo",
"snapshot.mode": "initial",
"collection.include.list": "mydb.users,mydb.orders"
}
}
3. 启动 Sink 端写入
使用 Kafka Connect 的 MongoDB Sink Connector 将数据写入目标库:
{
"name": "mongo-sink-connector",
"config": {
"connector.class": "io.confluent.connect.mongodb.MongoDbSinkConnector",
"tasks.max": "1",
"connection.uri": "mongodb://user:pass@target_host:27017/",
"topics": "mongo.cluster0.mydb.users,mongo.cluster0.mydb.orders",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewDocumentState",
"key.ignore": "false"
}
}
优点:数据实时同步,延迟通常在毫秒级。
注意:需要确保源库开启了 oplog 并保留足够长的时间(默认 24 小时,建议调整为更大)。
第四阶段:解决兼容性与数据结构陷阱
迁移过程中,最容易出问题的不是数据丢了,而是数据坏了。
1. 数据类型转换陷阱
MongoDB 是动态类型的,但不同版本对某些类型的处理可能有细微差别。
- ObjectId vs String:确保你的应用程序在查询时使用正确的类型。如果在源库中
_id是字符串,而目标库期望 ObjectId,查询会失败。 - Date 类型:MongoDB 3.6+ 统一使用 BSON Date(64位整数毫秒)。旧版本可能是 JavaScript Date 对象。迁移工具通常会自动处理,但最好手动检查一下:
// 检查日期字段是否为标准 BSON Date
db.orders.find({}).limit(5).forEach(doc => {
printjson(doc.createTime);
print(typeof doc.createTime); // 应该是 object,instanceof Date
});
2. 索引重建优化
不要指望迁移工具能完美复制所有索引属性。特别是复合索引的顺序和唯一性约束。
在目标库上,手动创建索引通常比依赖工具更快、更可控:
// 在目标库执行
db.orders.createIndex(
{ "userId": 1, "createdAt": -1 },
{ unique: false, background: true } // background: true 允许在创建索引时继续读写
);
提示:如果数据量巨大,建议在低峰期创建索引,或者先导入数据再建索引,最后再建唯一索引。
3. 分片键(Shard Key)迁移
如果你在使用分片集群,迁移分片键是最棘手的。
- 规则:分片键一旦确定,不能修改。
- 策略:
- 目标集群必须预先规划好分片键。
- 如果源库没有分片,迁移到分片库时,需要先选择一个合适的分片键(如
_id或高频查询字段)。 - 使用
mongos连接目标集群,通过sh.enableSharding()和sh.shardCollection()初始化。
第五阶段:数据一致性校验——如何证明你没骗我?
迁移完成不代表结束。你必须提供一份“验收报告”。以下是三种校验方法,从粗到细。
1. 文档计数比对(快速但不够精确)
# 源库
mongosh source_db --eval "print(db.orders.countDocuments())"
# 目标库
mongosh target_db --eval "print(db.orders.countDocuments())"
2. MD5/SHA 哈希校验(高效且可靠)
对每个集合计算哈希值,比较源和目标。
// 在 mongosh 中运行
db.orders.aggregate([
{ $group: { _id: null, hash: { $sum: 1 } } }, // 这只是示例,实际需用自定义函数
// 更准确的做法是使用 $out 到一个临时集合,然后计算二进制数据的哈希
]);
实际上,更常用的方法是使用 MongoDB Compass 的“比较数据库”功能,或者编写一个简单的 Python 脚本:
import hashlib
from pymongo import MongoClient
def get_collection_hash(client, db_name, coll_name):
db = client[db_name]
coll = db[coll_name]
cursor = coll.find({}, {"_id": 0}).sort("_id", 1) # 排序以确保顺序一致
hasher = hashlib.sha256()
for doc in cursor:
# 将文档转为字符串并编码
doc_str = str(sorted(doc.items(), key=lambda x: x[0])).encode('utf-8')
hasher.update(doc_str)
return hasher.hexdigest()
# 对比源和目标
source_hash = get_collection_hash(source_client, "mydb", "users")
target_hash = get_collection_hash(target_client, "mydb", "users")
if source_hash == target_hash:
print("✅ 数据完全一致!")
else:
print("❌ 数据存在差异,请检查!")
3. 抽样查询验证(人工最终确认)
随机抽取 100 条记录,人工比对关键字段(如金额、状态、时间戳)。
// 源库
let docs = db.orders.aggregate([{ $sample: { size: 10 } }]).toArray();
printjson(docs);
// 目标库同样查询,肉眼比对
第六阶段:平滑切换——从“读旧写新”到“全量新库”
数据同步好了,现在要让业务流量切过去。这里有两种策略:
策略一:双写模式(Double Write)
- 代码改造:在应用层,每次写入操作同时写入源库和目标库。
- 如果源库写入成功,目标库失败 -> 重试目标库,记录错误日志。
- 如果目标库写入成功,源库失败 -> 忽略(因为数据已在目标库)。
- 优势:实现真正的零停机切换。
- 劣势:代码侵入性强,需要复杂的异常处理逻辑。
策略二:DNS/代理层切换(推荐大多数场景)
- 准备:目标库已经同步好最新数据。
- 灰度测试:修改应用配置,让 1% 的流量指向新库,观察错误率。
- 全量切换:
- 修改 DNS 解析,将
db.example.com指向新库 IP。 - 或者,如果使用连接池,重启应用服务以获取新连接。
- 修改 DNS 解析,将
- 监控:密切监控新库的 QPS、延迟和错误日志。
# application.yml 示例
spring:
data:
mongodb:
uri: mongodb://new-cluster-host:27017/mydb
# 切换后,旧连接断开,新连接建立
回滚计划
如果切换后发现严重 Bug,必须能在 5 分钟内回滚。
- 方法:保留源库只读状态,不删除。将 DNS 指回源库 IP。
- 注意:切换期间产生的新数据会丢失。如果这不可接受,必须在切换前暂停写入,或者使用双写方案。
第七阶段:事后清理与优化
切换成功后,别急着庆祝。还有几件小事要做:
- 删除旧索引:检查新库是否有冗余索引,使用
db.collection.getIndexes()查看。 - 收集统计信息:运行
db.collection.reIndex()或db.collection.stats()更新优化器统计信息,帮助查询规划器做出更好决策。 - 归档备份:将源库的数据备份移动到冷存储(如 AWS S3 Glacier),保留至少 30 天以防万一。
- 文档更新:更新内部的运维文档,记录新的集群地址、端口和配置参数。
结语:迁移是一场艺术,不只是技术
MongoDB 迁移看起来只是数据的搬运,但实际上它是对系统设计、数据理解和应急能力的综合考验。
- 不要相信“默认设置”:总是显式地指定超时、重试和并行度。
- 永远有 Plan B:备份是底线,回滚方案是保险。
- 沟通大于技术:提前通知开发团队、产品经理和业务方,管理好他们的预期。
希望这份指南能帮你顺利完成下一次迁移。记住,每一次成功的迁移,都是你对系统掌控力的一次提升。如果你在过程中遇到具体的报错,欢迎随时回来查阅,或者深入探讨某个特定环节的细节。
祝你的数据迁移,丝般顺滑。
