引言:为什么MongoDB备份至关重要

在现代应用架构中,MongoDB作为领先的NoSQL数据库,承载着大量关键业务数据。无论是电商网站的用户数据、金融系统的交易记录,还是物联网设备的时序信息,数据丢失都可能造成灾难性后果。根据行业统计,数据丢失的平均成本高达每小时数万美元,而完善的备份策略能将恢复时间目标(RTO)和恢复点目标(RPO)控制在可接受范围内。

MongoDB的备份不仅仅是简单的数据复制,它需要考虑数据库的运行模式(单机、副本集、分片集群)、数据量大小、业务连续性要求以及合规性需求。一个优秀的备份策略应该具备以下特征:可靠性(确保数据完整性)、高效性(最小化对生产环境的影响)、灵活性(支持多种恢复场景)和安全性(保护数据不被未授权访问)。

本文将从基础备份方法入手,逐步深入到高可用环境下的增量备份与恢复实战技巧,帮助您构建完整的MongoDB数据保护体系。我们将涵盖mongodump/mongorestore工具的使用、文件系统快照、Oplog重放、Point-in-Time Recovery(PITR)实现,以及在副本集和分片集群环境中的最佳实践。

第一部分:MongoDB基础备份方法详解

1.1 mongodump与mongorestore:官方推荐工具

mongodump是MongoDB官方提供的逻辑备份工具,它将数据库中的数据导出为BSON格式文件,而mongorestore则用于将这些文件恢复到数据库中。这种方法适用于各种部署模式,是备份策略的基础组件。

mongodump核心参数与使用示例

mongodump支持丰富的参数来满足不同备份需求。以下是一个典型的备份命令示例:

# 基础备份命令:备份整个数据库
mongodump --host mongodb01.example.com --port 27017 --db myapp --out /backup/mongodb/2024-01-15/

# 带认证的备份(生产环境必选)
mongodump --host mongodb01.example.com --port 27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp \
  --out /backup/mongodb/2024-01-15/

# 备份特定集合(适用于大型数据库的分段备份)
mongodump --host mongodb01.example.com --port 27017 \
  --db myapp --collection users \
  --query '{"created_at": {"$gte": {"$date": "2024-01-01T00:00:00Z"}}}' \
  --out /backup/mongodb/2024-01-15/

# 压缩备份(节省存储空间)
mongodump --host mongodb01.example.com --port 27017 \
  --db myapp \
  --gzip \
  --out /backup/mongodb/2024-01-15/

# 增量备份(基于Oplog)
mongodump --host mongodb01.example.com --port 27017 \
  --oplog \
  --out /backup/mongodb/2024-01-15/

参数详解:

  • --host/--port:指定MongoDB服务器地址
  • --username/--password:认证凭据,生产环境必须使用具有backup角色的专用用户
  • --authenticationDatabase:认证数据库,通常是admin
  • --db:指定要备份的数据库,省略则备份所有数据库
  • --collection:指定要备份的集合,适用于部分备份
  • --query:使用JSON查询过滤要备份的数据,支持复杂的查询条件
  • --gzip:启用压缩,减少存储空间占用和传输时间
  • --oplog:生成Oplog快照,用于实现Point-in-Time Recovery
  • --out:指定备份输出目录

mongorestore恢复实战

mongorestore用于将mongodump生成的备份文件恢复到数据库中。恢复时需要考虑数据覆盖、索引重建等问题。

# 基础恢复:将备份恢复到新数据库
mongorestore --host mongodb01.example.com --port 27017 \
  --db myapp_restore \
  /backup/mongodb/2024-01-15/myapp/

# 恢复到原数据库(覆盖现有数据)
mongorestore --host mongodb01.example.com --port 27017 \
  --db myapp \
  --drop \  # 先删除现有集合再恢复
  /backup/mongodb/2024-01-15/myapp/

# 带认证的恢复
mongorestore --host mongodb01.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp \
  --gzip \  # 解压gzip格式的备份
  /backup/mongodb/2024-01-15/

# 恢复特定集合
mongorestore --host mongodb01.example.com --port 27017 \
  --db myapp --collection users \
  /backup/mongodb/2024-01-15/myapp/users.bson

