数据迁移是 MongoDB 生产运维中风险最高、最容易出错的操作之一。无论是从自建集群迁往 Atlas、跨版本升级(如 4.x → 7.x)、机房搬迁,还是集群重组,迁移方案的背后都绕不开三个问题:怎么把存量数据搬过去、怎么把增量数据追平、怎么在失败时安全回滚。本文将以真实命令为骨架,系统讲解逻辑/物理迁移工具、在线同步工具 mongosync 的机制、oplog 回放原理,以及迁移后的校验与回滚预案。
前置提醒:任何迁移在开工前都应完成一次完整备份。迁移不是"复制文件",而是数据、索引、权限、集合选项(如 collation、validator)的完整重建,遗漏任何一项都会在后续暴露问题。
1. 逻辑备份:mongodump / mongorestore
mongodump 从 MongoDB 读取数据并写出 BSON 文件,属于逻辑备份:它通过查询接口读取文档,输出为与存储引擎无关的 BSON + metadata 文件。优点是可跨版本、跨引擎恢复;缺点是速度慢于物理拷贝,且大集合下资源占用明显。
# 逻辑备份:全库导出为 BSON(gzip 压缩)
mongodump \
--uri="mongodb://backup:pass@mongodb-primary:27017/admin?replicaSet=rs0" \
--out=/backup/dump-20260927 \
--gzip \
--numParallelCollections=4
# 仅导出指定数据库/集合
mongodump --uri="$MONGO_URI" --db=shop --collection=orders --gzip --out=/backup/orders-only
# 归档模式:输出单文件,便于管道传输
mongodump --uri="$MONGO_URI" --gzip --archive=/backup/mongo.archive.gz
--archive 可以把整个转储写成单一归档流,配合 gzip 后可直接传输。需要 Point-in-Time 一致性时,mongodump 默认给出的是导出开始时间点的一致快照(对副本集使用 --oplog 可捕获导出期间的增量):
# 带 oplog 的备份:可以恢复到最后一条 oplog 条目的时间点
mongodump \
--uri="$MONGO_URI" \
--oplog \
--gzip \
--out=/backup/dump-with-oplog
恢复端对应工具为 mongorestore:
# 恢复整个 dump 目录
mongorestore --uri="mongodb://target:27017" --gzip --drop /backup/dump-20260927
# 恢复单集合
mongorestore --uri="$TARGET_URI" --gzip --db=shop --collection=orders /backup/dump-20260927/shop/orders.bson.gz
# 归档 + oplog 回放到指定时间点
mongorestore --uri="$TARGET_URI" --gzip --archive=/backup/mongo.archive.gz
mongorestore --uri="$TARGET_URI" --oplogReplay --gzip /backup/dump-with-oplog
| 场景 | 工具 | 注意 |
|---|---|---|
| 全量迁移 | mongodump –archive | 目标端先建好索引可加速 |
| 单库/单集合 | mongodump –db –collection | 不迁移其他库 |
| 跨大版本升级 | mongodump/restore 或 mongosync | 版本差异需先读 release notes |
| 点恢复 | –oplog + –oplogReplay | 需保留 oplog 窗口 |
提示:mongodump 在 4.2+ 不再支持
--dbpath物理直读;对大数据集(数百 GB 以上),逻辑备份耗时可能以小时计,此时应优先考虑物理备份或在线同步工具。
2. 物理备份与逻辑备份的差异
物理备份直接复制磁盘上的数据文件(/data/db 下的 WiredTiger 文件),速度与一致性表现不同,但依赖同一版本与同架构。
| 维度 | 逻辑备份(mongodump) | 物理备份(文件快照) |
|---|---|---|
| 数据形式 | BSON 文档 | WiredTiger 数据文件 |
| 速度 | 慢(逐文档读取) | 快(文件级拷贝) |
| 跨版本 | 可(BSON 兼容) | 严格同版本 |
| 资源占用 | 高(客户端驱动) | 低(可 LVM/云快照) |
| 一致性 | 需 –oplog | 需 fsync 锁或一致性快照 |
物理备份的标准做法是配合 db.fsyncLock() 或文件系统快照实现一致快照:
// 触发一致性快照前,锁定写入
db.fsyncLock()
// 此时复制 /data/db 目录(示例:tar 打包)
// tar -czf /backup/physical.tgz /data/db
// 解锁
db.fsyncUnlock()
警告:
db.fsyncLock()会暂停整个 mongod 的写入,只应在副本集备用节点或低峰期使用;4.0+ 对副本集成员使用db.fsyncLock()时只能短暂锁定。
3. 在线同步:mongomirror 与 mongosync
生产迁移需要"双跑":旧集群继续服务,新集群在后台追平数据,最后切换。官方在线同步工具经历了从 mongomirror(Atlas 迁移专用,已弃用)到 mongosync(Atlas 与自建集群通用)的演进。
mongosync 在 MongoDB 集群层面建立连续同步,通过消费源端 oplog 把增量变更应用到目标端,并支持进度查询与 commit 切换:
# 启动 mongosync:source 为源端,cluster0 为同步主节点
mongosync \
--cluster0 mongodb://admin:pass@source-rs:27017/admin?replicaSet=src \
--cluster1 mongodb://admin:pass@target-rs:27017/admin?replicaSet=dst \
--logPath /var/log/mongosync
# 查询同步进度
mongosync --cluster0 "$SRC_URI" --cluster1 "$DST_URI" --progress
# 查询当前状态(开始/运行/提交)
curl -s http://localhost:27182/api/v1/status
mongosync 提供 HTTP API 控制生命周期,关键阶段包括:IDLE(等待任务)→ RUNNING(全量+增量追平)→ COMMITTING(暂停写入验证一致性)→ COMMITTED(完成切换准备)。
| mongosync 阶段 | 行为 | 业务影响 |
|---|---|---|
| RUNNING | 全量复制 + oplog 回放 | 无影响 |
| COMMITTING | 停止应用源端新写入,核对数据 | 需短暂停写窗口 |
| COMMITTED | 目标端数据一致,可切换 | 允许应用切流量 |
mongomirror 是早期 Atlas Live Migration 的底层组件,已被 mongosync 取代。自建集群之间的持续同步,mongosync 是目前官方推荐的路径。
注意:mongosync 1.x 对目标端有严格要求(空库或白名单集合),且不支持 TTL 索引之外的部分 DDL(如 collMod 验证器等),迁移前需核对支持矩阵。
mongosync 的部署通常以容器或独立进程运行,通过 27182 端口暴露 REST API:
# 以 Docker 运行 mongosync(示例)
docker run -d \
--name mongosync \
-p 27182:27182 \
-v /data/mongosync:/var/log/mongosync \
registry.mongodb.com/mongosync/mongosync \
mongosync --cluster0 "$SRC_URI" --cluster1 "$DST_URI"
# 查看 API 端点与版本
curl -s http://localhost:27182/api/v1/status | python3 -m json.tool
curl -s http://localhost:27182/api/v1/version
版本兼容性是线上迁移最常见的坑。源端与目标端的 MongoDB 大版本差异过大(如 4.4 → 8.0)、或者源端存在 mongosync 不支持的特性(集合级 collation、视图、部分 DDL),都会导致任务中断或数据语义漂移。
| mongosync 检查项 | 说明 | 处理方式 |
|---|---|---|
| 源/目标大版本差 | 跨 3 个大版本以上风险高 | 分阶段升级或改用 dump/restore |
| 视图与函数 | mongosync 不同步 view | 迁移后手动重建视图 |
| collMod 验证器 | 部分版本不支持 | 迁移后手工应用 validator |
| 空库校验 | 目标库应为空或白名单 | 预先清理目标库 |
| oplog 窗口 | 需覆盖全量耗时 | 放大 oplogSize 后再迁移 |
4. oplog 回放原理
MongoDB 副本集内所有写操作都会写入本地的 capped 集合 oplog.rs。oplog 条目本质上是一条描述操作的幂等日志:
// 查看一条 oplog 条目
db.getSiblingDB("local").oplog.rs.find().sort({ $natural: -1 }).limit(1).toArray()
// [
// {
// op: "i", // i=insert, u=update, d=delete, c=command, n=noop
// ns: "shop.orders", // 操作的命名空间
// ui: UUID("..."), // 集合 UUID
// ts: Timestamp(1785400000, 1), // 逻辑时间戳(秒 + 序号)
// o: { _id: ObjectId("..."), amount: 120 }, // 操作内容
// o2: { _id: ObjectId("...") } // 更新条件的过滤键
// }
// ]
在线迁移工具的增量同步本质是"从源端某个 ts 开始持续消费 oplog,并在目标端重放"。ts 是唯一排序依据:它是单调递增的 Timestamp(sec, ord),Secondary 与迁移工具都靠它对齐复制位置。
// 手工模拟增量回放(概念演示):读取并重放最新操作
const cursor = db.getSiblingDB("local").oplog.rs.find({
ts: { $gt: Timestamp(1785390000, 1) }
}).sort({ ts: 1 }).addOption(2 /* tailable */)
cursor.forEach(entry => {
// 依据 entry.op 分发到目标集合
// "i" -> insertOne, "u" -> updateOne, "d" -> deleteOne
})
oplog 是 capped 集合,容量有限(默认约为磁盘的 5% 或按 oplogSizeMB 指定)。如果迁移工具消费速度跟不上写入速率,源端 oplog 可能被覆盖,增量同步会被迫中断并重新全量。因此迁移前应确认 rs.printReplicationInfo() 显示的 oplog 窗口足够覆盖全量拷贝 + 增量追平的时间。
| 检查项 | 命令 | 迁移前阈值 |
|---|---|---|
| oplog 总大小 | rs.printReplicationInfo() | 窗口 > 预估全量耗时 × 2 |
| 当前落后量 | rs.printSecondaryReplicationInfo() | < 数秒 |
| 写入速率 | mongostat opcounters | 评估消费压力 |
5. 双写与停机窗口策略
迁移切换有两种典型策略:停机窗口(maintenance window)与双写(dual-write)。
停机窗口策略(简单可靠):
- 维护窗口内停掉写入应用
- 全量同步 + oplog 追平(
mongosynccommit 或 mongodump+restore) - 校验数据与索引
- 切换连接串,恢复写入
# 停机窗口内的完整操作序列(示意)
# 1) 停应用 -> 2) mongodump --oplog -> 3) mongorestore -> 4) 校验 -> 5) 切流量
双写策略(无停机):
应用同时写新旧两库,随后回放增量到目标端。双写能实现近零停机,但引入一致性与补偿复杂度:双写期间任一库失败都会造成数据分歧,且目标端在建索引期间可能阻塞写入。
| 维度 | 停机窗口 | 双写 |
|---|---|---|
| 停机时间 | 分钟~小时级 | 接近零 |
| 一致性风险 | 低 | 高(需补偿机制) |
| 实施复杂度 | 低 | 高 |
| 适用场景 | 夜间维护 | 严格 SLA 场景 |
工程建议:优先采用"mongosync 在线追平 + 短停写 commit + 校验切换"的组合,把停机窗口压缩到分钟级,同时规避双写的一致性负担。
6. 迁移到 Atlas:Live Migration
从自建集群迁移到 Atlas 有两种官方路径:手动迁移(mongodump/restore 或 mongosync)与 Atlas Live Migration(托管式在线迁移)。
Atlas Live Migration 使用代理抓取源端 oplog,把增量变更流式迁移到 Atlas,整个过程源端保持可读可写:
# 手动迁移到 Atlas:mongodump + mongorestore(一次性的简单方案)
mongodump --uri="$SELF_HOSTED_URI" --gzip --archive | \
mongorestore --uri="$ATLAS_SRV" --gzip --archive
# 在线迁移:在 Atlas 控制台创建 Live Migration 任务后,
# 源端开启如下权限(示例):
# - 具有 root 或 readAnyDatabase + 相应角色的备份用户
Live Migration 的关键步骤:
- 在 Atlas 中创建目标集群与迁移用户
- 在源端开启
db.setProfilingLevel(0)避免 profiler 干扰,并确认 oplog 窗口充足 - Atlas 执行全量拷贝 + 增量追平
- 达到 catch-up 状态后,选择切换时间点(Cutover)
- 切换:更新应用连接串到 Atlas,旧集群只读或下线
提示:迁移到 Atlas 时注意目标集群的
srv连接串(mongodb+srv://...),并确保应用使用的驱动版本支持 DNS Seedlist 与 SRV 解析。
7. 增量同步与一致性校验
迁移完成并不等于数据正确。切换前必须做三层校验:文档级计数、数据指纹(checksum)、索引与集合选项对比。
// 第 1 层:文档计数
const src = Mongo("mongodb://src:27017").getDB("shop")
const dst = Mongo("mongodb://dst:27017").getDB("shop")
print("src.orders =", src.orders.countDocuments({}))
print("dst.orders =", dst.orders.countDocuments({}))
// 第 2 层:按 _id 分桶计算文档哈希指纹
// 对每个分桶:聚合 $bsonSize 或字段拼接后哈希,对比两侧结果
src.orders.aggregate([
{ $group: { _id: { $bucket: { groupBy: "$_id", boundaries: [0, 100000, 200000, 300000] } },
hash: { $accumulator: { init: () => "", accumulate: (acc, doc) => acc + JSON.stringify(doc), accumulateArgs: ["$$CURRENT"], merge: (a, b) => a + b, finalize: acc => md5(acc) } } } }
])
// 实际项目更推荐:导出样本 + 逐条对比,或使用三方校验工具
实践中更常用且可控的方式是按主键分桶抽样对比:取两侧按 _id 排序的前 N 条与后 N 条做字段级比对,再对随机桶做全量比对。
以下是一个按 _id 前缀分桶、逐桶计算文档级摘要的校验脚本骨架,可扩展为全量校验:
// 校验脚本:两侧按 _id 分桶,输出每个桶的文档数与内容摘要
const srcDB = Mongo("mongodb://src:27017").getDB("shop")
const dstDB = Mongo("mongodb://dst:27017").getDB("shop")
const MIN = ObjectId("000000000000000000000000")
const MAX = ObjectId("ffffffffffffffffffffffff")
for (let i = 0; i < 16; i++) {
const lo = i * 0x1000000000000000
const hi = (i + 1) * 0x1000000000000000
const lower = { _id: { $gte: MIN } }
const upper = { _id: { $lt: MAX } }
// 简化示意:实际使用 _id 范围过滤($lt/$gt)
const srcCount = srcDB.orders.countDocuments({ _id: { $gte: new ObjectId(lo.toString(16).padStart(24, "0")), $lt: new ObjectId(hi.toString(16).padStart(24, "0")) } })
const dstCount = dstDB.orders.countDocuments({ _id: { $gte: new ObjectId(lo.toString(16).padStart(24, "0")), $lt: new ObjectId(hi.toString(16).padStart(24, "0")) } })
print(`bucket-${i}: src=${srcCount} dst=${dstCount} ${srcCount === dstCount ? "OK" : "MISMATCH"}`)
}
校验时间点选择:统计类校验应在 mongosync 达到 COMMITTING(停写)后进行,否则两侧存在合法的增量差。若必须在线校验,请使用同一时间基准(如
$match截止时间)限定数据范围。
// 第 3 层:索引与集合选项对比
// 源端导出索引
src.orders.getIndexes().forEach(i => printjson(i))
// 目标端应完全一致(名称、key、options)
db.orders.getIndexes().forEach(i => printjson(i))
// 校验集合物理完整性
db.orders.validate({ full: true })
// { valid: true, nInvalidDocuments: 0, ... }
| 校验层 | 内容 | 工具 |
|---|---|---|
| 文档计数 | countDocuments | mongosh |
| 数据指纹 | 哈希对比 | 自定义聚合 / 三方工具 |
| 索引对比 | getIndexes 差异 | mongosh 脚本 |
| 物理校验 | validate({full:true}) | mongosh |
8. 回滚预案
无论迁移多么顺利,都必须预先定义回滚条件与动作。回滚的核心前提是:迁移期间保留旧集群可读可写,直到新集群稳定运行 N 天后才彻底下线。
// 回滚场景 1:切换前发现数据不一致
// 动作:中止 mongosync commit,停止向目标端写入,恢复应用连接旧集群
// mongosync --cluster0 "$SRC" --cluster1 "$DST" --abort
// 回滚场景 2:切换后应用异常
// 动作:应用连接串切回旧集群,旧集群作为唯一真相源
回滚设计要点:
- 旧集群在切换后保持"只读"或"读写"状态至少一个观察期(通常 24~72 小时)
- 切换前对旧集群再做一次备份,作为回滚基准
- 若使用 mongosync,切换(commit)后源端会被暂停应用,需明确谁能执行
--resume恢复
# mongosync 常用控制命令汇总
mongosync --cluster0 "$SRC_URI" --cluster1 "$DST_URI" --progress # 查询进度
mongosync --cluster0 "$SRC_URI" --cluster1 "$DST_URI" --commit # 提交/切换
mongosync --cluster0 "$SRC_URI" --cluster1 "$DST_URI" --abort # 中止并清理
铁律:所有迁移任务必须有文档化的回滚清单(谁批准、谁执行、执行哪些命令、观察哪些指标),并预先演练至少一次。生产迁移失败后手忙脚乱找命令,是大多数数据事故的根源。
9. 迁移后的收尾与验证
切换完成后还有一系列收尾动作:确认新集群的备份策略与监控告警生效、回收旧集群资源、更新配置中心中的连接串、验证读写链路与延迟。建议在切换后对核心链路执行冒烟测试,并持续观察 24 小时以上的复制延迟、慢查询与连接数指标。迁移不是终点,而是新集群运维的起点。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。