Skip to content

备份策略

📋 概述

备份策略是保护数据和确保业务连续性的关键措施。有效的备份策略应该包括数据备份、系统备份、配置备份和灾难恢复计划,确保在各种故障场景下都能快速恢复服务。

🎯 学习目标

  • 理解备份策略的核心概念和重要性
  • 掌握不同类型数据的备份方法
  • 学会设计和实施备份计划
  • 了解灾难恢复和业务连续性规划

📚 备份基础概念

备份类型

mermaid
graph TB
    A[备份类型] --> B[完全备份]
    A --> C[增量备份]
    A --> D[差异备份]
    
    B --> B1[备份所有数据]
    B --> B2[恢复速度快]
    B --> B3[存储空间大]
    
    C --> C1[只备份变更数据]
    C --> C2[存储空间小]
    C --> C3[恢复时间长]
    
    D --> D1[备份自上次完全备份后的变更]
    D --> D2[平衡存储和恢复时间]

RTO和RPO概念

javascript
// 恢复时间目标 (RTO) - Recovery Time Objective
const RTO = {
  tier1: '15分钟',    // 关键业务系统
  tier2: '4小时',     // 重要业务系统
  tier3: '24小时'     // 一般业务系统
};

// 恢复点目标 (RPO) - Recovery Point Objective
const RPO = {
  tier1: '5分钟',     // 可接受的最大数据丢失时间
  tier2: '1小时',
  tier3: '24小时'
};

🛠 数据库备份策略

PostgreSQL备份

bash
#!/bin/bash
# postgres-backup.sh

set -e

# 配置变量
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-nodejs_app}
DB_USER=${DB_USER:-postgres}
BACKUP_DIR=${BACKUP_DIR:-/backups/postgres}
RETENTION_DAYS=${RETENTION_DAYS:-30}
S3_BUCKET=${S3_BUCKET:-""}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql"
BACKUP_FILE_COMPRESSED="${BACKUP_FILE}.gz"

echo "🔄 开始数据库备份: $DB_NAME"

# 执行备份
pg_dump \
  --host="$DB_HOST" \
  --port="$DB_PORT" \
  --username="$DB_USER" \
  --dbname="$DB_NAME" \
  --verbose \
  --clean \
  --if-exists \
  --create \
  --format=plain \
  --file="$BACKUP_FILE"

# 压缩备份文件
echo "📦 压缩备份文件..."
gzip "$BACKUP_FILE"

# 验证备份文件
if [ ! -f "$BACKUP_FILE_COMPRESSED" ]; then
    echo "❌ 备份失败: 文件不存在"
    exit 1
fi

BACKUP_SIZE=$(du -h "$BACKUP_FILE_COMPRESSED" | cut -f1)
echo "✅ 备份完成: $BACKUP_FILE_COMPRESSED ($BACKUP_SIZE)"

# 上传到S3(如果配置了)
if [ -n "$S3_BUCKET" ]; then
    echo "☁️ 上传备份到S3..."
    aws s3 cp "$BACKUP_FILE_COMPRESSED" "s3://$S3_BUCKET/postgres/$(basename $BACKUP_FILE_COMPRESSED)"
    echo "✅ S3上传完成"
fi

# 清理旧备份
echo "🧹 清理旧备份文件..."
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "✅ 清理完成"

# 备份验证
echo "🔍 验证备份文件完整性..."
if gunzip -t "$BACKUP_FILE_COMPRESSED"; then
    echo "✅ 备份文件完整性验证通过"
else
    echo "❌ 备份文件损坏"
    exit 1
fi

echo "🎉 备份流程完成"

MongoDB备份

bash
#!/bin/bash
# mongodb-backup.sh

set -e

# 配置变量
MONGO_HOST=${MONGO_HOST:-localhost}
MONGO_PORT=${MONGO_PORT:-27017}
MONGO_DB=${MONGO_DB:-nodejs_app}
MONGO_USER=${MONGO_USER:-""}
MONGO_PASS=${MONGO_PASS:-""}
BACKUP_DIR=${BACKUP_DIR:-/backups/mongodb}
RETENTION_DAYS=${RETENTION_DAYS:-30}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_PATH="$BACKUP_DIR/${MONGO_DB}_${TIMESTAMP}"