# 并行恢复(加速大型数据库恢复)
mongorestore --host mongodb01.example.com --port 27017 \
  --db myapp \
  --numInsertionWorkersPerCollection=4 \
  --batchSize=1000 \
  /backup/mongodb/2024-01-15/

关键注意事项:

  1. 索引处理:mongorestore会自动重建索引,但对于大型集合,这可能耗时很长。可以在恢复前手动删除索引,恢复完成后再重建。
  2. 数据一致性:在恢复过程中,应确保应用停止写入,避免数据不一致。
  3. 存储空间:恢复需要足够的临时空间,特别是处理压缩备份时。
  4. 版本兼容性:mongorestore版本应与目标MongoDB版本兼容,最好保持一致。

1.2 文件系统快照备份

文件系统快照是另一种高效的备份方式,特别适用于大型数据库。它利用存储层的快照功能(如LVM、ZFS、云存储快照)在瞬间创建数据文件的完整副本。

LVM快照备份示例

# 1. 创建LVM快照(假设MongoDB数据目录在LVM卷上)
lvcreate --size 10G --snapshot --name mongo-snap /dev/vg0/mongo-data

# 2. 挂载快照卷
mkdir /mnt/mongo-snap
mount /dev/vg0/mongo-snap /mnt/mongo-snap

# 3. 复制数据文件(使用rsync保持一致性)
rsync -av /mnt/mongo-snap/data/ /backup/mongodb/snapshot-2024-01-15/

# 4. 卸载并删除快照
umount /mnt/mongo-snap
lvremove -f /dev/vg0/mongo-snap

优势与限制:

  • 优势:备份速度快(秒级完成),对数据库性能影响极小,恢复速度快
  • 限制:需要特定的存储基础设施,无法跨平台,备份文件较大

1.3 备份策略设计原则

设计备份策略时,应考虑以下核心要素:

  1. RPO(恢复点目标):可容忍的数据丢失量,决定备份频率
  2. RTO(恢复时间目标):恢复数据库所需的时间,影响备份方式和存储位置
  3. 备份窗口:业务低峰期,限制备份操作的时间范围
  4. 保留周期:根据合规要求和业务需求确定备份保留时间
  5. 存储位置:本地存储(快速恢复)+ 异地存储(灾难恢复)

典型策略示例:

  • 每日全量备份:使用mongodump或文件系统快照,保留7天
  • 每小时增量备份:基于Oplog,保留48小时
  • 每周归档备份:全量备份归档到长期存储,保留1年
  • 实时Oplog备份:用于Point-in-Time Recovery,保留24小时

第二部分:高可用环境下的增量备份与恢复

2.1 MongoDB Oplog机制详解

Oplog(Operations Log)是MongoDB副本集的核心组件,记录了所有数据变更操作。它是实现增量备份和Point-in-Time Recovery的基础。

Oplog结构与工作原理

Oplog是一个特殊的capped collection(固定大小集合),位于local数据库中:

// 查看Oplog信息
use local
db.oplog.rs.find().sort({$natural: -1}).limit(1)

// Oplog文档结构示例
{
  "ts": Timestamp(1705315200, 1),  // 时间戳:秒+顺序号
  "h": NumberLong("1234567890"),    // 操作的唯一哈希
  "v": 2,                          // 操作版本
  "op": "i",                       // 操作类型:i=insert, u=update, d=delete, c=command, n=no-op
  "ns": "myapp.users",             // 命名空间(数据库.集合)
  "o": {                           // 操作对象
    "_id": ObjectId("65a4b3c8d9e8f7a1b2c3d4e5"),
    "name": "John Doe",
    "email": "john@example.com"
  },
  "o2": {                          // 更新操作的查询条件(仅update/delete)
    "_id": ObjectId("65a4b3c8d9e8f7a1b2c3d4e5")
  }
}

Oplog大小配置: 默认情况下,Oplog大小为可用磁盘空间的5%(但有上限和下限)。对于高写入负载的系统,应适当增大Oplog以确保有足够的回溯窗口。

# 查看当前Oplog大小
db.adminCommand({getReplicationInfo: 1})

