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 思考题

  1. 如果一个 TB 级数据库每天增量约 50GB,Oplog 窗口需要保持 48 小时。计算需要多大的 Oplog 磁盘空间?如果备份存储的带宽是 100MB-s,增量备份是否会在 5 分钟内完成?
  2. 灾备演练中如果恢复后的数据校验出"集合存在但文档数为 0",而生产环境的文档数正常,最可能的原因是什么?

(答案将在第 30 章末尾揭晓)

上一章思考题答案

  1. mongodb_exporter 推荐连 mongos——因为分片集群的核心指标(如总 QPS、总连接数、整体复制延迟)是跨所有分片的聚合值,只有 mongos 能提供这个聚合视图。直连单个分片拿不到其他分片的数据。

  2. 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 实战修炼与源码剖析

posted on 2026-08-13 11:11  一天不进步,就是退步  阅读(2)  评论(0)    收藏  举报