echo "🔄 开始MongoDB备份: $MONGO_DB"

# 构建mongodump命令
MONGODUMP_CMD="mongodump --host $MONGO_HOST:$MONGO_PORT --db $MONGO_DB --out $BACKUP_PATH"

if [ -n "$MONGO_USER" ] && [ -n "$MONGO_PASS" ]; then
    MONGODUMP_CMD="$MONGODUMP_CMD --username $MONGO_USER --password $MONGO_PASS"
fi

# 执行备份
eval $MONGODUMP_CMD

# 压缩备份
echo "📦 压缩备份文件..."
tar -czf "${BACKUP_PATH}.tar.gz" -C "$BACKUP_DIR" "$(basename $BACKUP_PATH)"
rm -rf "$BACKUP_PATH"

BACKUP_SIZE=$(du -h "${BACKUP_PATH}.tar.gz" | cut -f1)
echo "✅ 备份完成: ${BACKUP_PATH}.tar.gz ($BACKUP_SIZE)"

# 清理旧备份
find "$BACKUP_DIR" -name "${MONGO_DB}_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "🎉 MongoDB备份完成"

Redis备份

bash
#!/bin/bash
# redis-backup.sh

set -e

# 配置变量
REDIS_HOST=${REDIS_HOST:-localhost}
REDIS_PORT=${REDIS_PORT:-6379}
REDIS_PASSWORD=${REDIS_PASSWORD:-""}
BACKUP_DIR=${BACKUP_DIR:-/backups/redis}
RETENTION_DAYS=${RETENTION_DAYS:-7}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/redis_${TIMESTAMP}.rdb"

echo "🔄 开始Redis备份"

# 构建redis-cli命令
REDIS_CLI_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT"

if [ -n "$REDIS_PASSWORD" ]; then
    REDIS_CLI_CMD="$REDIS_CLI_CMD -a $REDIS_PASSWORD"
fi

# 执行BGSAVE命令
echo "📦 执行后台保存..."
$REDIS_CLI_CMD BGSAVE

# 等待备份完成
while [ "$($REDIS_CLI_CMD LASTSAVE)" = "$($REDIS_CLI_CMD LASTSAVE)" ]; do
    sleep 1
done

# 复制RDB文件
REDIS_DATA_DIR=$($REDIS_CLI_CMD CONFIG GET dir | tail -1)
REDIS_RDB_FILE=$($REDIS_CLI_CMD CONFIG GET dbfilename | tail -1)

cp "$REDIS_DATA_DIR/$REDIS_RDB_FILE" "$BACKUP_FILE"

# 压缩备份文件
gzip "$BACKUP_FILE"

BACKUP_SIZE=$(du -h "${BACKUP_FILE}.gz" | cut -f1)
echo "✅ Redis备份完成: ${BACKUP_FILE}.gz ($BACKUP_SIZE)"

# 清理旧备份
find "$BACKUP_DIR" -name "redis_*.rdb.gz" -mtime +$RETENTION_DAYS -delete

echo "🎉 Redis备份流程完成"

📁 文件系统备份

应用代码和配置备份

bash
#!/bin/bash
# application-backup.sh

set -e

# 配置变量
APP_DIR=${APP_DIR:-/opt/nodejs-app}
CONFIG_DIR=${CONFIG_DIR:-/etc/nodejs-app}
BACKUP_DIR=${BACKUP_DIR:-/backups/application}
RETENTION_DAYS=${RETENTION_DAYS:-30}
S3_BUCKET=${S3_BUCKET:-""}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/app_backup_${TIMESTAMP}.tar.gz"

echo "🔄 开始应用备份"

# 创建备份
tar -czf "$BACKUP_FILE" \
    --exclude="$APP_DIR/node_modules" \
    --exclude="$APP_DIR/logs" \
    --exclude="$APP_DIR/tmp" \
    --exclude="$APP_DIR/.git" \
    "$APP_DIR" \
    "$CONFIG_DIR"

BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "✅ 应用备份完成: $BACKUP_FILE ($BACKUP_SIZE)"