# 调整Oplog大小(需要重启mongod,或在副本集成员上执行)
# 1. 停止secondary节点
# 2. 以单机模式启动
mongod --port 27017 --dbpath /data/db --replSet rs0 --oplogSize 20480
# 3. 连接并删除local数据库中的oplog.rs集合
# 4. 重新以副本集模式启动

2.2 Point-in-Time Recovery(PITR)实现

PITR允许将数据库恢复到任意时间点,是增量备份的终极目标。实现PITR需要两个组件:基础全量备份 + 连续的Oplog。

完整PITR备份流程

#!/bin/bash
# MongoDB PITR备份脚本

BACKUP_BASE="/backup/mongodb/pitr"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_BASE/full_$DATE"
OPLOG_DIR="$BACKUP_BASE/oplog"

# 创建目录
mkdir -p $BACKUP_DIR $OPLOG_DIR

# 1. 执行全量备份(带Oplog)
echo "Starting full backup with oplog..."
mongodump --host mongodb01.example.com --port 27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --oplog \
  --gzip \
  --out $BACKUP_DIR

if [ $? -eq 0 ]; then
  echo "Full backup completed successfully"
  # 记录备份时间戳
  echo $DATE > $BACKUP_BASE/latest_full.txt
else
  echo "Full backup failed!"
  exit 1
fi

# 2. 备份Oplog(持续进行,每小时一次)
# 这个脚本应作为cron任务定期执行
echo "Backing up oplog..."
mongodump --host mongodb01.example.com --port 27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db local --collection oplog.rs \
  --query '{"ts": {"$gte": {"$timestamp": {"t": '$(date +%s)', "i": 1}}}}' \
  --gzip \
  --out $OPLOG_DIR/oplog_$DATE

# 3. 清理旧备份(保留最近7天的全量,3天的oplog)
find $BACKUP_BASE/full_* -type d -mtime +7 -exec rm -rf {} \;
find $OPLOG_DIR/oplog_* -type d -mtime +3 -exec rm -rf {} \;

恢复到特定时间点

假设我们需要恢复到2024-01-15 14:30:00这个时间点,操作步骤如下:

#!/bin/bash
# PITR恢复脚本

RESTORE_TIME="2024-01-15 14:30:00"
BACKUP_BASE="/backup/mongodb/pitr"
LATEST_FULL=$(cat $BACKUP_BASE/latest_full.txt)
FULL_BACKUP_DIR="$BACKUP_BASE/full_$LATEST_FULL"
OPLOG_DIR="$BACKUP_BASE/oplog"

# 1. 恢复基础全量备份
echo "Restoring base full backup..."
mongorestore --host mongodb01.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --gzip \
  --drop \
  $FULL_BACKUP_DIR

# 2. 提取并应用Oplog到目标时间点
# 首先找到需要应用的Oplog范围
OPLOG_FILE="$OPLOG_DIR/oplog_$(date -d "$RESTORE_TIME" +%Y%m%d_%H%M%S)/local/oplog.rs.bson"

# 使用mongorestore的oplogReplay选项
# 注意:需要先将oplog转换为正确的格式
mongorestore --host mongodb01.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --oplogReplay \
  --oplogLimit $(date -d "$RESTORE_TIME" +%s):1 \
  $FULL_BACKUP_DIR

# 3. 如果需要更精确的恢复,可以使用mongo shell手动应用Oplog
# 这种方法适用于复杂场景
mongo --host mongodb01.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin << 'EOF'
use local
// 读取Oplog并应用到目标时间点
var targetTime = new Timestamp(1705315200, 1); // 替换为实际时间戳
db.oplog.rs.find({
  "ts": {"$gte": startTimestamp, "$lte": targetTime}
}).forEach(function(op) {
  // 根据操作类型执行相应操作
  if (op.op === "i") {
    db.getSiblingDB(op.ns.split('.')[0])[op.ns.split('.')[1]].insert(op.o);
  } else if (op.op === "u") {
    db.getSiblingDB(op.ns.split('.')[0])[op.ns.split('.')[1]].update(op.o2, op.o);
  } else if (op.op === "d") {
    db.getSiblingDB(op.ns.split('.')[0])[op.ns.split('.')[1]].remove(op.o2);
  }
});
EOF

