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

在当今数据驱动的世界中,数据库备份是保障业务连续性的关键环节。MongoDB作为最流行的NoSQL数据库之一,虽然具备高可用性和容错能力,但仍然面临硬件故障、人为误操作、恶意攻击等多种数据丢失风险。一套完善的备份策略不仅能防止数据丢失,还能确保在灾难发生时能够快速恢复业务。

本文将全面解析MongoDB的备份策略,从基础的mongodump工具使用,到高级的自动化备份方案,涵盖单机部署、副本集和分片集群等不同场景,帮助您构建可靠的数据保护体系。

一、MongoDB备份基础:mongodump工具详解

1.1 mongodump工作原理

mongodump是MongoDB官方提供的逻辑备份工具,它通过连接到MongoDB服务器,读取集合数据并导出为BSON格式文件。与物理备份相比,mongodump具有以下特点:

  • 逻辑备份:导出的是数据的逻辑表示,而非磁盘上的物理文件
  • 跨平台/版本兼容:导出的数据可以在不同平台和MongoDB版本间迁移
  • 选择性备份:可以按数据库、集合甚至查询条件进行部分备份
  • 性能影响:在备份过程中会读取数据库,对线上业务有一定性能影响

1.2 基础备份命令与参数详解

1.2.1 备份整个实例

# 备份所有数据库(需要管理员权限)
mongodump --host localhost --port 27017 --username admin --password "yourpassword" --authenticationDatabase admin --out /backup/mongodb/$(date +%Y%m%d)

参数说明

  • --host:MongoDB服务器地址
  • --port:MongoDB服务器端口
  • --username:数据库用户名
  • --password:用户密码
  • --authenticationDatabase:用户认证数据库
  • --out:备份输出目录,这里使用日期作为子目录名

1.2.2 备份指定数据库

# 备份单个数据库
mongodump --db myapp --out /backup/mongodb/myapp_$(date +%Y%m%d)

# 备份多个数据库(用逗号分隔)
mongodump --db myapp,test --out /backup/mongodb/multi_$(date +%Y%m%d)

1.2.3 备份指定集合

# 备份单个集合
mongodump --db myapp --collection users --out /backup/mongodb/users_$(date +%Y%m%d)

# 备份多个集合
mongodump --db myapp --collection users,orders --out /backup/mongodb/collections_$(date +%Y%m%d)

1.2.4 带查询条件的备份

# 只备份符合条件的文档
mongodump --db myapp --collection users --query '{"status":"active","last_login":{"$gte":{"$date":"2023-01-01T00:00:00Z"}}}' --out /backup/mongodb/active_users_$(date +%Y%m%d)

查询条件说明

  • 使用JSON格式指定查询条件
  • 日期格式需要使用{"$date": "ISO日期字符串"}
  • 支持所有MongoDB查询操作符

1.2.5 增量备份策略

MongoDB本身不支持增量备份,但可以通过以下方式实现:

# 1. 首次全量备份
mongodump --oplog --out /backup/mongodb/full_$(date +%Y%m%d)

# 2. 后续增量备份(利用oplog)
# 注意:增量备份需要配合oplog窗口期,确保在两次备份之间oplog未被覆盖

# 3. 恢复时需要按顺序应用oplog
mongorestore --oplogReplay --oplogLimit "2023-12-01T12:00:00Z" /backup/mongodb/full_20231201

重要提示

  • --oplog参数用于创建具有时间点一致性的备份
  • 增量备份需要精确控制oplog窗口,适合有固定oplog保留时间的环境
  • 生产环境建议使用更可靠的WAL归档或Percona Backup for MongoDB

1.3 mongorestore恢复工具详解

1.3.1 基础恢复命令

# 恢复整个备份
mongorestore --host localhost --port 27017 --username admin --password "yourpassword" --authenticationDatabase admin /backup/mongodb/20231201

# 恢复指定数据库
mongorestore --db myapp /backup/mongodb/20231201/myapp

# 恢复指定集合
mongorestore --db myapp --collection users /backup/mongodb/20231201/myapp/users.bson

1.3.2 高级恢复选项

# 恢复时重命名数据库
mongorestore --nsFrom "myapp.*" --nsTo "myapp_restore.*" /backup/mongodb/20231201

# 恢复时忽略索引(加快恢复速度,后续再重建索引)
mongorestore --noIndexRestore /backup/mongodb/20231201

# 恢复时使用多个线程加速
mongorestore --numInsertionWorkersPerCollection 4 --batchSize 1000 /backup/mongodb/20231201

# 恢复oplog(实现时间点恢复)
mongorestore --oplogReplay --oplogLimit "2023-12-01T12:00:00Z" /backup/mongodb/20231201

1.3.3 恢复验证

# 恢复后验证数据完整性
mongosh --eval "
db.adminCommand({listDatabases:1}).databases.forEach(function(db){
    var colls = db.getSiblingDB(db.name).getCollectionNames();
    colls.forEach(function(coll){
        if(coll.indexOf('system.') === -1){
            var count = db.getSiblingDB(db.name)[coll].countDocuments();
            print(db.name + '.' + coll + ': ' + count + ' documents');
        }
    });
});
"

二、物理备份策略:文件系统快照

2.1 物理备份的优势与适用场景

物理备份直接复制MongoDB的数据文件(/data/db目录),相比逻辑备份具有以下优势:

  • 速度快:直接文件复制,无需解析数据
  • 性能影响小:对线上业务影响较小
  • 适合大数据量:TB级数据备份效率高
  • 恢复快:直接恢复文件,无需数据导入