# 上传到S3
if [ -n "$S3_BUCKET" ]; then
    aws s3 cp "$BACKUP_FILE" "s3://$S3_BUCKET/application/$(basename $BACKUP_FILE)"
    echo "☁️ 已上传到S3"
fi

# 清理旧备份
find "$BACKUP_DIR" -name "app_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "🎉 应用备份流程完成"

用户上传文件备份

bash
#!/bin/bash
# uploads-backup.sh

set -e

# 配置变量
UPLOADS_DIR=${UPLOADS_DIR:-/var/www/uploads}
BACKUP_DIR=${BACKUP_DIR:-/backups/uploads}
S3_BUCKET=${S3_BUCKET:-""}
RETENTION_DAYS=${RETENTION_DAYS:-90}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

echo "🔄 开始用户上传文件备份"

# 使用rsync进行增量备份
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_PATH="$BACKUP_DIR/uploads_$TIMESTAMP"

rsync -av \
    --delete \
    --exclude="*.tmp" \
    --exclude="*.temp" \
    "$UPLOADS_DIR/" \
    "$BACKUP_PATH/"

# 压缩备份
tar -czf "${BACKUP_PATH}.tar.gz" -C "$BACKUP_DIR" "$(basename $BACKUP_PATH)"
rm -rf "$BACKUP_PATH"

BACKUP_SIZE=$(du -h "${BACKUP_PATH}.tar.gz" | cut -f1)
echo "✅ 用户文件备份完成: ${BACKUP_PATH}.tar.gz ($BACKUP_SIZE)"

# 同步到S3(如果配置了)
if [ -n "$S3_BUCKET" ]; then
    aws s3 sync "$UPLOADS_DIR/" "s3://$S3_BUCKET/uploads/" \
        --delete \
        --exclude "*.tmp" \
        --exclude "*.temp"
    echo "☁️ 已同步到S3"
fi

# 清理旧备份
find "$BACKUP_DIR" -name "uploads_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "🎉 用户文件备份完成"

🔄 自动化备份系统

备份调度器

javascript
// backup-scheduler.js
const cron = require('node-cron');
const { exec } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const logger = require('./logger');

class BackupScheduler {
  constructor(config) {
    this.config = config;
    this.backupJobs = new Map();
  }

  // 启动备份调度
  start() {
    logger.info('Starting backup scheduler');
    
    // 数据库备份 - 每天凌晨2点
    this.scheduleJob('database-backup', '0 2 * * *', () => {
      this.runDatabaseBackup();
    });
    
    // 应用备份 - 每天凌晨3点
    this.scheduleJob('application-backup', '0 3 * * *', () => {
      this.runApplicationBackup();
    });
    
    // 用户文件备份 - 每6小时
    this.scheduleJob('uploads-backup', '0 */6 * * *', () => {
      this.runUploadsBackup();
    });
    
    // 日志备份 - 每天凌晨4点
    this.scheduleJob('logs-backup', '0 4 * * *', () => {
      this.runLogsBackup();
    });
    
    // 备份验证 - 每天上午9点
    this.scheduleJob('backup-verification', '0 9 * * *', () => {
      this.verifyBackups();
    });
  }

  scheduleJob(name, schedule, task) {
    const job = cron.schedule(schedule, async () => {
      try {
        logger.info(`Starting scheduled backup: ${name}`);
        await task();
        logger.info(`Completed scheduled backup: ${name}`);
      } catch (error) {
        logger.error(`Backup failed: ${name}`, { error: error.message });
        await this.sendAlertNotification(name, error);
      }
    }, {
      scheduled: false,
      timezone: this.config.timezone || 'UTC'
    });
    
    this.backupJobs.set(name, job);
    job.start();
    
    logger.info(`Scheduled backup job: ${name} (${schedule})`);
  }

  async runDatabaseBackup() {
    const script = path.join(__dirname, 'scripts/postgres-backup.sh');
    return this.executeScript(script, {
      DB_HOST: this.config.database.host,
      DB_NAME: this.config.database.name,
      DB_USER: this.config.database.user,
      BACKUP_DIR: this.config.backupDir,
      S3_BUCKET: this.config.s3Bucket
    });
  }