2.3 副本集环境下的备份策略

在副本集中,备份应优先在Secondary节点执行,以避免影响Primary节点的性能。

在Secondary节点备份的最佳实践

# 1. 确保Secondary节点数据最新
mongo --host secondary.example.com --port 27017 \
  --eval "db.adminCommand({replSetGetStatus: 1}).members.forEach(m => print(m.name + ': ' + m.optimeDate))"

# 2. 在Secondary节点执行备份(使用--readPreference=secondary)
mongodump --host replicaSet/mongodb01:27017,mongodb02:27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --readPreference=secondary \
  --oplog \
  --gzip \
  --out /backup/mongodb/replica_set/

# 3. 临时将Secondary节点设为standalone进行备份(适用于非常大的数据库)
# 注意:这会暂时中断复制,需要谨慎操作
# 步骤:
# a. 停止Secondary节点的复制
# b. 以单机模式启动
# c. 执行文件系统快照备份
# d. 重新启动并加入副本集

副本集备份的注意事项

  1. 复制延迟监控:备份前检查rs.status()中的optimeDate,确保Secondary节点延迟在可接受范围内(通常<10秒)
  2. 备份窗口选择:在业务低峰期执行,避免长时间占用I/O资源
  3. 多节点轮换:不要总是备份同一个Secondary节点,应轮换以平衡负载
  4. Oplog窗口:确保Oplog有足够的回溯空间,覆盖备份时间+恢复时间

2.4 分片集群环境下的备份

分片集群的备份更为复杂,需要协调多个分片和配置服务器。

分片集群备份策略

#!/bin/bash
# 分片集群备份脚本

# 配置
CONFIG_SERVERS="config1.example.com:27019,config2.example.com:27019,config3.example.com:27019"
SHARDS="shard1.example.com:27018,shard2.example.com:27018,shard3.example.com:27018"
MONGOS="mongos.example.com:27017"
BACKUP_BASE="/backup/mongodb/sharded_cluster"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_BASE/cluster_$DATE"

mkdir -p $BACKUP_DIR

# 1. 备份配置服务器(必须一致)
echo "Backing up config servers..."
mongodump --host $CONFIG_SERVERS \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db config \
  --gzip \
  --out $BACKUP_DIR/config

# 2. 备份每个分片(并行执行)
for SHARD in $(echo $SHARDS | tr ',' ' '); do
  echo "Backing up shard: $SHARD"
  mongodump --host $SHARD \
    --username backupuser --password 'SecurePass123!' \
    --authenticationDatabase admin \
    --oplog \
    --gzip \
    --out $BACKUP_DIR/shard_$(echo $SHARD | cut -d: -f1) &
done
wait

# 3. 记录分片元数据
mongo --host $MONGOS --port 27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "db.adminCommand({listDatabases: 1}).databases.forEach(d => printjson(d))" > $BACKUP_DIR/databases.json

# 4. 记录分片信息
mongo --host $MONGOS --port 27017 \
  --username backupuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "db.adminCommand({listShards: 1}).shards.forEach(s => printjson(s))" > $BACKUP_DIR/shards.json

分片集群恢复流程

分片集群的恢复需要特别注意:

  1. 先恢复配置服务器:确保元数据一致
  2. 再恢复各个分片:每个分片独立恢复
  3. 最后检查均衡器:确保数据分布正常
# 恢复配置服务器
mongorestore --host config1.example.com:27019 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --gzip \
  $BACKUP_DIR/config

# 恢复分片(每个分片独立恢复)
mongorestore --host shard1.example.com:27018 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --gzip \
  $BACKUP_DIR/shard_shard1

# 在Mongos上刷新配置
mongo --host $MONGOS --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "db.adminCommand({flushRouterConfig: 1})"

第三部分:备份自动化与监控

3.1 自动化备份脚本

生产环境需要自动化的备份流程,包括执行、验证、清理和告警。

#!/usr/bin/env python3
"""
MongoDB自动化备份脚本
支持单机、副本集、分片集群模式
"""

