82、数据库备份自动化:定时备份、增量备份、备份验证、异地备份

说实话,做了这么多年嵌入式数据库开发,我见过太多因为备份不到位而翻车的案例了。有一次,一个工业控制系统的数据库突然崩溃,结果发现备份文件已经三个月没更新了——因为运维人员忘了手动执行。从那以后,我给自己定了个规矩:凡是生产环境,备份必须自动化

这一章,我们就来聊聊怎么把备份这件事做得滴水不漏。定时备份、增量备份、备份验证、异地备份,这四个环节缺一不可。

为什么备份自动化这么重要?

你想想看,嵌入式设备往往部署在无人值守的环境里。工控机、车载系统、医疗设备……这些地方谁会天天盯着数据库?一旦出问题,手动恢复的成本高得吓人。

我个人习惯把备份策略分成四个层次:

  • 定时备份:按固定周期做全量备份,比如每天凌晨2点
  • 增量备份:在全量备份之间,只记录变化的数据
  • 备份验证:备份完了得检查文件是否完整、能否恢复
  • 异地备份:把备份文件传到另一个物理位置,防止单点故障

这四个层次叠加起来,才能算得上一个靠谱的备份方案。

核心架构:备份自动化流程

下面这张图是我自己总结的备份自动化流程,你可以把它当作一个模板来用:

数据库备份自动化流程图 定时器触发(cron) 是否全量? 全量备份 增量备份 备份验证 异地传输(SCP/RSYNC)

这个流程的核心思想是:定时触发 → 判断备份类型 → 执行备份 → 验证 → 异地传输。每一步都不能跳过,尤其是验证这一步,很多人会忽略。

定时备份:用cron搞定

在Linux嵌入式系统里,cron是最常用的定时任务工具。我一般这样配置:

# 每天凌晨2点执行全量备份
0 2 * * * /usr/local/bin/db_backup.sh full

# 每6小时执行一次增量备份
0 */6 * * * /usr/local/bin/db_backup.sh incremental

这里要注意一点:cron的环境变量和交互式shell不一样。我曾经踩过这个坑——脚本在终端跑得好好的,放到cron里就报错。后来发现是PATH没设置对。所以建议在脚本开头显式指定路径:

#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export DB_HOME=/opt/database

增量备份:只存变化的数据

全量备份虽然完整,但太占空间。增量备份只记录从上一次备份以来的变化,效率高得多。

在SQLite这种嵌入式数据库里,我常用的做法是:

  • 使用.backup命令做全量备份
  • 使用WAL(Write-Ahead Log)文件做增量备份
  • 记录每个表的最后修改时间戳

下面是一个简单的增量备份脚本示例:

#!/bin/bash
# 增量备份脚本 - 只备份当天修改过的表

DB_PATH="/data/mydb.db"
BACKUP_DIR="/backup/incremental/$(date +%Y%m%d)"
LAST_BACKUP="/backup/last_full.txt"

mkdir -p $BACKUP_DIR

# 获取上次全量备份时间
if [ -f "$LAST_BACKUP" ]; then
    LAST_TIME=$(cat $LAST_BACKUP)
else
    echo "错误:没有找到全量备份记录,请先执行全量备份"
    exit 1
fi

# 遍历所有表,检查修改时间
sqlite3 $DB_PATH ".tables" | tr ' ' '\n' | while read table; do
    MOD_TIME=$(sqlite3 $DB_PATH "SELECT max(mtime) FROM (SELECT mtime FROM $table)")
    if [ "$MOD_TIME" -gt "$LAST_TIME" ]; then
        echo "备份表: $table (修改时间: $MOD_TIME)"
        sqlite3 $DB_PATH ".dump $table" > "$BACKUP_DIR/${table}.sql"
    fi
done

echo "增量备份完成: $(date)"
小提示:增量备份虽然省空间,但恢复时依赖全量备份。我建议每周做一次全量备份,每天做增量备份,这样恢复时只需要「全量+最近增量」即可。

备份验证:别等到恢复时才发现文件坏了

嗯,这里我要重点说一下。备份验证是我见过被忽略最多的环节。很多人备份完了就扔在那,等到真要恢复时才发现文件损坏了。

我曾经在一个项目里遇到过:备份脚本跑了半年,文件都好好的,但有一次磁盘坏道导致备份文件损坏。因为没有验证机制,直到系统崩溃要恢复时才发现——那叫一个欲哭无泪。

验证的方法其实很简单:

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

BACKUP_FILE=$1
DB_CHECK="/usr/bin/sqlite3"

if [ ! -f "$BACKUP_FILE" ]; then
    echo "错误:备份文件不存在"
    exit 1
fi

# 1. 检查文件大小
FILE_SIZE=$(stat -c%s "$BACKUP_FILE")
if [ "$FILE_SIZE" -lt 1024 ]; then
    echo "警告:备份文件过小,可能不完整"
    exit 1
fi

# 2. 使用SQLite的完整性检查
$DB_CHECK "$BACKUP_FILE" "PRAGMA integrity_check;" > /dev/null 2>&1
if [ $? -ne 0 ]; then
    echo "错误:数据库完整性检查失败"
    exit 1