  async runApplicationBackup() {
    const script = path.join(__dirname, 'scripts/application-backup.sh');
    return this.executeScript(script, {
      APP_DIR: this.config.appDir,
      CONFIG_DIR: this.config.configDir,
      BACKUP_DIR: this.config.backupDir,
      S3_BUCKET: this.config.s3Bucket
    });
  }

  async runUploadsBackup() {
    const script = path.join(__dirname, 'scripts/uploads-backup.sh');
    return this.executeScript(script, {
      UPLOADS_DIR: this.config.uploadsDir,
      BACKUP_DIR: this.config.backupDir,
      S3_BUCKET: this.config.s3Bucket
    });
  }

  async runLogsBackup() {
    const logsDir = this.config.logsDir;
    const backupDir = path.join(this.config.backupDir, 'logs');
    const timestamp = new Date().toISOString().slice(0, 10);
    
    await fs.mkdir(backupDir, { recursive: true });
    
    const command = `tar -czf ${backupDir}/logs_${timestamp}.tar.gz -C ${logsDir} .`;
    return this.executeCommand(command);
  }

  async verifyBackups() {
    const backupTypes = ['postgres', 'application', 'uploads', 'logs'];
    const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
    
    for (const type of backupTypes) {
      try {
        const backupDir = path.join(this.config.backupDir, type);
        const files = await fs.readdir(backupDir);
        
        const todayBackups = files.filter(file => file.includes(today));
        
        if (todayBackups.length === 0) {
          throw new Error(`No backup found for ${type} today`);
        }
        
        // 验证备份文件完整性
        for (const file of todayBackups.slice(0, 1)) { // 只验证最新的
          const filePath = path.join(backupDir, file);
          await this.verifyBackupFile(filePath);
        }
        
        logger.info(`Backup verification passed: ${type}`);
      } catch (error) {
        logger.error(`Backup verification failed: ${type}`, { error: error.message });
        await this.sendAlertNotification(`backup-verification-${type}`, error);
      }
    }
  }

  async verifyBackupFile(filePath) {
    const stats = await fs.stat(filePath);
    
    if (stats.size === 0) {
      throw new Error('Backup file is empty');
    }
    
    // 对于压缩文件,验证压缩完整性
    if (filePath.endsWith('.gz')) {
      const command = `gunzip -t "${filePath}"`;
      await this.executeCommand(command);
    } else if (filePath.endsWith('.tar.gz')) {
      const command = `tar -tzf "${filePath}" > /dev/null`;
      await this.executeCommand(command);
    }
  }

  async executeScript(scriptPath, env = {}) {
    const envVars = { ...process.env, ...env };
    return this.executeCommand(`bash "${scriptPath}"`, envVars);
  }

  async executeCommand(command, env = process.env) {
    return new Promise((resolve, reject) => {
      exec(command, { env }, (error, stdout, stderr) => {
        if (error) {
          logger.error(`Command failed: ${command}`, { 
            error: error.message,
            stderr 
          });
          reject(error);
        } else {
          logger.info(`Command completed: ${command}`, { stdout });
          resolve(stdout);
        }
      });
    });
  }

  async sendAlertNotification(jobName, error) {
    // 发送告警通知(Slack, Email等)
    const alertMessage = {
      title: `Backup Job Failed: ${jobName}`,
      message: error.message,
      timestamp: new Date().toISOString(),
      severity: 'critical'
    };
    
    // 这里可以集成各种通知渠道
    logger.error('Backup alert', alertMessage);
  }

  stop() {
    logger.info('Stopping backup scheduler');
    
    for (const [name, job] of this.backupJobs) {
      job.stop();
      logger.info(`Stopped backup job: ${name}`);
    }
    
    this.backupJobs.clear();
  }

  // 手动触发备份
  async triggerBackup(jobName) {
    const jobMap = {
      'database': () => this.runDatabaseBackup(),
      'application': () => this.runApplicationBackup(),
      'uploads': () => this.runUploadsBackup(),
      'logs': () => this.runLogsBackup()
    };
    
    const job = jobMap[jobName];
    if (!job) {
      throw new Error(`Unknown backup job: ${jobName}`);
    }
    
    logger.info(`Manually triggering backup: ${jobName}`);
    await job();
    logger.info(`Manual backup completed: ${jobName}`);
  }
}