适用场景

  • 大数据量环境(TB级别)
  • 对备份恢复时间要求严格(RTO短)
  • 数据库文件位于支持快照的存储系统上

2.2 文件系统快照备份

2.2.1 使用LVM快照(Linux)

# 1. 确保MongoDB数据目录在LVM卷上
# 查看当前LVM信息
sudo lvdisplay
sudo vgdisplay

# 2. 创建一致性快照(需要先锁定数据库)
mongosh --eval "db.fsyncLock()"

# 3. 创建LVM快照(假设数据卷为/dev/vg0/mongo_data)
sudo lvcreate -L 10G -s -n mongo_snap /dev/vg0/mongo_data

# 4. 解锁数据库
mongosh --eval "db.fsyncUnlock()"

# 5. 挂载快照并复制数据
sudo mount /dev/vg0/mongo_snap /mnt/mongo_snapshot
sudo rsync -av /mnt/mongo_snapshot/ /backup/mongodb/snapshot_$(date +%Y%m%d)/

# 6. 清理快照
sudo umount /mnt/mongo_snapshot
sudo lvremove -f /dev/vg0/mongo_snap

2.2.2 使用AWS EBS快照

# 1. 获取MongoDB数据卷的Volume ID
aws ec2 describe-instances --instance-ids i-1234567890abcdef0 \
  --query "Reservations[0].Instances[0].BlockDeviceMappings[?DeviceName=='/dev/sdf'].Ebs.VolumeId" \
  --output text

# 2. 创建一致性快照(需要先锁定数据库)
mongosh --eval "db.fsyncLock()"

# 3. 创建EBS快照
aws ec2 create-snapshot --volume-id vol-0123456789abcdef0 \
  --description "MongoDB backup $(date +%Y%m%d)" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=BackupType,Value=Daily},{Key=Environment,Value=Production}]'

# 4. 等待快照完成
aws ec2 wait snapshot-completed --snapshot-ids snap-0123456789abcdef0

# 5. 解锁数据库
mongosh --eval "db.fsyncUnlock()"

# 6. 清理旧快照(保留最近7天)
aws ec2 describe-snapshots --filters "Name=tag:BackupType,Values=Daily" \
  --query "Snapshots[?StartTime<='$(date -d '7 days ago' +%Y-%m-%d)'].SnapshotId" \
  --output text | xargs -I {} aws ec2 delete-snapshot --snapshot-id {}

2.2.3 使用文件系统快照工具(如ZFS)

# ZFS快照备份(假设数据集为tank/mongodb)
# 1. 创建快照
zfs snapshot tank/mongodb@backup_$(date +%Y%m%d)

# 2. 发送快照到备份服务器
zfs send tank/mongodb@backup_$(date +%Y%m%d) | ssh backup-server "zfs receive tank/mongodb_backup"

# 3. 删除旧快照(保留最近7天)
for snap in $(zfs list -t snapshot -o name | grep 'tank/mongodb@backup_' | head -n -7); do
    zfs destroy $snap
done

2.3 物理备份的恢复

2.3.1 基本恢复步骤

# 1. 停止MongoDB服务
sudo systemctl stop mongod

# 2. 备份当前数据目录(如果需要)
sudo mv /data/db /data/db.bak

# 3. 复制备份文件到数据目录
sudo rsync -av /backup/mongodb/snapshot_20231201/ /data/db/

# 4. 修复数据库(可选,确保数据一致性)
sudo mongod --repair --dbpath /data/db

# 5. 启动MongoDB
sudo systemctl start mongod

# 6. 验证数据
mongosh --eval "db.adminCommand({listDatabases:1})"

2.3.2 跨平台/版本恢复注意事项

# 如果备份来自不同版本的MongoDB,可能需要使用mongodump/mongorestore进行迁移
# 1. 从物理备份启动临时实例
sudo mongod --dbpath /backup/mongodb/snapshot_20231201 --port 27018 --fork --logpath /tmp/mongod_restore.log

# 2. 使用mongodump导出数据
mongodump --host localhost --port 27018 --out /tmp/mongodb_dump

# 3. 停止临时实例
sudo mongod --dbpath /backup/mongodb/snapshot_20231201 --port 27018 --shutdown

# 4. 使用mongorestore导入到新版本
mongorestore --host localhost --port 27017 /tmp/mongodb_dump

三、副本集备份策略

3.1 副本集备份的最佳实践

在副本集环境中,备份策略需要特别考虑:

  • 从Secondary节点备份:避免影响Primary节点的业务
  • 读隔离(Read Concern):确保备份的数据一致性
  • Oplog窗口:确保备份期间oplog不会被覆盖

3.2 从Secondary节点备份

# 1. 识别副本集中的Secondary节点
mongosh --eval "rs.status().members.forEach(m => print(m.name + ': ' + m.stateStr))"

# 2. 从Secondary节点执行备份(使用--readPreference=secondary)
mongodump --host secondary-host --port 27017 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --readPreference=secondary \
  --out /backup/mongodb/replica_set_$(date +%Y%m%d)

3.3 使用副本集Oplog进行时间点恢复

# 1. 创建带Oplog的备份
mongodump --host secondary-host --port 27017 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --readPreference=secondary \
  --oplog \
  --out /backup/mongodb/replica_set_oplog_$(date +%Y%m%d)

# 2. 恢复时应用Oplog
mongorestore --host localhost --port 27017 \
  --username admin --password "adminpass" --authenticationDatabase admin \
  --oplogReplay \
  --oplogLimit "2023-12-01T12:00:00Z" \
  /backup/mongodb/replica_set_oplog_20231201