fi

# 3. 尝试恢复到一个临时位置
TEMP_DB="/tmp/verify_$$.db"
cp "$BACKUP_FILE" "$TEMP_DB"
$DB_CHECK "$TEMP_DB" ".tables" > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "验证通过:备份文件可正常读取"
    rm -f "$TEMP_DB"
else
    echo "错误:备份文件无法读取"
    rm -f "$TEMP_DB"
    exit 1
fi
警告:验证脚本一定要在备份完成后立即执行,不要等到第二天。我见过有人把验证任务放在cron里,结果备份失败了,验证还在跑——白白浪费了时间。

异地备份:别把所有鸡蛋放一个篮子里

异地备份说白了就是:本地一份,远程一份。万一机房着火了、硬盘坏了、被人删库了,你还有远程备份可以救命。

在嵌入式系统里,异地备份通常用SCP或RSYNC实现。我个人更推荐RSYNC,因为它支持增量传输,效率更高:

#!/bin/bash
# 异地备份传输脚本

LOCAL_BACKUP="/backup/full/$(date +%Y%m%d)/mydb.db"
REMOTE_HOST="backup@192.168.1.100"
REMOTE_PATH="/remote_backup/device_01/"

# 使用RSYNC传输,支持断点续传
rsync -avz --partial --progress \
    -e "ssh -i /root/.ssh/backup_key -o StrictHostKeyChecking=no" \
    "$LOCAL_BACKUP" \
    "${REMOTE_HOST}:${REMOTE_PATH}"

if [ $? -eq 0 ]; then
    echo "异地备份传输成功"
    # 传输成功后,在远程执行验证
    ssh -i /root/.ssh/backup_key "$REMOTE_HOST" \
        "/usr/local/bin/verify_backup.sh ${REMOTE_PATH}mydb.db"
else
    echo "异地备份传输失败,请检查网络"
    # 这里可以触发告警
fi

完整的备份自动化脚本

把上面这些整合起来,就是一个完整的备份自动化方案。下面是我在实际项目中用过的模板:

#!/bin/bash
# 完整备份自动化脚本
# 用法: ./backup_auto.sh [full|incremental]

MODE=$1
DB_PATH="/data/mydb.db"
BACKUP_ROOT="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/db_backup.log"

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

# 全量备份
do_full_backup() {
    local BACKUP_DIR="${BACKUP_ROOT}/full/${DATE}"
    mkdir -p $BACKUP_DIR
    
    log "开始全量备份..."
    sqlite3 $DB_PATH ".backup ${BACKUP_DIR}/mydb.db"
    
    # 记录全量备份时间
    echo "$(date +%s)" > "${BACKUP_ROOT}/last_full.txt"
    
    log "全量备份完成: ${BACKUP_DIR}/mydb.db"
}

# 增量备份
do_incremental_backup() {
    local BACKUP_DIR="${BACKUP_ROOT}/incremental/${DATE}"
    mkdir -p $BACKUP_DIR
    
    log "开始增量备份..."
    # 这里调用前面写的增量备份逻辑
    # ...
    
    log "增量备份完成"
}

# 验证备份
verify_backup() {
    local BACKUP_FILE=$1
    log "验证备份文件: $BACKUP_FILE"
    
    sqlite3 $BACKUP_FILE "PRAGMA integrity_check;" 2>/dev/null
    if [ $? -eq 0 ]; then
        log "验证通过"
        return 0
    else
        log "验证失败"
        return 1
    fi
}

# 异地传输
remote_transfer() {
    local BACKUP_FILE=$1
    log "开始异地传输..."
    
    rsync -avz --partial "$BACKUP_FILE" backup@remote:/backup/
    if [ $? -eq 0 ]; then
        log "异地传输成功"
    else
        log "异地传输失败"
    fi
}

# 主流程
case $MODE in
    full)
        do_full_backup
        BACKUP_FILE="${BACKUP_ROOT}/full/${DATE}/mydb.db"
        verify_backup $BACKUP_FILE && remote_transfer $BACKUP_FILE
        ;;
    incremental)
        do_incremental_backup
        BACKUP_FILE="${BACKUP_ROOT}/incremental/${DATE}/mydb.db"
        verify_backup $BACKUP_FILE && remote_transfer $BACKUP_FILE
        ;;
    *)
        echo "用法: $0 [full|incremental]"
        exit 1
        ;;
esac

备份策略对比

不同场景适合不同的备份策略,我整理了一个表格供你参考:

备份类型 空间占用 恢复速度 适用场景
全量备份 快(一次恢复) 每周一次,作为基础
增量备份 慢(需叠加全量) 每日执行,节省空间
差异备份 中(需叠加全量) 对恢复速度有要求的场景
我的建议:对于嵌入式数据库,推荐「每周全量 + 每日增量」的组合。全量备份放在凌晨业务低峰期,增量备份每6小时一次。这样既保证了数据安全,又不会占用太多存储空间。

最后说一句:备份不是做给别人看的,是给自己留后路的。你永远不知道系统什么时候会出问题,但你可以确保出了问题能快速恢复。这就是备份自动化的意义所在。


公众号:蓝海资料掘金营,微信deep3321