module.exports = BackupScheduler;

备份配置

javascript
// backup-config.js
const config = {
  // 基础配置
  timezone: 'Asia/Shanghai',
  backupDir: '/backups',
  
  // 数据库配置
  database: {
    host: process.env.DB_HOST || 'localhost',
    port: process.env.DB_PORT || 5432,
    name: process.env.DB_NAME || 'nodejs_app',
    user: process.env.DB_USER || 'postgres'
  },
  
  // 应用配置
  appDir: '/opt/nodejs-app',
  configDir: '/etc/nodejs-app',
  uploadsDir: '/var/www/uploads',
  logsDir: '/var/log/nodejs-app',
  
  // 云存储配置
  s3Bucket: process.env.S3_BACKUP_BUCKET,
  
  // 保留策略
  retention: {
    daily: 30,    // 保留30天的每日备份
    weekly: 12,   // 保留12周的每周备份
    monthly: 12   // 保留12个月的每月备份
  },
  
  // 备份验证
  verification: {
    enabled: true,
    maxAge: 24 * 60 * 60 * 1000, // 24小时内的备份才验证
    sampleSize: 1 // 每种类型验证最新的1个备份
  },
  
  // 通知配置
  notifications: {
    slack: {
      enabled: process.env.SLACK_WEBHOOK_URL ? true : false,
      webhookUrl: process.env.SLACK_WEBHOOK_URL,
      channel: '#alerts'
    },
    email: {
      enabled: process.env.SMTP_HOST ? true : false,
      recipients: ['admin@company.com', 'devops@company.com']
    }
  }
};

module.exports = config;

🔄 灾难恢复计划

数据库恢复

bash
#!/bin/bash
# postgres-restore.sh

set -e

# 配置变量
BACKUP_FILE=${1:-""}
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-nodejs_app}
DB_USER=${DB_USER:-postgres}

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

echo "🔄 开始数据库恢复"
echo "备份文件: $BACKUP_FILE"
echo "目标数据库: $DB_NAME @ $DB_HOST:$DB_PORT"

# 确认操作
read -p "⚠️  这将覆盖现有数据库。确认继续?(yes/no): " confirm
if [ "$confirm" != "yes" ]; then
    echo "操作已取消"
    exit 0
fi

# 解压备份文件(如果需要)
if [[ "$BACKUP_FILE" == *.gz ]]; then
    echo "📦 解压备份文件..."
    TEMP_FILE="/tmp/$(basename $BACKUP_FILE .gz)"
    gunzip -c "$BACKUP_FILE" > "$TEMP_FILE"
    BACKUP_FILE="$TEMP_FILE"
fi

# 停止应用服务
echo "⏸️  停止应用服务..."
systemctl stop nodejs-app || true

# 创建恢复前备份
echo "💾 创建恢复前备份..."
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
pg_dump \
    --host="$DB_HOST" \
    --port="$DB_PORT" \
    --username="$DB_USER" \
    --dbname="$DB_NAME" \
    --file="/tmp/pre_restore_backup_${TIMESTAMP}.sql" \
    || echo "⚠️  无法创建恢复前备份,可能数据库不存在"

# 执行恢复
echo "🔄 执行数据库恢复..."
psql \
    --host="$DB_HOST" \
    --port="$DB_PORT" \
    --username="$DB_USER" \
    --dbname="postgres" \
    --file="$BACKUP_FILE"

# 验证恢复
echo "🔍 验证数据库恢复..."
TABLE_COUNT=$(psql \
    --host="$DB_HOST" \
    --port="$DB_PORT" \
    --username="$DB_USER" \
    --dbname="$DB_NAME" \
    --tuples-only \
    --command="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" \
    | xargs)

echo "✅ 恢复完成,共 $TABLE_COUNT 个表"

# 清理临时文件
if [ -f "$TEMP_FILE" ]; then
    rm -f "$TEMP_FILE"
fi

# 重启应用服务
echo "▶️  重启应用服务..."
systemctl start nodejs-app

