24. MongoDB 数据迁移与同步实战

mongodump/mongorestore 逻辑备份、mongosync 在线迁移与 oplog 回放原理、双写与停机窗口策略、迁移到 Atlas Live Migration、增量同步与校验回滚预案

数据迁移是 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)。

停机窗口策略(简单可靠):

  1. 维护窗口内停掉写入应用
  2. 全量同步 + oplog 追平(mongosync commit 或 mongodump+restore)
  3. 校验数据与索引
  4. 切换连接串,恢复写入
# 停机窗口内的完整操作序列(示意)
# 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 的关键步骤:

  1. 在 Atlas 中创建目标集群与迁移用户
  2. 在源端开启 db.setProfilingLevel(0) 避免 profiler 干扰,并确认 oplog 窗口充足
  3. Atlas 执行全量拷贝 + 增量追平
  4. 达到 catch-up 状态后,选择切换时间点(Cutover)
  5. 切换:更新应用连接串到 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, ... }
校验层内容工具
文档计数countDocumentsmongosh
数据指纹哈希对比自定义聚合 / 三方工具
索引对比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 小时以上的复制延迟、慢查询与连接数指标。迁移不是终点,而是新集群运维的起点。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mongodb」更多文章

  1. 27. MongoDB 多租户与隔离架构设计
  2. 26. MongoDB 监控与可观测性实践
  3. 25. MongoDB WiredTiger 存储引擎深入