很多人提到 MongoDB 迁移,第一反应是 mongodump 和 mongorestore。这没错,但在生产环境,尤其是涉及 TB 级数据或高可用要求时,单纯靠这两个工具往往显得力不从心。今天我们不聊教科书式的定义,直接切入实战中真正让人头秃的三个问题:到底该用哪个工具?怎么确保数据没丢没错?万一跑一半断了怎么办?
一、 工具选型:没有银弹,只有场景匹配
选工具就像选鞋子,跑马拉松穿皮鞋肯定不行。MongoDB 的迁移场景通常分为三类:全量离线迁移、在线热迁移(不停机)、跨版本/跨集群平滑升级。
1. 轻量级/小数据量:官方原生工具 (mongodump / mongorestore)
- 适用场景:数据量在几十 GB 以内,或者测试环境、开发环境的数据同步。
- 优点:无需安装额外组件,配置简单,支持 BSON 格式,保留索引和约束信息。
- 缺点:单线程恢复慢,网络中断后很难断点续传(除非配合
--gzip和手动拆分 dump 文件),对源库有短暂锁表压力(取决于--oplog的使用)。
2. 企业级/大数据量/高性能:Percona MongoDB Tools (PMT)
- 适用场景:TB 级别数据迁移,对速度有极致要求。
- 核心优势:
- 多线程并发:
mongorestore的增强版,支持并行导入多个集合,速度比官方快 3-5 倍。 - 智能重试机制:网络抖动时自动重试特定文档,而不是整个任务失败。
- 资源控制:可以限制 CPU 和网络带宽占用,避免拖垮生产库。
- 多线程并发:
3. 在线零停机迁移:MongoDB Replication (副本集) + Application Switch
- 适用场景:核心业务系统,要求 99.99% 可用性,绝对不能停服。
- 原理:这不是一个“工具”,而是一种架构方案。通过建立新集群作为旧集群的 Replica Set Member,数据实时同步,最后切换应用连接字符串。
- 优点:真正的零停机,数据一致性由 MongoDB 内部协议保证。
- 缺点:架构复杂,需要维护两套集群直到切换完成。
4. 结构化/异构迁移:Debezium + Kafka + MongoDB Connector
- 适用场景:需要从 MySQL/PostgreSQL 迁移到 MongoDB,或者需要基于 CDC (Change Data Capture) 进行实时数据流转。
- 优点:实时性强,解耦源和目标,支持复杂的数据转换逻辑。
专家建议:如果是同构迁移(MongoDB 到 MongoDB),且数据量大于 100GB,首选 Percona 的多线程工具;如果要求绝对不中断业务,走副本集同步方案。
二、 数据一致性:如何证明你的数据“毫发无损”?
这是迁移中最核心的信任问题。仅仅“导完了”不代表“导对了”。我们需要从三个维度来保证一致性:完整性、准确性、时效性。
1. 文档级哈希校验 (MD5/SHA256)
这是最基础也最有效的方法。在迁移前后,分别计算所有文档内容的哈希值总和。
操作思路:
- 在源库执行聚合管道,提取
_id和$binary类型的序列化内容,计算哈希。 - 在目标库执行相同操作。
- 对比两个哈希值是否完全一致。
- 在源库执行聚合管道,提取
代码示例 (Python 脚本逻辑):
import hashlib from pymongo import MongoClient def calculate_collection_hash(client, db_name, coll_name): """ 计算集合中文档内容的哈希指纹 """ db = client[db_name] collection = db[coll_name] # 使用聚合管道获取每个文档的 BSON 表示并计算哈希 # 注意:实际生产中可能需要分片处理,这里展示核心逻辑 pipeline = [ {"$project": { "content": "$$ROOT" }}, {"$group": { "_id": None, "hash_sum": {"$sum": {"$toLong": "$$REMOVE"}} # 伪代码,实际需自定义哈希算法 }} ] # 更稳妥的方式:遍历文档计算 MD5 累加 total_hash = hashlib.md5() cursor = collection.find({}, no_cursor_timeout=True) for doc in cursor: # 将文档转为 JSON 字符串再编码为 bytes doc_str = str(doc).encode('utf-8') total_hash.update(doc_str) return total_hash.hexdigest() source_hash = calculate_collection_hash(source_client, 'mydb', 'users') target_hash = calculate_collection_hash(target_client, 'mydb', 'users') if source_hash == target_hash: print("✅ 数据一致性校验通过!") else: print("❌ 数据不一致!请检查迁移日志。")
2. 计数与大小比对
- 文档数量:
db.collection.countDocuments()必须相等。 - 磁盘大小:比较源库和目标库该集合占用的磁盘空间。虽然不完全精确(因为索引压缩率可能不同),但如果差异超过 1%,通常意味着有问题。
3. 抽样比对 (Sample Check)
对于超大规模数据,全量哈希计算耗时极长。可以采用随机抽样策略:
- 从源库随机抽取 1000 个文档 ID。
- 在目标库查找这些 ID 对应的文档。
- 逐字段比对内容。
- 如果抽样无误,统计学上可推断整体数据一致。
4. 利用 --oplog 保证最终一致性 (针对在线迁移)
如果你使用的是 mongodump --oplog,它会在 dump 结束后记录一段时间内的操作日志 (Oplog)。在恢复时,先恢复快照数据,再回放 Oplog。
- 关键点:回放 Oplog 时,务必监控是否有冲突错误。Oplog 回放能保证的是时间点一致性,即恢复到 dump 结束那一刻的状态。
三、 迁移中断与恢复:如何优雅地“断点续传”?
迁移跑了一半,服务器重启了?网络断了?别慌,不同的工具有不同的恢复策略。
1. 官方 mongorestore 的局限性
官方的 mongorestore 不支持原生的断点续传。如果中断,你通常需要:
- 重新从头开始:效率极低。
- 手动拆分文件:将
.bson文件按集合或时间范围拆分,只恢复未完成的集合。这需要极强的脚本能力。
2. Percona MongoDB Tools 的“智能恢复”
Percona 的 mongorestore 提供了 -r (resume) 参数,这是解决中断问题的神器。
工作原理:
- 它会在目标数据库创建一个隐藏的辅助集合(如
pmt_resume_info),记录每个文档的_id和迁移状态。 - 当迁移中断时,再次运行命令并加上
-r。 - 工具会查询这个隐藏集合,跳过已成功的文档,只重传失败的文档。
- 它会在目标数据库创建一个隐藏的辅助集合(如
实战命令:
# 首次迁移 pmmongorestore --host src_host --db mydb --archive=mydata.gz # 假设中途断开,修复网络后,使用 -r 参数恢复 pmmongorestore --host dst_host --db mydb --archive=mydata.gz -r注意:使用
-r前,确保目标库中没有残留的脏数据干扰,或者确认工具能识别这些脏数据并覆盖。
3. 副本集同步方案的“天然恢复”
如果你采用的是副本集同步方案,那么“中断”的概念被弱化了。
- 场景:新节点作为 Secondary 加入旧主库集群。
- 中断处理:如果网络断开,Secondary 停止同步。网络恢复后,Secondary 会自动从断点处继续拉取 Oplog 进行回放。
- 优势:这是 MongoDB 内置的机制,极其稳定,不需要人工干预恢复逻辑。
4. 编程级自定义恢复 (针对 Python/Java 应用)
如果你是自己写脚本迁移,必须实现幂等性和状态持久化。
核心逻辑:
- 建立一个“迁移状态表” (
migration_state),存储{collection_name, last_processed_id, status}。 - 每次读取源数据时,带上
filter: {_id: {$gt: last_processed_id}}。 - 每处理 N 条数据,更新一次状态表。
- 程序崩溃重启后,读取状态表,从
last_processed_id继续。
- 建立一个“迁移状态表” (
代码示例 (Python 实现简易断点续传):
from pymongo import MongoClient import json class MigrationResumer: def __init__(self, source_uri, target_uri, db_name, coll_name): self.source = MongoClient(source_uri)[db_name][coll_name] self.target = MongoClient(target_uri)[db_name][coll_name] self.state_db = MongoClient(target_uri)['migration_meta']['state'] self.batch_size = 1000 def get_last_id(self): """获取上次处理的最后一个文档ID""" state_doc = self.state_db.find_one({"coll": self.source.name}) return state_doc.get("last_id", None) if state_doc else None def save_state(self, last_id): """保存当前进度""" self.state_db.update_one( {"coll": self.source.name}, {"$set": {"last_id": last_id, "updated_at": datetime.now()}}, upsert=True ) def migrate(self): start_id = self.get_last_id() query = {} if start_id: query["_id"] = {"$gt": start_id} # 确保按 _id 排序,保证断点续传的确定性 cursor = self.source.find(query).sort("_id", 1) batch = [] for doc in cursor: batch.append(doc) if len(batch) >= self.batch_size: self.target.insert_many(batch) self.save_state(batch[-1]["_id"]) batch = [] print(f"Migrated batch ending at {batch[-1]['_id'] if batch else 'N/A'}") if batch: self.target.insert_many(batch) self.save_state(batch[-1]["_id"]) print("Final batch migrated.") # 使用示例 # resumer = MigrationResumer("mongodb://localhost:27017", "mongodb://localhost:27018", "testdb", "users") # resumer.migrate()
四、 给小朋友也能听懂的总结
想象你要把一箱乐高积木从一个房间搬到另一个房间。
选工具:
- 积木少(几十块):用手拿(
mongodump)就行。 - 积木多(几千块):找几个帮手一起搬(Percona 多线程),或者用推车(副本集同步)。
- 不能停玩具时间:让新朋友在旁边慢慢拼,拼好了再换过去(在线热迁移)。
- 积木少(几十块):用手拿(
保一致:
- 搬完后,数一数积木块数对不对(Count)。
- 随机挑几块看看颜色和形状有没有变(Sampling)。
- 最好给每块积木拍张照,搬完再拍一张,对比照片是否一样(Hash)。
防中断:
- 如果路上累了休息,要记住自己搬到了第几块积木(Save State)。
- 下次接着搬时,不要从头再来,而是从记住的那一块继续(Resume)。
- Percona 工具就像一个聪明的机器人,它自己会记笔记,断线后自动接着记过的地方搬。
五、 最终建议
在实际生产环境中,没有单一的完美解决方案。我的建议是:
- 评估数据量:<50GB 用 `mongodump` + 脚本包装实现简易断点;>50GB 直接用 Percona PMT。
- 评估停机容忍度:能停机,用批量迁移工具+哈希校验;不能停机,用副本集同步+应用层灰度切换。
- 永远不要相信“看起来成功了”:迁移完成后,务必运行一致性校验脚本,并观察目标库的业务指标至少 24 小时,确认无异常后再切断源库连接。
迁移是一场与数据的对话,细心和严谨比速度更重要。希望这份指南能帮你平稳度过每一次数据迁徙。