echo "🎉 数据库恢复完成"

完整系统恢复

bash
#!/bin/bash
# system-restore.sh

set -e

# 配置变量
BACKUP_DATE=${1:-""}
BACKUP_DIR=${BACKUP_DIR:-/backups}
S3_BUCKET=${S3_BUCKET:-""}

if [ -z "$BACKUP_DATE" ]; then
    echo "Usage: $0 <backup_date> (format: YYYYMMDD)"
    exit 1
fi

echo "🔄 开始系统完整恢复"
echo "恢复日期: $BACKUP_DATE"

# 确认操作
read -p "⚠️  这将恢复整个系统到指定日期状态。确认继续?(yes/no): " confirm
if [ "$confirm" != "yes" ]; then
    echo "操作已取消"
    exit 0
fi

# 从S3下载备份(如果需要)
if [ -n "$S3_BUCKET" ]; then
    echo "☁️ 从S3下载备份文件..."
    
    aws s3 cp "s3://$S3_BUCKET/postgres/nodejs_app_${BACKUP_DATE}_*.sql.gz" "$BACKUP_DIR/postgres/" || true
    aws s3 cp "s3://$S3_BUCKET/application/app_backup_${BACKUP_DATE}_*.tar.gz" "$BACKUP_DIR/application/" || true
    aws s3 cp "s3://$S3_BUCKET/uploads/uploads_${BACKUP_DATE}_*.tar.gz" "$BACKUP_DIR/uploads/" || true
fi

# 停止所有服务
echo "⏸️  停止所有服务..."
systemctl stop nodejs-app
systemctl stop nginx
systemctl stop postgresql

# 恢复数据库
echo "🗄️  恢复数据库..."
DB_BACKUP=$(find "$BACKUP_DIR/postgres" -name "nodejs_app_${BACKUP_DATE}_*.sql.gz" | head -1)
if [ -n "$DB_BACKUP" ]; then
    ./postgres-restore.sh "$DB_BACKUP"
else
    echo "❌ 未找到数据库备份文件"
    exit 1
fi

# 恢复应用代码
echo "📁 恢复应用代码..."
APP_BACKUP=$(find "$BACKUP_DIR/application" -name "app_backup_${BACKUP_DATE}_*.tar.gz" | head -1)
if [ -n "$APP_BACKUP" ]; then
    # 备份当前应用
    mv /opt/nodejs-app /opt/nodejs-app.backup.$(date +%s)
    
    # 解压备份
    tar -xzf "$APP_BACKUP" -C /
    
    # 重新安装依赖
    cd /opt/nodejs-app
    npm ci --only=production
else
    echo "❌ 未找到应用备份文件"
fi

# 恢复用户文件
echo "📎 恢复用户文件..."
UPLOADS_BACKUP=$(find "$BACKUP_DIR/uploads" -name "uploads_${BACKUP_DATE}_*.tar.gz" | head -1)
if [ -n "$UPLOADS_BACKUP" ]; then
    # 备份当前文件
    mv /var/www/uploads /var/www/uploads.backup.$(date +%s)
    
    # 解压备份
    mkdir -p /var/www/uploads
    tar -xzf "$UPLOADS_BACKUP" -C /var/www/uploads
    
    # 设置权限
    chown -R www-data:www-data /var/www/uploads
else
    echo "⚠️  未找到用户文件备份"
fi

# 启动服务
echo "▶️  启动服务..."
systemctl start postgresql
systemctl start nodejs-app
systemctl start nginx

# 验证恢复
echo "🔍 验证系统恢复..."
sleep 10

# 检查服务状态
if systemctl is-active --quiet nodejs-app; then
    echo "✅ Node.js应用运行正常"
else
    echo "❌ Node.js应用启动失败"
    systemctl status nodejs-app
fi

# 检查HTTP响应
if curl -f http://localhost/health > /dev/null 2>&1; then
    echo "✅ HTTP服务响应正常"
else
    echo "❌ HTTP服务无响应"
fi

echo "🎉 系统恢复完成"
echo "📝 请检查应用功能并验证数据完整性"

📊 备份监控和报告

备份状态监控

javascript
// backup-monitor.js
const fs = require('fs').promises;
const path = require('path');
const logger = require('./logger');