3.4 副本集备份脚本示例

#!/bin/bash
# 副本集备份脚本:replica_backup.sh

# 配置
BACKUP_BASE="/backup/mongodb"
DATE=$(date +%Y%m%d_%H%M%S)
REPLICA_NAME="rs0"
SECONDARY_HOST="secondary.example.com"
PORT=27017
USER="backupuser"
PASS="backupPass"
RETENTION_DAYS=7

# 创建备份目录
BACKUP_DIR="${BACKUP_BASE}/${REPLICA_NAME}_${DATE}"
mkdir -p "$BACKUP_DIR"

# 检查节点状态
NODE_STATUS=$(mongosh --host "$SECONDARY_HOST" --port "$PORT" --eval "
    var status = rs.status();
    var myMember = status.members.find(m => m.name.includes('$SECONDARY_HOST'));
    if (!myMember) {
        print('ERROR: Node not found in replica set');
        quit(1);
    }
    if (myMember.stateStr !== 'SECONDARY') {
        print('ERROR: Node is not SECONDARY. Current state: ' + myMember.stateStr);
        quit(1);
    }
    print('OK: Node is SECONDARY');
" --quiet)

if [ $? -ne 0 ]; then
    echo "Node status check failed: $NODE_STATUS"
    exit 1
fi

# 执行备份
echo "Starting backup to $BACKUP_DIR"
mongodump --host "$SECONDARY_HOST" --port "$PORT" \
  --username "$USER" --password "$PASS" --authenticationDatabase admin \
  --readPreference=secondary \
  --oplog \
  --out "$BACKUP_DIR"

if [ $? -eq 0 ]; then
    echo "Backup completed successfully"
    
    # 压缩备份
    echo "Compressing backup..."
    tar -czf "${BACKUP_DIR}.tar.gz" -C "$BACKUP_BASE" "${REPLICA_NAME}_${DATE}"
    rm -rf "$BACKUP_DIR"
    
    # 清理旧备份
    echo "Cleaning up old backups..."
    find "$BACKUP_BASE" -name "${REPLICA_NAME}_*.tar.gz" -mtime +$RETENTION_DAYS -delete
    
    echo "Backup process completed: ${BACKUP_DIR}.tar.gz"
else
    echo "Backup failed!"
    exit 1
fi

四、分片集群备份策略

4.1 分片集群架构回顾

MongoDB分片集群包含以下组件:

  • Config Servers:存储元数据
  • Shards:实际数据分片
  • Mongos:路由节点

备份分片集群需要协调所有组件,确保全局一致性。

4.2 分片集群备份步骤

4.2.1 备份Config Servers

# Config Servers必须作为副本集部署,从Secondary备份
mongodump --host config-server-secondary --port 27019 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --readPreference=secondary \
  --oplog \
  --out /backup/mongodb/config_$(date +%Y%m%d)

4.2.2 备份每个Shard

# 对每个分片副本集执行备份
for SHARD in shard1 shard2 shard3; do
    # 找到分片的Secondary节点
    SECONDARY=$(mongosh --host mongos --port 27017 --eval "
        db.adminCommand({listShards:1}).shards.forEach(s => {
            if (s._id === '$SHARD') {
                // 连接到分片副本集获取Secondary
                var rsStatus = new Mongo(s.host).getDB('admin').rs.status();
                var secondary = rsStatus.members.find(m => m.stateStr === 'SECONDARY');
                if (secondary) print(secondary.name);
            }
        });
    " --quiet)
    
    if [ -n "$SECONDARY" ]; then
        mongodump --host "$SECONDARY" --port 27018 \
          --username backupuser --password "backupPass" --authenticationDatabase admin \
          --readPreference=secondary \
          --oplog \
          --out /backup/mongodb/${SHARD}_$(date +%Y%m%d)
    fi
done

4.2.3 备份Mongos(可选)

Mongos本身不存储数据,但可以备份其配置:

# 备份Mongos配置(主要是路由规则)
mongodump --host mongos --port 27017 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --db config --collection chunks \
  --out /backup/mongodb/mongos_config_$(date +%Y%m%d)

4.3 分片集群恢复

分片集群恢复需要按特定顺序进行:

# 1. 停止所有Mongos
sudo systemctl stop mongos

# 2. 恢复Config Servers
mongorestore --host config-server-primary --port 27019 \
  --username admin --password "adminpass" --authenticationDatabase admin \
  --oplogReplay \
  /backup/mongodb/config_20231201

# 3. 恢复每个Shard
for SHARD in shard1 shard2 shard3; do
    mongorestore --host ${SHARD}-primary --port 27018 \
      --username admin --password "adminpass" --authenticationDatabase admin \
      --oplogReplay \
      /backup/mongodb/${SHARD}_20231201
done

# 4. 启动Mongos
sudo systemctl start mongos

# 5. 验证集群状态
mongosh --host mongos --port 27017 --eval "db.adminCommand({listShards:1})"

五、高级自动化备份方案

5.1 Percona Backup for MongoDB(PBM)

Percona Backup for MongoDB是MongoDB的开源备份管理工具,支持增量备份、时间点恢复和多云备份。

5.1.1 安装与配置

# 1. 安装PBM
wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb
sudo dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb
sudo apt-get update
sudo apt-get install percona-backup-mongodb

# 2. 配置PBM(在每个节点上)
sudo systemctl stop mongod

# 3. 配置PBM连接字符串
sudo tee /etc/pbm-agent.conf <<EOF
connection:
  host: localhost
  port: 27017
  user: pbmuser
  password: "pbmpass"
storage:
  type: filesystem
  filesystem:
    path: /backup/pbm
EOF

# 4. 创建PBM用户(在MongoDB中)
mongosh --eval "
db.getSiblingDB('admin').createUser({
    user: 'pbmuser',
    pwd: 'pbmpass',
    roles: [
        {role: 'backup', db: 'admin'},
        {role: 'clusterMonitor', db: 'admin'},
        {role: 'readAnyDatabase', db: 'admin'},
        {role: 'readWrite', db: 'local'}
    ]
});
"

# 5. 启动PBM Agent
sudo systemctl start pbm-agent

5.1.2 使用PBM进行备份

# 1. 配置备份存储(S3示例)
pbm config --file /dev/stdin <<EOF
storage:
  type: s3
  s3:
    region: us-east-1
    bucket: my-mongodb-backups
    prefix: production
    credentials:
      access-key-id: AKIAIOSFODNN7EXAMPLE
      secret-access-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
EOF

# 2. 执行备份
pbm backup --type=full --compression=gzip

# 3. 查看备份列表
pbm list

# 4. 恢复备份
pbm restore 2023-12-01T12:00:00Z

# 5. 时间点恢复(PITR)
pbm config --file /dev/stdin <<EOF
pitr:
  enabled: true
  oplogSpanMin: 10
EOF

# 6. 执行PITR恢复
pbm restore --time="2023-12-01T12:30:00"

5.2 自动化脚本与Cron任务

5.2.1 完整的自动化备份脚本

#!/bin/bash
# MongoDB自动化备份脚本:mongo_backup_auto.sh

# 配置
BACKUP_BASE="/backup/mongodb"
DATE=$(date +%Y%m%d_%H%M%S)
HOST="localhost"
PORT=27017
USER="backupuser"
PASS="backupPass"
AUTH_DB="admin"
RETENTION_DAYS=7
COMPRESS=true
LOG_FILE="/var/log/mongodb_backup.log"

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 错误处理
error_exit() {
    log "ERROR: $1"
    exit 1
}

# 检查MongoDB连接
check_mongodb() {
    log "Checking MongoDB connection..."
    if ! mongosh --host "$HOST" --port "$PORT" --username "$USER" --password "$PASS" --authenticationDatabase "$AUTH_DB" --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; then
        error_exit "Cannot connect to MongoDB"
    fi
    log "MongoDB connection OK"
}

# 检查磁盘空间
check_disk_space() {
    log "Checking disk space..."
    AVAILABLE=$(df "$BACKUP_BASE" | awk 'NR==2 {print $4}')
    # 估算需要的空间(这里简化,实际应根据数据大小调整)
    REQUIRED=$((10 * 1024 * 1024)) # 10GB
    if [ "$AVAILABLE" -lt "$REQUIRED" ]; then
        error_exit "Insufficient disk space. Available: ${AVAILABLE}KB, Required: ${REQUIRED}KB"
    fi
    log "Disk space OK"
}

# 执行备份
perform_backup() {
    local backup_dir="${BACKUP_BASE}/${DATE}"
    mkdir -p "$backup_dir"
    
    log "Starting backup to $backup_dir"
    
    # 获取副本集状态
    REPLICA_STATUS=$(mongosh --host "$HOST" --port "$PORT" --username "$USER" --password "$PASS" --authenticationDatabase "$AUTH_DB" --eval "
        try {
            var status = db.adminCommand({replSetGetStatus:1});
            if (status.ok) {
                var primary = status.members.find(m => m.stateStr === 'PRIMARY');
                var secondary = status.members.find(m => m.stateStr === 'SECONDARY');
                if (secondary) {
                    print('SECONDARY:' + secondary.name);
                } else if (primary) {
                    print('PRIMARY:' + primary.name);
                } else {
                    print('STANDALONE:' + '$HOST');
                }
            } else {
                print('STANDALONE:' + '$HOST');
            }
        } catch (e) {
            print('STANDALONE:' + '$HOST');
        }
    " --quiet)
    
    # 确定备份源
    if [[ "$REPLICA_STATUS" =~ ^SECONDARY: ]]; then
        BACKUP_HOST="${REPLICA_STATUS#SECONDARY:}"
        READ_PREF="--readPreference=secondary"
        log "Using SECONDARY node: $BACKUP_HOST"
    elif [[ "$REPLICA_STATUS" =~ ^PRIMARY: ]]; then
        BACKUP_HOST="${REPLICA_STATUS#PRIMARY:}"
        READ_PREF=""
        log "Using PRIMARY node: $BACKUP_HOST (no secondary available)"
    else
        BACKUP_HOST="$HOST"
        READ_PREF=""
        log "Using standalone node: $BACKUP_HOST"
    fi
    
    # 执行备份
    if mongodump --host "$BACKUP_HOST" --port "$PORT" \
      --username "$USER" --password "$PASS" --authenticationDatabase "$AUTH_DB" \
      $READ_PREF \
      --oplog \
      --out "$backup_dir" 2>> "$LOG_FILE"; then
        log "Backup completed successfully"
    else
        error_exit "Backup failed"
    fi
    
    # 压缩
    if [ "$COMPRESS" = true ]; then
        log "Compressing backup..."
        if tar -czf "${backup_dir}.tar.gz" -C "$BACKUP_BASE" "$DATE" 2>> "$LOG_FILE"; then
            rm -rf "$backup_dir"
            log "Compression completed: ${backup_dir}.tar.gz"
            BACKUP_FILE="${backup_dir}.tar.gz"
        else
            error_exit "Compression failed"
        fi
    else
        BACKUP_FILE="$backup_dir"
    fi
}

# 清理旧备份
cleanup_old_backups() {
    log "Cleaning up backups older than $RETENTION_DAYS days..."
    find "$BACKUP_BASE" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete 2>> "$LOG_FILE"
    find "$BACKUP_BASE" -mindepth 1 -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>> "$LOG_FILE"
    log "Cleanup completed"
}

# 发送通知(可选)
send_notification() {
    local status=$1
    local message=$2
    
    # 邮件通知(需要配置mailx或sendmail)
    if command -v mailx &> /dev/null; then
        echo "$message" | mailx -s "MongoDB Backup $status: $DATE" admin@example.com
    fi
    
    # Slack通知(需要配置SLACK_WEBHOOK)
    if [ -n "$SLACK_WEBHOOK" ]; then
        curl -X POST -H 'Content-type: application/json' \
          --data "{\"text\":\"MongoDB Backup $status: $DATE\n$message\"}" \
          "$SLACK_WEBHOOK"
    fi
}

# 主函数
main() {
    log "=== MongoDB Backup Started ==="
    
    check_mongodb
    check_disk_space
    perform_backup
    cleanup_old_backups
    
    log "=== MongoDB Backup Completed Successfully ==="
    send_notification "SUCCESS" "Backup completed: $BACKUP_FILE"
}

# 错误处理
trap 'error_exit "Script interrupted"' INT TERM

# 执行主函数
main "$@"

5.2.2 Cron任务配置

# 编辑crontab
crontab -e

# 添加以下行(每天凌晨2点执行)
0 2 * * * /usr/local/bin/mongo_backup_auto.sh

# 每周日执行完整备份,其他时间增量(使用PBM)
0 2 * * 0 /usr/local/bin/pbm_backup_full.sh
0 2 * * 1-6 /usr/local/bin/pbm_backup_incremental.sh

# 每小时检查备份状态
0 * * * * /usr/local/bin/check_backup_status.sh

5.2.3 备份监控与告警脚本

#!/bin/bash
# 备份监控脚本:check_backup_status.sh

BACKUP_BASE="/backup/mongodb"
LOG_FILE="/var/log/mongodb_backup.log"
ALERT_EMAIL="admin@example.com"

# 检查最近24小时是否有成功备份
find "$BACKUP_BASE" -name "*.tar.gz" -mtime -1 | grep -q .
if [ $? -ne 0 ]; then
    echo "CRITICAL: No successful backup found in last 24 hours" | mailx -s "MongoDB Backup Alert" "$ALERT_EMAIL"
    exit 1
fi

# 检查备份文件大小(异常告警)
LATEST_BACKUP=$(find "$BACKUP_BASE" -name "*.tar.gz" -mtime -1 | head -1)
if [ -n "$LATEST_BACKUP" ]; then
    SIZE=$(stat -c%s "$LATEST_BACKUP")
    # 如果备份文件小于100MB,可能有问题
    if [ "$SIZE" -lt 104857600 ]; then
        echo "WARNING: Latest backup size is suspiciously small: $SIZE bytes" | mailx -s "MongoDB Backup Warning" "$ALERT_EMAIL"
    fi
fi

# 检查日志中的错误
if grep -q "ERROR" "$LOG_FILE" | tail -100; then
    echo "ERROR found in backup log" | mailx -s "MongoDB Backup Error" "$ALERT_EMAIL"
    exit 1
fi

echo "Backup status OK"
exit 0

六、备份验证与恢复测试

6.1 备份验证的重要性

备份不测试等于没有备份。定期验证备份的完整性和可恢复性至关重要。

6.2 自动化验证脚本

#!/bin/bash
# 备份验证脚本:verify_backup.sh

BACKUP_BASE="/backup/mongodb"
VERIFY_DIR="/tmp/mongodb_verify"
MONGOD_PORT=27020

# 清理旧验证环境
cleanup() {
    if [ -d "$VERIFY_DIR" ]; then
        sudo rm -rf "$VERIFY_DIR"
    fi
    if ps -p $MONGOD_PID > /dev/null 2>&1; then
        sudo kill $MONGOD_PID 2>/dev/null
        sleep 2
        sudo pkill -f "mongod.*port.*$MONGOD_PORT"
    fi
}

trap cleanup EXIT

# 选择最新备份
LATEST_BACKUP=$(find "$BACKUP_BASE" -name "*.tar.gz" -mtime -1 | sort | tail -1)
if [ -z "$LATEST_BACKUP" ]; then
    echo "No backup found to verify"
    exit 1
fi

echo "Verifying backup: $LATEST_BACKUP"

# 创建验证目录
mkdir -p "$VERIFY_DIR"
mkdir -p "$VERIFY_DIR/db"

# 解压备份
echo "Extracting backup..."
if ! tar -xzf "$LATEST_BACKUP" -C "$VERIFY_DIR" 2>/dev/null; then
    echo "FAILED: Cannot extract backup"
    exit 1
fi

# 找到解压后的BSON文件目录
EXTRACTED_DIR=$(find "$VERIFY_DIR" -name "*.bson" -type f | head -1 | xargs dirname)
if [ -z "$EXTRACTED_DIR" ]; then
    echo "FAILED: No BSON files found in backup"
    exit 1
fi

# 启动临时MongoDB实例
echo "Starting temporary MongoDB instance on port $MONGOD_PORT..."
sudo mongod --dbpath "$VERIFY_DIR/db" --port $MONGOD_PORT --fork --logpath "$VERIFY_DIR/mongod.log" --bind_ip 127.0.0.1
MONGOD_PID=$(cat "$VERIFY_DIR/db/mongod.lock" 2>/dev/null)

# 等待MongoDB启动
sleep 5

# 恢复数据
echo "Restoring data to temporary instance..."
if ! mongorestore --host 127.0.0.1 --port $MONGOD_PORT "$EXTRACTED_DIR" 2>/dev/null; then
    echo "FAILED: Restore failed"
    exit 1
fi

# 验证数据
echo "Verifying data integrity..."
VERIFICATION_RESULT=$(mongosh --host 127.0.0.1 --port $MONGOD_PORT --eval "
    var errors = [];
    
    // 检查数据库列表
    var dbs = db.adminCommand({listDatabases:1}).databases;
    if (dbs.length === 0) errors.push('No databases found');
    
    // 检查每个数据库的集合
    dbs.forEach(function(db) {
        if (db.name === 'admin' || db.name === 'local' || db.name === 'config') return;
        
        var colls = db.getSiblingDB(db.name).getCollectionNames();
        colls.forEach(function(coll) {
            if (coll.indexOf('system.') === -1) {
                var count = db.getSiblingDB(db.name)[coll].countDocuments();
                if (count === 0) {
                    errors.push('Empty collection: ' + db.name + '.' + coll);
                }
            }
        });
    });
    
    if (errors.length > 0) {
        print('VERIFICATION FAILED:');
        errors.forEach(function(err) { print('  - ' + err); });
        quit(1);
    } else {
        print('VERIFICATION SUCCESS: All databases and collections have data');
        quit(0);
    }
" --quiet)

if [ $? -eq 0 ]; then
    echo "Backup verification PASSED"
    exit 0
else
    echo "$VERIFICATION_RESULT"
    exit 1
fi

6.3 定期恢复测试计划

#!/bin/bash
# 每月恢复测试脚本:monthly_restore_test.sh

# 配置
BACKUP_BASE="/backup/mongodb"
TEST_ENV="/tmp/mongodb_restore_test"
TEST_PORT=27021
TEST_DB="restore_test_db"

# 创建测试环境
mkdir -p "$TEST_ENV/db"
mkdir -p "$TEST_ENV/restore"

# 选择一个随机备份(至少30天前)
BACKUP=$(find "$BACKUP_BASE" -name "*.tar.gz" -mtime +30 | shuf -n 1)
if [ -z "$BACKUP" ]; then
    echo "No suitable backup found for monthly test"
    exit 1
fi

echo "Monthly restore test using: $BACKUP"

# 解压备份
tar -xzf "$BACKUP" -C "$TEST_ENV/restore"

# 找到BSON文件
BSON_DIR=$(find "$TEST_ENV/restore" -name "*.bson" -type f | head -1 | xargs dirname)

# 启动临时实例
sudo mongod --dbpath "$TEST_ENV/db" --port $TEST_PORT --fork --logpath "$TEST_ENV/mongod.log" --bind_ip 127.0.0.1

# 恢复到测试数据库(重命名)
mongorestore --host 127.0.0.1 --port $TEST_PORT \
  --nsFrom "*.*" --nsFrom "*.*" --nsTo "${TEST_DB}.*" \
  "$BSON_DIR"

# 运行数据完整性检查
mongosh --host 127.0.0.1 --port $TEST_PORT --eval "
    var testDB = db.getSiblingDB('$TEST_DB');
    var collections = testDB.getCollectionNames();
    var totalDocs = 0;
    
    collections.forEach(function(coll) {
        if (coll.indexOf('system.') === -1) {
            var count = testDB[coll].countDocuments();
            totalDocs += count;
            print('Collection ' + coll + ': ' + count + ' documents');
        }
    });
    
    print('Total documents restored: ' + totalDocs);
    
    // 执行一些查询测试
    if (totalDocs > 0) {
        var sampleColl = collections.find(c => c.indexOf('system.') === -1);
        if (sampleColl) {
            var sample = testDB[sampleColl].findOne();
            print('Sample document: ' + JSON.stringify(sample, null, 2));
        }
    }
"

# 清理
sudo mongod --dbpath "$TEST_ENV/db" --port $TEST_PORT --shutdown
sudo rm -rf "$TEST_ENV"

echo "Monthly restore test completed successfully"

七、备份安全与合规

7.1 备份加密

7.1.1 使用GPG加密备份

#!/bin/bash
# 加密备份脚本:encrypt_backup.sh

BACKUP_FILE="$1"
GPG_RECIPIENT="backup-key@example.com"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file>"
    exit 1
fi

# 生成对称加密密钥(更安全)
SYMMETRIC_KEY=$(openssl rand -base64 32)
echo "$SYMMETRIC_KEY" | gpg --batch --yes --passphrase-fd 0 --symmetric --cipher-algo AES256 "${BACKUP_FILE}.key"

# 使用对称密钥加密备份
gpg --batch --yes --passphrase-file "${BACKUP_FILE}.key" --symmetric --cipher-algo AES256 --output "${BACKUP_FILE}.gpg" "$BACKUP_FILE"

# 清理原始文件(可选)
# rm "$BACKUP_FILE"

echo "Backup encrypted: ${BACKUP_FILE}.gpg"
echo "Key file: ${BACKUP_FILE}.key"
echo "Store the key file securely!"

7.1.2 使用MongoDB加密备份(WiredTiger加密)

# 如果MongoDB使用WiredTiger加密,物理备份自动加密
# 但逻辑备份需要额外处理

# 在mongodump时使用加密传输
mongodump --host localhost --port 27017 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --ssl --sslPEMKeyFile /path/to/client.pem \
  --out /backup/mongodb/encrypted_$(date +%Y%m%d)

7.2 备份访问控制

# 创建专用备份用户(最小权限原则)
mongosh --eval "
db.getSiblingDB('admin').createUser({
    user: 'backupuser',
    pwd: 'backupPass',
    roles: [
        {role: 'backup', db: 'admin'},
        {role: 'clusterMonitor', db: 'admin'},
        {role: 'readAnyDatabase', db: 'admin'},
        {role: 'readWrite', db: 'local'}
    ]
});
"

# 限制备份用户只能从特定IP访问
# 在mongod.conf中配置:
# security:
#   authorization: enabled
#   authorization: enabled
#   clusterAuthMode: x509

7.3 备份审计

# 记录所有备份操作到系统日志
logger -t mongodb_backup "Starting backup of $DATABASE"

# 在备份脚本中添加审计日志
log_backup_action() {
    local action=$1
    local status=$2
    local details=$3
    
    # 写入本地日志
    echo "$(date -Iseconds) $action $status $details" >> /var/log/mongodb_backup_audit.log
    
    # 发送到远程日志服务器
    logger -t mongodb_backup -p local0.info "$action $status $details"
    
    # 可选:发送到SIEM系统
    # curl -X POST https://siem.example.com/api/logs \
    #   -H "Authorization: Bearer $SIEM_TOKEN" \
    #   -d "{\"timestamp\":\"$(date -Iseconds)\",\"action\":\"$action\",\"status\":\"$status\",\"details\":\"$details\"}"
}

八、备份策略设计与最佳实践

8.1 备份策略设计原则

8.1.1 3-2-1备份规则

  • 3:至少3份数据副本
  • 2:存储在2种不同介质上
  • 1:至少1份异地备份

8.1.2 RPO与RTO定义

  • RPO(恢复点目标):可接受的数据丢失量(如1小时)
  • RTO(恢复时间目标):恢复业务所需时间(如4小时)

8.2 不同场景的备份策略示例

8.2.1 开发/测试环境

# 策略:每日全量备份,保留7天
# 使用mongodump,无需oplog
0 2 * * * mongodump --host localhost --port 27017 --out /backup/dev/$(date +%Y%m%d) && find /backup/dev -mtime +7 -delete

8.2.2 生产环境(单机/副本集)

# 策略:
# - 每日全量备份(使用PBM)
# - 每小时增量备份(PITR)
# - 保留30天全量,7天增量
# - 异地备份到S3

# PBM配置
pbm config --file /dev/stdin <<EOF
storage:
  type: s3
  s3:
    region: us-east-1
    bucket: prod-mongodb-backups
    prefix: daily
pitr:
  enabled: true
  oplogSpanMin: 60
EOF

# 每日全量
0 2 * * * pbm backup --type=full --compression=gzip

# 每月1号执行验证
0 3 1 * * /usr/local/bin/verify_backup.sh

8.2.3 金融/医疗行业(合规要求)

# 策略:
# - 每日全量备份
# - 实时WAL归档(Percona Server)
# - 备份加密
# - 异地多副本存储
# - 保留7年

# 配置WAL归档(Percona Server for MongoDB)
# mongod.conf
storage:
  wiredTiger:
    engineConfig:
      journalCompressor: snappy
  syncPeriodSecs: 60

# 启用归档
sudo mkdir -p /var/lib/mongodb/archive
sudo chown -R mongodb:mongodb /var/lib/mongodb/archive

# 配置归档脚本(每15分钟)
*/15 * * * * /usr/local/bin/archive_wal.sh

# 备份加密
0 2 * * * /usr/local/bin/mongo_backup_auto.sh && /usr/local/bin/encrypt_backup.sh /backup/mongodb/latest.tar.gz

8.3 备份监控与告警体系

# 综合监控脚本:backup_monitor.sh

#!/bin/bash

# 配置
BACKUP_BASE="/backup/mongodb"
LOG_FILE="/var/log/mongodb_backup_monitor.log"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
PAGERDUTY_KEY="your-pagerduty-key"

# 检查函数
check_backup_age() {
    local max_age_hours=25
    local latest=$(find "$BACKUP_BASE" -name "*.tar.gz" -type f -mmin -$((max_age_hours * 60)) 2>/dev/null)
    
    if [ -z "$latest" ]; then
        send_alert "CRITICAL" "No backup found in last $max_age_hours hours"
        return 1
    fi
    
    local age=$(stat -c %Y "$latest")
    local now=$(date +%s)
    local age_hours=$(( (now - age) / 3600 ))
    
    if [ $age_hours -gt 24 ]; then
        send_alert "WARNING" "Latest backup is $age_hours hours old"
    fi
    
    return 0
}

check_backup_size() {
    local latest=$(find "$BACKUP_BASE" -name "*.tar.gz" -type f -printf "%s\n" | head -1)
    
    if [ -n "$latest" ] && [ "$latest" -lt 104857600 ]; then
        send_alert "WARNING" "Backup size too small: $latest bytes"
    fi
}

check_restore_test() {
    local last_test=$(stat -c %Y /tmp/mongodb_verify 2>/dev/null)
    local now=$(date +%s)
    local days=$(( (now - last_test) / 86400 ))
    
    if [ $days -gt 30 ]; then
        send_alert "WARNING" "Restore test not run in $days days"
    fi
}

# 告警函数
send_alert() {
    local level=$1
    local message=$2
    local timestamp=$(date -Iseconds)
    
    # 日志
    echo "[$timestamp] $level: $message" >> "$LOG_FILE"
    
    # Slack
    if [ -n "$SLACK_WEBHOOK" ]; then
        curl -X POST -H 'Content-type: application/json' \
          --data "{\"text\":\"MongoDB Backup $level: $message\"}" \
          "$SLACK_WEBHOOK" > /dev/null 2>&1
    fi
    
    # PagerDuty(仅CRITICAL)
    if [ "$level" = "CRITICAL" ] && [ -n "$PAGERDUTY_KEY" ]; then
        curl -X POST -H "Content-Type: application/json" \
          -d "{\"routing_key\":\"$PAGERDUTY_KEY\",\"event_action\":\"trigger\",\"payload\":{\"summary\":\"$message\",\"severity\":\"critical\"}}" \
          https://events.pagerduty.com/v2/enqueue > /dev/null 2>&1
    fi
    
    # 邮件
    if [ "$level" = "CRITICAL" ]; then
        echo "$message" | mailx -s "MongoDB Backup $level" admin@example.com
    fi
}

# 主监控流程
main() {
    local errors=0
    
    check_backup_age || ((errors++))
    check_backup_size || ((errors++))
    check_restore_test || ((errors++))
    
    if [ $errors -eq 0 ]; then
        echo "All backup checks passed"
        exit 0
    else
        echo "$errors backup check(s) failed"
        exit 1
    fi
}

main "$@"

九、常见问题与解决方案

9.1 备份失败常见原因

9.1.1 网络问题

# 诊断网络连接
mongosh --host backup-host --eval "db.adminCommand('ping')" --quiet

# 使用带超时的备份
mongodump --host backup-host --port 27017 \
  --username backupuser --password "backupPass" --authenticationDatabase admin \
  --timeoutMS=30000 \
  --out /backup/mongodb/$(date +%Y%m%d)

9.1.2 磁盘空间不足

# 预估备份大小
mongosh --eval "
var totalSize = 0;
db.adminCommand({listDatabases:1}).databases.forEach(function(db){
    var size = db.getSiblingDB(db.name).stats().dataSize;
    totalSize += size;
});
print('Estimated backup size: ' + (totalSize / 1024 / 1024 / 1024).toFixed(2) + ' GB');
"

# 清理磁盘空间
# 1. 删除旧备份
find /backup/mongodb -name "*.tar.gz" -mtime +30 -delete

# 2. 清理MongoDB日志
sudo journalctl --vacuum-time=7d

# 3. 压缩旧日志
find /var/log/mongodb -name "*.log" -mtime +7 -exec gzip {} \;

9.1.3 权限不足

# 检查备份用户权限
mongosh --eval "
db.getSiblingDB('admin').getUser('backupuser')
"

# 重新授权
mongosh --eval "
db.getSiblingDB('admin').grantRolesToUser('backupuser', [
    {role: 'backup', db: 'admin'},
    {role: 'clusterMonitor', db: 'admin'},
    {role: 'readAnyDatabase', db: 'admin'}
]);
"

9.2 恢复失败常见原因

9.2.1 版本不兼容

# 检查备份的MongoDB版本
mongodump --version

# 如果版本差异大,使用中间版本过渡
# 例如:从3.6备份恢复到5.0
# 1. 使用4.0版本临时恢复
# 2. 从4.0导出为JSON
# 3. 导入到5.0

9.2.2 索引损坏

# 恢复时跳过索引,后续重建
mongorestore --noIndexRestore /backup/mongodb/20231201

# 重建索引
mongosh --eval "
db.getCollectionNames().forEach(function(coll){
    if (coll.indexOf('system.') === -1) {
        print('Reindexing: ' + coll);
        db[coll].reIndex();
    }
});
"

9.2.3 数据冲突(分片集群)

# 在分片集群恢复时,如果数据已存在,需要使用--drop
mongorestore --drop --host mongos --port 27017 /backup/mongodb/20231201

# 或者使用--mode=merge(PBM)
pbm restore --mode=merge 2023-12-01T12:00:00Z

十、总结与建议

10.1 备份策略选择指南

场景 推荐工具 备份频率 保留周期 恢复时间
开发测试 mongodump 每日 7天 小时级
生产单机 PBM + 文件系统快照 每日全量 + 每小时增量 30天 + 7天 分钟级
生产副本集 PBM + 从Secondary备份 每日全量 + 每小时PITR 30天 + 7天 分钟级
生产分片集群 PBM + 分片级备份 每日全量 + 每小时PITR 30天 + 7天 小时级
合规要求 PBM + WAL归档 + 加密 每日全量 + 实时归档 7年 小时级

10.2 关键成功因素

  1. 自动化:所有备份和恢复操作应自动化,减少人为错误
  2. 监控:建立完善的监控和告警体系
  3. 测试:定期进行恢复测试,确保备份可用
  4. 文档:详细记录备份策略和恢复步骤
  5. 安全:保护备份数据,防止未授权访问
  6. 异地:确保至少一份备份存储在异地

10.3 持续改进

  • 定期审查:每季度审查备份策略的有效性
  • 容量规划:根据数据增长调整备份存储
  • 技术更新:关注MongoDB新版本的备份特性
  • 演练:每半年进行一次灾难恢复演练

通过实施本文介绍的备份策略,您可以有效降低数据丢失风险,确保在发生灾难时能够快速恢复业务。记住,备份的价值只有在恢复成功时才能体现,因此请务必重视备份验证和恢复测试。