import subprocess
import os
import sys
import json
import smtplib
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class MongoDBBackup:
    def __init__(self, config):
        self.config = config
        self.backup_dir = config['backup_dir']
        self.timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        self.backup_path = os.path.join(self.backup_dir, f"backup_{self.timestamp}")
        
    def run_command(self, cmd, check=True):
        """执行命令并记录日志"""
        print(f"[{datetime.now()}] Executing: {' '.join(cmd)}")
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, check=check)
            if result.stdout:
                print(f"STDOUT: {result.stdout}")
            if result.stderr:
                print(f"STDERR: {result.stderr}")
            return result
        except subprocess.CalledProcessError as e:
            self.send_alert(f"Backup command failed: {e}")
            raise
    
    def full_backup(self):
        """执行全量备份"""
        os.makedirs(self.backup_path, exist_ok=True)
        
        cmd = [
            'mongodump',
            '--host', self.config['host'],
            '--port', str(self.config['port']),
            '--username', self.config['username'],
            '--password', self.config['password'],
            '--authenticationDatabase', self.config['auth_db'],
            '--gzip',
            '--out', self.backup_path
        ]
        
        # 如果是副本集,添加oplog
        if self.config.get('replica_set'):
            cmd.extend(['--oplog'])
        
        self.run_command(cmd)
        
        # 验证备份完整性
        self.verify_backup()
        
        return self.backup_path
    
    def incremental_backup(self):
        """基于Oplog的增量备份"""
        oplog_dir = os.path.join(self.backup_dir, 'oplog')
        os.makedirs(oplog_dir, exist_ok=True)
        
        # 获取上次备份的时间戳
        last_ts_file = os.path.join(self.backup_dir, 'last_oplog_ts.txt')
        if os.path.exists(last_ts_file):
            with open(last_ts_file, 'r') as f:
                last_ts = f.read().strip()
        else:
            # 如果没有记录,从1小时前开始
            last_ts = (datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ')
        
        # 备份Oplog
        oplog_file = os.path.join(oplog_dir, f"oplog_{self.timestamp}.bson")
        
        cmd = [
            'mongodump',
            '--host', self.config['host'],
            '--port', str(self.config['port']),
            '--username', self.config['username'],
            '--password', self.config['password'],
            '--authenticationDatabase', self.config['auth_db'],
            '--db', 'local',
            '--collection', 'oplog.rs',
            '--query', f'{{"ts": {{"$gte": {{"$date": "{last_ts}"}}}}}}',
            '--gzip',
            '--out', oplog_dir
        ]
        
        self.run_command(cmd)
        
        # 更新时间戳
        current_ts = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
        with open(last_ts_file, 'w') as f:
            f.write(current_ts)
        
        return oplog_file
    
    def verify_backup(self):
        """验证备份完整性"""
        # 检查备份目录是否存在且非空
        if not os.path.exists(self.backup_path):
            raise Exception("Backup directory not created")
        
        # 检查关键文件
        db_path = os.path.join(self.backup_path, self.config.get('db', 'myapp'))
        if not os.path.exists(db_path):
            raise Exception("Backup database directory missing")
        
        # 检查.bson文件大小
        total_size = 0
        for root, dirs, files in os.walk(self.backup_path):
            for file in files:
                file_path = os.path.join(root, file)
                total_size += os.path.getsize(file_path)
        
        if total_size < 1024:  # 小于1KB认为有问题
            raise Exception("Backup size too small, possible corruption")
        
        print(f"Backup verified successfully. Size: {total_size / (1024*1024):.2f} MB")
    
    def cleanup_old_backups(self):
        """清理旧备份"""
        retention_days = self.config.get('retention_days', 7)
        cutoff_date = datetime.now() - timedelta(days=retention_days)
        
        for item in os.listdir(self.backup_dir):
            item_path = os.path.join(self.backup_dir, item)
            if os.path.isdir(item_path) and item.startswith('backup_'):
                # 提取日期时间
                try:
                    item_date = datetime.strptime(item.replace('backup_', ''), '%Y%m%d_%H%M%S')
                    if item_date < cutoff_date:
                        print(f"Removing old backup: {item}")
                        subprocess.run(['rm', '-rf', item_path])
                except ValueError:
                    continue
        
        # 清理旧的oplog
        oplog_dir = os.path.join(self.backup_dir, 'oplog')
        if os.path.exists(oplog_dir):
            for item in os.listdir(oplog_dir):
                item_path = os.path.join(oplog_dir, item)
                if os.path.isfile(item_path):
                    item_date_str = item.replace('oplog_', '').replace('.bson.gz', '')
                    try:
                        item_date = datetime.strptime(item_date_str, '%Y%m%d_%H%M%S')
                        if item_date < cutoff_date:
                            print(f"Removing old oplog: {item}")
                            os.remove(item_path)
                    except ValueError:
                        continue
    
    def send_alert(self, message):
        """发送告警邮件"""
        if not self.config.get('email'):
            return
        
        msg = MIMEMultipart()
        msg['From'] = self.config['email']['from']
        msg['To'] = ', '.join(self.config['email']['to'])
        msg['Subject'] = f"MongoDB Backup Alert - {self.timestamp}"
        
        body = f"""
        MongoDB Backup Status Report
        ============================
        Time: {self.timestamp}
        Host: {self.config['host']}:{self.config['port']}
        Status: {'SUCCESS' if 'success' in message.lower() else 'FAILED'}
        Message: {message}
        """
        
        msg.attach(MIMEText(body, 'plain'))
        
        try:
            server = smtplib.SMTP(self.config['email']['smtp_server'], 587)
            server.starttls()
            server.login(self.config['email']['smtp_user'], self.config['email']['smtp_password'])
            server.send_message(msg)
            server.quit()
            print("Alert email sent successfully")
        except Exception as e:
            print(f"Failed to send alert: {e}")
    
    def run_full_backup(self):
        """执行完整备份流程"""
        try:
            print(f"Starting backup at {self.timestamp}")
            
            # 执行备份
            backup_path = self.full_backup()
            
            # 清理旧备份
            self.cleanup_old_backups()
            
            # 发送成功通知
            self.send_alert(f"Backup completed successfully: {backup_path}")
            
            print(f"Backup completed: {backup_path}")
            return True
            
        except Exception as e:
            error_msg = f"Backup failed: {str(e)}"
            print(error_msg)
            self.send_alert(error_msg)
            return False

# 配置示例
CONFIG = {
    'host': 'mongodb01.example.com',
    'port': 27017,
    'username': 'backupuser',
    'password': 'SecurePass123!',
    'auth_db': 'admin',
    'backup_dir': '/backup/mongodb/automation',
    'retention_days': 7,
    'replica_set': True,
    'email': {
        'smtp_server': 'smtp.example.com',
        'smtp_user': 'alerts@example.com',
        'smtp_password': 'emailpass',
        'from': 'mongodb-backup@example.com',
        'to': ['dba@example.com', 'ops@example.com']
    }
}

if __name__ == '__main__':
    backup = MongoDBBackup(CONFIG)
    
    if len(sys.argv) > 1 and sys.argv[1] == 'incremental':
        backup.incremental_backup()
    else:
        backup.run_full_backup()

3.2 备份验证与监控

备份不验证等于没有备份。必须建立自动化的验证机制。

#!/bin/bash
# 备份验证脚本

BACKUP_DIR="/backup/mongodb/automation"
RESTORE_DIR="/tmp/backup_verify_$(date +%s)"
mkdir -p $RESTORE_DIR

# 1. 随机选择一个备份进行验证
BACKUP_LIST=($(ls -d $BACKUP_DIR/backup_* 2>/dev/null | shuf -n 1))
if [ ${#BACKUP_LIST[@]} -eq 0 ]; then
  echo "No backups found"
  exit 1
fi

BACKUP_PATH=${BACKUP_LIST[0]}
echo "Verifying backup: $BACKUP_PATH"

# 2. 恢复到临时实例
mongod --port 27018 --dbpath $RESTORE_DIR --logpath $RESTORE_DIR/mongod.log --fork

# 3. 执行恢复
mongorestore --host localhost --port 27018 \
  --gzip \
  --quiet \
  $BACKUP_PATH

if [ $? -eq 0 ]; then
  # 4. 验证数据完整性
  mongo --host localhost --port 27018 --eval "
    var stats = db.adminCommand({listDatabases: 1});
    var totalSize = 0;
    stats.databases.forEach(function(db) {
      totalSize += db.sizeOnDisk;
    });
    print('Total database size: ' + (totalSize / (1024*1024)).toFixed(2) + ' MB');
    
    // 检查几个关键集合
    var dbs = stats.databases.map(db => db.name);
    print('Databases: ' + dbs.join(', '));
    
    // 验证数据可查询
    dbs.forEach(function(dbName) {
      if (dbName !== 'admin' && dbName !== 'local' && dbName !== 'config') {
        var collections = db.getSiblingDB(dbName).getCollectionNames();
        if (collections.length > 0) {
          var sample = db.getSiblingDB(dbName)[collections[0]].findOne();
          if (sample) {
            print('Sample data from ' + dbName + '.' + collections[0] + ': ' + JSON.stringify(sample._id));
          }
        }
      }
    });
  "
  
  VERIFY_RESULT=$?
else
  VERIFY_RESULT=1
fi

# 5. 清理临时实例
mongod --port 27018 --shutdown
rm -rf $RESTORE_DIR

# 6. 报告结果
if [ $VERIFY_RESULT -eq 0 ]; then
  echo "Backup verification PASSED"
  exit 0
else
  echo "Backup verification FAILED"
  exit 1
fi

3.3 监控指标与告警

关键监控指标:

  • 备份成功率:最近24小时备份成功率应>99%
  • 备份时长:全量备份时间应<备份窗口,增量备份应分钟
  • 备份大小:监控备份大小变化,异常增长可能表示数据问题
  • Oplog窗口:确保Oplog能覆盖至少2倍的备份间隔
  • 存储空间:备份目录可用空间应>30%
# 监控脚本示例
#!/bin/bash
# MongoDB备份监控

# 检查最近一次备份是否成功
LAST_BACKUP=$(ls -t /backup/mongodb/automation/backup_* 2>/dev/null | head -1)
if [ -z "$LAST_BACKUP" ]; then
  echo "CRITICAL: No backups found"
  exit 2
fi

# 检查备份时间(应不超过24小时)
BACKUP_AGE=$(($(date +%s) - $(stat -c %Y "$LAST_BACKUP")))
if [ $BACKUP_AGE -gt 86400 ]; then
  echo "WARNING: Last backup is older than 24 hours"
  exit 1
fi

# 检查备份大小(应大于1MB)
BACKUP_SIZE=$(du -sm "$LAST_BACKUP" | cut -f1)
if [ $BACKUP_SIZE -lt 1 ]; then
  echo "CRITICAL: Backup size too small"
  exit 2
fi

# 检查Oplog窗口
OPLOG_INFO=$(mongo --quiet --eval "db.adminCommand({getReplicationInfo: 1})" 2>/dev/null)
if [ $? -eq 0 ]; then
  OPLOG_SIZE=$(echo "$OPLOG_INFO" | grep "logSizeMB" | awk '{print $3}')
  if [ "$OPLOG_SIZE" -lt 1024 ]; then
    echo "WARNING: Oplog size is small ($OPLOG_SIZE MB)"
    exit 1
  fi
fi

echo "OK: Backup healthy - Age: $((BACKUP_AGE/3600))h, Size: ${BACKUP_SIZE}MB"
exit 0

第四部分:实战案例与最佳实践

4.1 案例1:从备份恢复误删除数据

场景:用户在14:25误删除了users集合中的1000条记录,需要恢复到14:20的状态。

解决方案

# 1. 确定恢复时间点:14:20
RESTORE_TIME="2024-01-15 14:20:00"

# 2. 找到最近的全量备份(假设是今天凌晨3点)
FULL_BACKUP="/backup/mongodb/pitr/full_20240115_030000"

# 3. 恢复全量备份到临时数据库
mongorestore --host localhost --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp_restore \
  --gzip \
  $FULL_BACKUP/myapp/

# 4. 提取并应用Oplog到14:20
# 首先找到需要的Oplog范围
OPLOG_START="2024-01-15T03:00:00Z"  # 全量备份时间
OPLOG_END="2024-01-15T14:20:00Z"    # 恢复目标时间

# 使用mongorestore的oplogLimit参数
mongorestore --host localhost --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp_restore \
  --oplogReplay \
  --oplogLimit $(date -d "$OPLOG_END" +%s):1 \
  $FULL_BACKUP/myapp/

# 5. 从临时数据库中导出需要恢复的集合
mongodump --host localhost --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp_restore \
  --collection users \
  --out /tmp/users_restore/

# 6. 恢复到生产数据库(使用--mode=upsert避免影响其他数据)
mongorestore --host localhost --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --db myapp \
  --collection users \
  --mode=upsert \
  /tmp/users_restore/myapp_restore/users.bson

# 7. 验证恢复结果
mongo --host localhost --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "db.myapp.users.count()"

4.2 案例2:分片集群灾难恢复

场景:整个分片集群所在机房断电,需要在异地灾备中心恢复。

解决方案

# 1. 准备灾备环境
# 在灾备中心启动所有MongoDB服务
# 配置网络和认证

# 2. 从异地存储获取备份
aws s3 sync s3://mongodb-backups/production/ /backup/mongodb/production/

# 3. 恢复配置服务器(必须最先恢复)
for config in config1 config2 config3; do
  mongorestore --host $config.example.com --port 27019 \
    --username restoreuser --password 'SecurePass123!' \
    --authenticationDatabase admin \
    --gzip \
    /backup/mongodb/production/cluster_20240115_030000/config/
done

# 4. 恢复各个分片(并行)
for shard in shard1 shard2 shard3; do
  mongorestore --host $shard.example.com --port 27018 \
    --username restoreuser --password 'SecurePass123!' \
    --authenticationDatabase admin \
    --gzip \
    /backup/mongodb/production/cluster_20240115_030000/${shard}_*/ &
done
wait

# 5. 启动Mongos并验证
mongos --configdb configReplSet/config1:27019,config2:27019,config3:27019 \
  --bind_ip_all --port 27017

# 6. 检查集群状态
mongo --host mongos.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "
    // 检查分片状态
    db.adminCommand({listShards: 1}).shards.forEach(s => {
      print('Shard: ' + s._id + ' - Host: ' + s.host);
    });
    
    // 检查数据库分布
    db.adminCommand({listDatabases: 1}).databases.forEach(db => {
      if (db.name !== 'admin' && db.name !== 'local' && db.name !== 'config') {
        print('Database: ' + db.name + ' - Size: ' + (db.sizeOnDisk/1024/1024).toFixed(2) + ' MB');
      }
    });
    
    // 检查均衡器状态
    var balancer = db.adminCommand({balancerStatus: 1});
    print('Balancer enabled: ' + balancer.mode);
  "

# 7. 重新启用均衡器(如果需要)
mongo --host mongos.example.com --port 27017 \
  --username restoreuser --password 'SecurePass123!' \
  --authenticationDatabase admin \
  --eval "db.adminCommand({balancerStart: 1})"

4.3 最佳实践总结

  1. 3-2-1备份原则

    • 3份数据副本
    • 2种不同存储介质
    • 1份异地备份
  2. 备份测试频率

    • 每月至少执行一次完整的恢复演练
    • 每周验证备份完整性
    • 每日检查备份状态
  3. 安全考虑

    • 备份文件加密(使用GPG或AWS KMS)
    • 备份用户权限最小化(仅backup/restore角色)
    • 备份存储访问控制(IP白名单、VPC隔离)
  4. 性能优化

    • 使用--numInsertionWorkersPerCollection加速恢复
    • 对大型集合使用--batchSize参数
    • 考虑使用--maintainInsertionOrder保持数据插入顺序
  5. 文档与流程

    • 维护详细的恢复操作手册
    • 记录所有备份配置和变更
    • 建立on-call响应流程

结论

MongoDB备份策略需要根据业务需求、数据规模和部署架构量身定制。从基础的mongodump到复杂的PITR,再到分片集群的协调备份,每个环节都需要精心设计和持续优化。记住,备份的价值只有在恢复时才能体现,因此定期测试和演练是确保备份有效性的关键。通过本文介绍的方法和工具,您可以构建一个可靠、高效、安全的MongoDB数据保护体系,为业务连续性提供坚实保障。