class BackupMonitor {
  constructor(config) {
    this.config = config;
    this.backupDir = config.backupDir;
  }

  async generateBackupReport() {
    const report = {
      timestamp: new Date().toISOString(),
      backupTypes: {},
      summary: {
        totalBackups: 0,
        totalSize: 0,
        oldestBackup: null,
        newestBackup: null,
        healthStatus: 'healthy'
      }
    };

    const backupTypes = ['postgres', 'application', 'uploads', 'logs'];
    
    for (const type of backupTypes) {
      try {
        const typeReport = await this.analyzeBackupType(type);
        report.backupTypes[type] = typeReport;
        report.summary.totalBackups += typeReport.count;
        report.summary.totalSize += typeReport.totalSize;
      } catch (error) {
        logger.error(`Failed to analyze backup type: ${type}`, { error: error.message });
        report.backupTypes[type] = { error: error.message };
        report.summary.healthStatus = 'unhealthy';
      }
    }

    // 检查备份健康状态
    await this.checkBackupHealth(report);
    
    return report;
  }

  async analyzeBackupType(type) {
    const typeDir = path.join(this.backupDir, type);
    
    try {
      await fs.access(typeDir);
    } catch {
      return {
        count: 0,
        totalSize: 0,
        files: [],
        status: 'missing_directory'
      };
    }

    const files = await fs.readdir(typeDir);
    const backupFiles = files.filter(file => 
      file.endsWith('.gz') || file.endsWith('.tar.gz') || file.endsWith('.sql')
    );

    const fileDetails = [];
    let totalSize = 0;

    for (const file of backupFiles) {
      const filePath = path.join(typeDir, file);
      const stats = await fs.stat(filePath);
      
      fileDetails.push({
        name: file,
        size: stats.size,
        created: stats.birthtime,
        modified: stats.mtime,
        age: Date.now() - stats.mtime.getTime()
      });
      
      totalSize += stats.size;
    }

    // 按创建时间排序
    fileDetails.sort((a, b) => b.created - a.created);

    return {
      count: fileDetails.length,
      totalSize,
      files: fileDetails,
      status: this.getBackupTypeStatus(fileDetails, type)
    };
  }

  getBackupTypeStatus(files, type) {
    if (files.length === 0) {
      return 'no_backups';
    }

    const latestBackup = files[0];
    const maxAge = this.getMaxAgeForType(type);
    
    if (latestBackup.age > maxAge) {
      return 'outdated';
    }

    // 检查是否有损坏的备份文件
    const corruptedFiles = files.filter(file => file.size < 1024); // 小于1KB可能损坏
    if (corruptedFiles.length > 0) {
      return 'corrupted_files';
    }

    return 'healthy';
  }

  getMaxAgeForType(type) {
    const maxAges = {
      postgres: 24 * 60 * 60 * 1000,    // 24小时
      application: 24 * 60 * 60 * 1000,  // 24小时
      uploads: 6 * 60 * 60 * 1000,       // 6小时
      logs: 24 * 60 * 60 * 1000          // 24小时
    };
    
    return maxAges[type] || 24 * 60 * 60 * 1000;
  }

  async checkBackupHealth(report) {
    const issues = [];

    // 检查各个备份类型的健康状态
    for (const [type, data] of Object.entries(report.backupTypes)) {
      if (data.error) {
        issues.push(`${type}: ${data.error}`);
      } else if (data.status !== 'healthy') {
        issues.push(`${type}: ${data.status}`);
      }
    }

    // 检查总体备份数量
    if (report.summary.totalBackups === 0) {
      issues.push('No backups found');
      report.summary.healthStatus = 'critical';
    } else if (issues.length > 0) {
      report.summary.healthStatus = 'warning';
    }

    report.summary.issues = issues;
  }

  async cleanupOldBackups() {
    const retention = this.config.retention;
    const results = {
      cleaned: {},
      errors: {}
    };

    const backupTypes = ['postgres', 'application', 'uploads', 'logs'];
    
    for (const type of backupTypes) {
      try {
        const cleaned = await this.cleanupBackupType(type, retention.daily);
        results.cleaned[type] = cleaned;
        logger.info(`Cleaned up old backups for ${type}`, { count: cleaned });
      } catch (error) {
        results.errors[type] = error.message;
        logger.error(`Failed to cleanup backups for ${type}`, { error: error.message });
      }
    }

    return results;
  }

