1. 项目背景
业务场景:本地生活电商平稳运行了一年,直到某周六凌晨——一个运维在做例行磁盘清理时,错误地把 /data/mongodb 目录当成了 /data/mongodb-backup 目录,执行了 rm -rf。凌晨 2 点被监控告警叫醒——MongoDB 主库数据目录被清空。团队紧急启动备份恢复流程,但发现最近的可用备份是 4 天前的——过去 4 天的 8 万笔订单全部丢失。
更糟糕的是,恢复过程中发现备份文件损坏——过去半年一直自动跑着备份脚本,但从来没验证过备份文件能否成功恢复。RPO(Recovery Point Objective)高达 4 天,RTO(Recovery Time Objective)花了 6 个小时——远超出公司对核心数据的 RPO 小于 1 小时、RTO 小于 30 分钟的要求。
痛点:备份做得不好,恢复等于没做。多数团队备份存在但不知道能不能恢复——备份文件损坏、备份脚本悄悄失败、备份存储介质与生产同盘。不具备 RPO-RTO 的概念——不知道业务允许丢多少数据、允许停多久。灾备演练不执行——因为害怕演练期间影响生产,但实际上不演练的灾备计划等于没有。
2. 项目设计
小胖(手还在发抖):大师,昨晚运维 rm -rf 把 MongoDB 数据目录清空了。备份发现是 4 天前的,8 万条订单丢失!老板说再出这种事就完蛋了。
大师:先冷静。从今天起重建备份体系——但这次要带三个硬指标:RPO、RTO、恢复演练。
小胖:RPO 和 RTO 是啥?
大师:RPO 即 Recovery Point Objective——你能容忍最多丢多少数据,以时间为单位。RPO=5 分钟意味着最多丢 5 分钟的数据,你的备份间隔必须小于等于 5 分钟。RTO 即 Recovery Time Objective——你能容忍恢复过程最多用多久。RTO=30 分钟意味着从发现故障到恢复完成的全部操作必须在 30 分钟内完成。
技术映射:RPO 决定备份的频率和策略(全量-增量-Oplog),RTO 决定恢复的手段和自动化程度(文件快照恢复快于 mongorestore 恢复快于全量重建索引恢复)。
小胖:那我们怎么做到 RPO 小于 5 分钟、RTO 小于 30 分钟?
大师:单靠 mongodump 做不到——mongodump 的全量备份耗时可能超过 2 小时。你需要 Oplog 增量备份——每隔 5 分钟从 Oplog 中备份增量变化。结合每天一次的文件系统快照(全量)加持续 Oplog 备份(增量),就可以实现 RPO 小于 5 分钟。
小白:文件系统快照是什么?比 mongodump 快在哪?
大师:文件系统快照(LVM Snapshot、ZFS Snapshot、EBS Snapshot)是在块设备层面一瞬间生成数据目录的完整快照——不需要遍历每个文档,不需要序列化和反序列化。一个 TB 级数据库的 mongodump 可能需要 8 小时,而文件系统快照只需数秒。
技术映射:文件系统快照等于物理级备份(快、大、依赖存储),mongodump 等于逻辑级备份(慢、小、跨平台兼容)。两种方法配合使用——每天一次快照作为全量基线,每 5 分钟 Oplog 备份作为增量。
小胖:快照不会锁库吗?
大师:现代的 LVM-ZFS-云厂商快照都支持热快照——在快照过程中文件系统处于瞬间冻结状态(通常毫秒级),应用层几乎无感知。MongoDB 的 Journal 文件确保了快照时刻的写入是完整一致的——恢复时 WiredTiger 会通过 Journal 自动 replay 确保数据完整。
小白:灾备演练该怎么做?是不是就恢复一下看看能不能启动?
大师:不止能启动。标准演练清单:选一个完全隔离的环境避免恢复覆盖生产;用最近的备份恢复完整的数据集;验证核心集合的文档数、索引数与生产一致(抽样校验);模拟业务查询——跑几个关键 API 确认数据完整性和可用性;记录恢复总耗时——对比 RTO 目标。演练频率——核心系统每季度一次,非核心系统每半年一次。
大师(总结):三点——备份策略要分层(全量快照加增量 Oplog);RPO-RTO 是业务语言而非技术语言(用这个跟老板沟通资源投入);不演练的灾备计划等于没有。
3. 项目实战
3.1 环境准备
需要复制集环境以使用 Oplog(单机无 Oplog)。使用第 17 章的 3 节点复制集。
3.2 分步实现
步骤一:建立 Oplog 增量备份
目标:从复制集的 Oplog 中持续提取增量变更。
// oplog-backup.js —— Oplog 增量备份脚本
// 原理:每隔 intervalSec 从 Oplog 中读取新增的条目并存储
function backupOplogIncrement(lastTimestamp, outputDb) {
var oplog = db.getSiblingDB("local").oplog.rs
var query = {}
if (lastTimestamp) {
query.ts = { $gt: lastTimestamp }
}
var incrementalOps = oplog.find(query).sort({ $natural: 1 }).toArray()
if (incrementalOps.length === 0) {
return { count: 0, lastTimestamp: lastTimestamp }
}
// 将增量 Oplog 条目存入备份集合
var backupColl = db.getSiblingDB(outputDb).oplog_increment
var bulkOps = incrementalOps.map(function(op) {
return { insertOne: { document: op } }
})
backupColl.bulkWrite(bulkOps, { ordered: false })
var newLastTimestamp = incrementalOps[incrementalOps.length - 1].ts
return { count: incrementalOps.length, lastTimestamp: newLastTimestamp }
}
// === 增量备份测试 ===
// 记录起始位置
var oplog = db.getSiblingDB("local").oplog.rs
var startTs = oplog.find().sort({ $natural: -1 }).limit(1).next().ts
print("起始 Oplog 位置:", startTs)
// 写入一些测试数据
db.backup_source.insertOne({ test: "oplog增量测试", createdAt: new Date() })
// 备份增量
sleep(2000) // 等 Oplog 写入
var result = backupOplogIncrement(startTs, "backup_db")
print("增量备份: " + result.count + " 条")
// 重复执行即可持续增量备份
// 生产环境中此脚本由 cron 每隔 5 分钟调用一次
// 每次调用传入上一次返回的 lastTimestamp
步骤二:基于 Oplog 做时间点恢复(PITR)
目标:将全量备份恢复到某个特定时间点。
// === 时间点恢复(PITR)概念流程 ===
// 该脚本展示了恢复的思想,实际执行需要 mongorestore
/*
PITR 恢复步骤:
1. 恢复最近一次全量备份(mongodump或快照)
mongorestore --host=recovery_host /backup/full_20260320/
2. 从 Oplog 增量备份中提取全量备份之后到目标时间点的操作
var targetTime = new ISODate("2026-03-21T14:30:00Z")
var ops = db.oplog_increment.find({
ts: { $gt: fullBackupTimestamp, $lte: Timestamp(targetTime, 0) }
}).sort({ ts: 1 })
3. 重放 Oplog 到恢复目标
使用 mongorestore 的 --oplogReplay 参数:
mongorestore --oplogReplay \
--oplogLimit="2026-03-21T14:30:00Z" \
/backup/with_oplog/
4. 关键注意事项:
- mongorestore --oplogReplay 需要在恢复全量数据之后立即执行
- 恢复后的数据库会包含全量快照加增量操作后的精确状态
- 事务期间的 Oplog 条目包含 applyOps 数组,需要完整还原
*/
print("PITR 恢复流程已记录,需在 mongorestore 命令行中执行")
步骤三:文件系统快照备份(概念 + Docker 模拟)
# === 文件系统快照概念演示 ===
# 1. 创建 LVM 快照(需 LVM 卷,此处为概念演示)
# lvcreate -L 10G -s -n mongo_snap /dev/vg_data/mongo_lv
# 2. 用 Docker 模拟"快照"(通过拷贝数据目录)
# 注意:实际生产中不需要 fsyncLock,热快照依赖 Journal 保证一致性
# 以下代码仅为理解"快照就是瞬时的数据目录镜像"
# MongoDB Journal 保证了崩溃一致性:
# docker exec mongodb-lab mongosh --eval "db.fsyncLock()" # 刷新并锁定写入
# docker cp mongodb-lab:/data/db ./backups/snapshot_$(date +%Y%m%d_%H%M%S)
# docker exec mongodb-lab mongosh --eval "db.fsyncUnlock()"
# 实际生产环境的快照策略:
# 方案 A:云厂商 EBS 快照
# 0 2 * * * /usr/bin/aws ec2 create-snapshot --volume-id vol-xxx --description "MongoDB daily"
# 方案 B:LVM 快照(自建机房)
# 0 2 * * * /usr/local/bin/mongo-snapshot.sh
# 方案 C:ZFS 快照 + 发送到备机
# zfs snapshot tank/mongodb@daily-$(date +%Y%m%d)
# zfs send tank/mongodb@daily-xxx | ssh backup-host zfs recv backup/mongodb
# 3. 恢复快照
# 停止目标 MongoDB
# 清空 /data/db 目录
# 拷贝快照文件至 /data/db
# 启动 MongoDB —— WiredTiger 自动通过 Journal 恢复到一致性状态
步骤四:灾备演练全流程
目标:编写可重复执行的演练脚本。
# disaster-recovery-drill.ps1
# 灾备演练全流程 (Windows PowerShell)
param(
[string]$BackupDir = "D:\backups\life_mall",
[string]$RecoveryHost = "localhost",
[int]$RecoveryPort = 27018,
[string]$RecoveryDb = "local_life_restored"
)
Write-Host "=== 灾备演练开始 ===" -ForegroundColor Yellow
$sw = [System.Diagnostics.Stopwatch]::StartNew()
# 步骤 1:找到最近一次可用备份
$latestBackup = Get-ChildItem $BackupDir -Filter "life_mall_*.zip" |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $latestBackup) {
Write-Host "FAIL: 未找到可用备份!" -ForegroundColor Red
exit 1
}
Write-Host "PASS: 最近备份: $($latestBackup.Name) ($([math]::Round($latestBackup.Length/1MB,0)) MB)"
# 步骤 2:恢复到隔离库
Write-Host "正在恢复..."
mongorestore --host=$RecoveryHost --port=$RecoveryPort `
--db=$RecoveryDb --drop `
--gzip --archive="$($latestBackup.FullName)" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "FAIL: 恢复失败!" -ForegroundColor Red
exit 1
}
Write-Host "PASS: 数据已恢复"
# 步骤 3:数据完整性校验
Write-Host "正在校验..."
$checks = mongosh --quiet "mongodb://${RecoveryHost}:${RecoveryPort}" --eval '
use local_life_restored
var r = {}
r.users = db.users.countDocuments()
r.products = db.products.countDocuments()
r.orders = db.orders.countDocuments()
r.indexes = db.products.getIndexes().length
print(JSON.stringify(r))
'
Write-Host "校验结果: $checks"
# 步骤 4:功能验证——确认索引可用
$queryTest = mongosh --quiet "mongodb://${RecoveryHost}:${RecoveryPort}" --eval '
use local_life_restored
print(db.products.find({status:"在售"}).sort({createdAt:-1}).limit(1)
.explain("executionStats").queryPlanner.winningPlan.stage)
'
# 步骤 5:报告
$sw.Stop()
$rto = [math]::Round($sw.Elapsed.TotalSeconds, 0)
Write-Host "=== 演练完成 ===" -ForegroundColor Green
Write-Host "RTO 实际: ${rto}s"
Write-Host "RPO: $([math]::Round(($latestBackup.LastWriteTime - (Get-Date)).TotalHours,0))h 前"
步骤五:备份恢复 3-2-1 原则落地
// === 备份合规检查 ===
// "3-2-1"原则:3 份副本、2 种介质、1 份异地
var checklist = {
copies: {
copy1: "日备份 (LVM快照 DAS存储)",
copy2: "日备份 (mongodump 对象存储 OSS)",
copy3: "Oplog 增量 (本地 + 同步到 OSS)",
count: 3
},
media: {
type1: "本地 DAS或SSD",
type2: "对象存储 (OSS或S3)",
count: 2
},
offsite: {
location: "OSS 跨区域复制 (华南到华北)",
enabled: true
},
encryption: "备份文件 AES-256 加密",
retention: {
daily: 7,
weekly: 4,
monthly: 12
}
}
print("=== 备份合规检查 ===")
print("副本数:", checklist.copies.count, checklist.copies.count >= 3 ? "PASS" : "FAIL")
print("介质数:", checklist.media.count, checklist.media.count >= 2 ? "PASS" : "FAIL")
print("异地备份:", checklist.offsite.enabled ? "PASS 已启用" : "FAIL 缺失")
print("加密:", checklist.encryption ? "PASS" : "FAIL")
步骤六:恢复优先级与业务流程的对齐
// 恢复优先级定义
// 并非所有数据都需要同等 RPO和RTO
var recoveryPriority = [
{ collection: "orders", rpo: "1min", rto: "15min", priority: "P0" },
{ collection: "coupons", rpo: "5min", rto: "30min", priority: "P1" },
{ collection: "users", rpo: "1hour", rto: "1hour", priority: "P1" },
{ collection: "products", rpo: "1hour", rto: "2hour", priority: "P2" },
{ collection: "reviews", rpo: "24hour", rto: "4hour", priority: "P3" }
]
print("=== 恢复优先级 ===")
recoveryPriority.forEach(function(r) {
var icon = r.priority === "P0" ? "[P0紧急]" : (r.priority === "P1" ? "[P1高]" : "[P2-3一般]")
print(icon + " " + r.collection + ": RPO=" + r.rpo + " RTO=" + r.rto)
})
// 意义:恢复时优先恢复 P0 数据(订单),P3 数据可以延后
// 实际恢复顺序:先恢复 orders 集合 -> 验证 -> 恢复 coupons -> 验证 -> ...
可能遇到的坑:
- mongodump 在分片集群中通过 mongos 执行时,数据一致性依赖 --oplog 参数,不加该参数各分片数据并非同一快照
- 全量备份期间如果发生 reshardCollection 操作,备份可能包含新旧两种分片键格式的数据
- 备份脚本的退出码检查——必须检查 mongodump 的退出码而非仅看 stdout 输出
3.3 完整代码清单
| 文件 | 用途 |
|---|---|
mongodb-lab/backup-scripts/oplog-backup.js |
Oplog 增量备份 |
mongodb-lab/backup-scripts/pitr-recovery.js |
时间点恢复概念 |
mongodb-lab/backup-scripts/drill.ps1 |
灾备演练全流程脚本 |
mongodb-lab/backup-scripts/checklist.js |
备份合规检查清单 |
3.4 测试验证
// 1. 确认 Oplog 可读(复制集环境)
try {
var oplog = db.getSiblingDB("local").oplog.rs
var count = oplog.find().count()
print("Oplog 可用:", count > 0 ? "PASS (" + count + " 条)" : "FAIL")
} catch(e) {
print("Oplog 不可用: 非复制集环境")
}
// 2. 验证 mongodump 可用性(宿主机命令行)
// mongodump --version
// 3. 验证 3-2-1 配置自查
print("备份自查: 你有几份备份副本?(应大于等于3)")
print("备份存储在不同介质上吗?(如 本地SSD + 对象存储)")
print("有一份备份存在异地吗?")
// 4. 验证备份文件完整性——恢复到临时库并运行 countDocuments
print("提示:执行 restore + countDocuments 验证备份可用性")
4. 项目总结
4.1 备份方案对比
| 方案 | RPO | RTO | 存储成本 | 恢复粒度 | 适用数据量 |
|---|---|---|---|---|---|
| mongodump 全量 | 1天 | 1-4 小时 | 低 | 库-集合 | 小于 100GB |
| mongodump + Oplog 增量 | 5 分钟 | 30 分钟 | 中 | 任意时间点 | 小于 500GB |
| 文件系统快照 + Oplog | 1 分钟 | 5 分钟 | 高(块级) | 任意时间点 | 不限 |
| 云厂商自动备份 | 5 分钟(PITR) | 视数据量 | 按量付费 | 任意时间点 | 不限 |
| 仅 Oplog 增量 | 实时 | 视 Oplog 大小 | 低 | 受 Oplog 窗口限制 | 仅配合全量使用 |
4.2 适用场景
分层备份策略适用:核心交易系统用文件系统快照加 Oplog 增量,RPO 小于 1 分钟;内容管理系统用 mongodump 日备加 Oplog 增量,RPO 小于 1 小时;日志-分析系统用 mongodump 周备,RPO 小于 1 天;归档数据仅 mongodump 不设增量。
4.3 注意事项
| 注意事项 | 说明 |
|---|---|
| 备份文件不能与生产数据在同一块磁盘上 | 磁盘损坏等于数据和备份一起丢失 |
| Oplog 增量恢复高度依赖 Oplog 窗口 | Oplog 窗口不足时增量断裂需重新做全量 |
| 恢复索引比恢复数据慢 | 快照恢复包含索引数据;mongorestore 需重建索引(耗时要纳入 RTO 计算) |
| 备份脚本需要监控和告警 | 备份失败无人知等于没有备份 |
4.4 常见踩坑经验
故障案例一:备份文件损坏但每月才检查一次
某公司在月底对账时才发现一个月内的所有 mongodump 备份文件都损坏了——原因是备份脚本所在的磁盘有坏道。解决:每次备份完成后立即执行 mongorestore --dryRun 验证备份文件可读性。
故障案例二:快照恢复后发现缺少 Oplog 增量文件
某团队执行文件系统快照做全量加本地磁盘存 Oplog 增量。磁盘故障后全量快照和 Oplog 增量在同盘全部丢失。解决:Oplog 增量必须实时同步到异介质(对象存储-独立 NAS),不能与全量快照同盘。
故障案例三:PITR 恢复到错误的时间点
某次事故后团队执行 PITR 恢复到事故发生前 5 分钟。结果恢复后发现——事故在 14:30 发生但 Oplog 增量脚本的服务器时钟快 5 分钟,实际恢复到了事故发生的同时。解决:所有参与备份和恢复的服务器使用同一 NTP 时间源;PITR 的时间点使用 MongoDB 内部的 clusterTime 而非操作系统的挂钟时间。
4.5 思考题
- 如果一个 TB 级数据库每天增量约 50GB,Oplog 窗口需要保持 48 小时。计算需要多大的 Oplog 磁盘空间?如果备份存储的带宽是 100MB-s,增量备份是否会在 5 分钟内完成?
- 灾备演练中如果恢复后的数据校验出"集合存在但文档数为 0",而生产环境的文档数正常,最可能的原因是什么?
(答案将在第 30 章末尾揭晓)
上一章思考题答案:
mongodb_exporter 推荐连 mongos——因为分片集群的核心指标(如总 QPS、总连接数、整体复制延迟)是跨所有分片的聚合值,只有 mongos 能提供这个聚合视图。直连单个分片拿不到其他分片的数据。
totalDocsExamined 小但查询仍慢——可能是网络延迟(客户端到 MongoDB 的 RTT 高)、锁等待(currentOp 中 waitingForLock)、大的 BSON 文档传输(文档体积数 MB)、或者执行时间中 CPU 时间不在 explain 统计但查询整体阻塞在排队中。用 db.currentOp 结合 profiler 确认实际阻塞点。
延伸阅读与资源
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号