  async cleanupBackupType(type, retentionDays) {
    const typeDir = path.join(this.backupDir, type);
    const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
    
    try {
      const files = await fs.readdir(typeDir);
      let cleanedCount = 0;

      for (const file of files) {
        const filePath = path.join(typeDir, file);
        const stats = await fs.stat(filePath);
        
        if (stats.mtime < cutoffDate) {
          await fs.unlink(filePath);
          cleanedCount++;
          logger.info(`Deleted old backup: ${filePath}`);
        }
      }

      return cleanedCount;
    } catch (error) {
      if (error.code === 'ENOENT') {
        return 0; // 目录不存在,返回0
      }
      throw error;
    }
  }

  formatSize(bytes) {
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    if (bytes === 0) return '0 Bytes';
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
  }

  async generateHtmlReport(report) {
    const html = `
    <!DOCTYPE html>
    <html>
    <head>
        <title>Backup Report - ${new Date(report.timestamp).toLocaleDateString()}</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            .header { background-color: #f4f4f4; padding: 20px; border-radius: 5px; }
            .summary { display: flex; justify-content: space-between; margin: 20px 0; }
            .metric { text-align: center; }
            .status-healthy { color: green; }
            .status-warning { color: orange; }
            .status-critical { color: red; }
            table { width: 100%; border-collapse: collapse; margin: 20px 0; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
            th { background-color: #f2f2f2; }
        </style>
    </head>
    <body>
        <div class="header">
            <h1>Backup Report</h1>
            <p>Generated: ${new Date(report.timestamp).toLocaleString()}</p>
            <p>Status: <span class="status-${report.summary.healthStatus}">${report.summary.healthStatus.toUpperCase()}</span></p>
        </div>
        
        <div class="summary">
            <div class="metric">
                <h3>${report.summary.totalBackups}</h3>
                <p>Total Backups</p>
            </div>
            <div class="metric">
                <h3>${this.formatSize(report.summary.totalSize)}</h3>
                <p>Total Size</p>
            </div>
        </div>
        
        ${Object.entries(report.backupTypes).map(([type, data]) => `
            <h2>${type.charAt(0).toUpperCase() + type.slice(1)} Backups</h2>
            <p>Status: <span class="status-${data.status || 'unknown'}">${(data.status || 'unknown').toUpperCase()}</span></p>
            <p>Count: ${data.count || 0}</p>
            <p>Total Size: ${this.formatSize(data.totalSize || 0)}</p>
            
            ${data.files && data.files.length > 0 ? `
                <table>
                    <tr>
                        <th>File</th>
                        <th>Size</th>
                        <th>Created</th>
                        <th>Age</th>
                    </tr>
                    ${data.files.slice(0, 10).map(file => `
                        <tr>
                            <td>${file.name}</td>
                            <td>${this.formatSize(file.size)}</td>
                            <td>${new Date(file.created).toLocaleString()}</td>
                            <td>${Math.round(file.age / (1000 * 60 * 60))} hours</td>
                        </tr>
                    `).join('')}
                </table>
            ` : '<p>No backup files found</p>'}
        `).join('')}
        
        ${report.summary.issues && report.summary.issues.length > 0 ? `
            <h2>Issues</h2>
            <ul>
                ${report.summary.issues.map(issue => `<li>${issue}</li>`).join('')}
            </ul>
        ` : ''}
    </body>
    </html>
    `;
    
    return html;
  }
}

module.exports = BackupMonitor;

📝 总结

有效的备份策略应该包括:

  • 多层次备份:数据库、应用、配置、用户文件
  • 自动化调度:定期执行备份任务
  • 异地存储:云存储或远程备份
  • 备份验证:确保备份文件完整性
  • 快速恢复:制定明确的恢复流程
  • 监控报告:及时发现备份问题

备份策略是业务连续性的重要保障,需要定期测试和优化。

🔗 相关资源