Linux: Phase 4 Mini Project——自动化备份脚本

Phase 4 的验收项目。综合运用进程管理、包管理、压缩归档和 Bash 脚本的全部技能,编写一个可用的自动化备份工具。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 小明的服务器硬盘坏了

(1) 痛点:硬盘坏了,数据没了

小明的服务器硬盘突然坏了,丢失了 3 天的数据。他意识到没有备份——而且手动备份太麻烦,总忘记做。

(2) 自动化备份方案

他用 Phase 4 学到的知识写了一个完整的备份工具:

BASH
# 备份到本地
./backup.sh -s /var/www -d /backup

# 备份到远程
./backup.sh -s /var/www -d user@backup:/backups

# 加入 cron 每天执行
0 2 * * * /opt/scripts/backup.sh -s /var/www -d /backup

(3) 收益:从此不怕丢数据

备份脚本每天凌晨 2 点自动执行,增量备份到远程。小明终于可以安心睡觉了。


3. 知识点讲解

(1) 备份脚本架构设计

一个好的备份脚本应该模块化:

TEXT 📖 仅展示

backup.sh
├── 配置加载
├── 检查(目录/依赖/空间)
├── 压缩(源 → tar.gz)
├── 同步(本地或远程 rsync)
├── 清理(删除过期备份)
├── 日志记录
└── 通知(成功/失败)

(2) 增量备份策略

TEXT 📖 仅展示
# 方式一:rsync --link-dest(推荐)
# 利用硬链接,每天一个完整目录,但只存储变化部分
rsync -av --link-dest=/backup/yesterday/ /data/ /backup/today/

> 💡 **提示**:`rsync --link-dest` 的增量备份原理是利用硬链接——未变化的文件在"今天"的备份中只是指向"昨天"文件的硬链接,不占额外磁盘空间;只有变化的文件才真正写入新数据。外观上每天都是完整目录,但磁盘占用接近增量。

# 方式二:rsync 增量传输
# rsync 默认只传输变化的部分
rsync -avz --delete /data/ user@host:/backup/

> ⚠️ **注意**:`rsync --delete` 会让目标端与源端完全一致——目标端有但源端没有的文件会被删除。如果源路径写错(如漏写末尾的 `/`),可能导致目标端被清空。建议先用 `--dry-run` 预览操作,确认无误后再执行。

# 方式三:带日期的命名
backup-2026-07-07.tar.gz
backup-2026-07-08.tar.gz  # 两个文件互相独立,但占用更多空间

ℹ️ 说明:专业备份策略遵循"3-2-1 原则"——3 份数据副本、2 种不同存储介质、1 份异地备份。本地备份 + 远程 rsync 就是一个 3-2-1 方案的基础。仅靠本地 tar.gz 不够,硬盘损坏会同时丢失源数据和备份。

(3) cron 定时

TEXT 📖 仅展示
# cron 格式:分 时 日 月 周 命令
# 每天凌晨 2:00 执行备份
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1

# 每小时执行
0 * * * * /opt/scripts/backup-hourly.sh

# 每周一凌晨 3:00
0 3 * * 1 /opt/scripts/backup-weekly.sh

▶ 示例:检查备份空间

BASH
#!/bin/bash
# 检查备份目录是否有足够空间

check_space() {
    local dest="$1"
    local min_gb="${2:-1}"
    
    local available=$(df -BG "$dest" | tail -1 | awk '{print $4}' | tr -d 'G')
    
    if [ "$available" -lt "$min_gb" ]; then
        echo "错误: 磁盘空间不足 (可用: ${available}G, 需要: ${min_gb}G)"
        return 1
    fi
    
    echo "磁盘空间充足: ${available}G"
    return 0
}

输出:

TEXT 📖 仅展示
磁盘空间充足: 50G

▶ 示例:日志函数

TEXT 📖 仅展示
#!/bin/bash

LOG_FILE="/var/log/backup.log"

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

log "INFO" "备份开始"
log "ERROR" "备份失败: 磁盘空间不足"
log "INFO" "备份完成"

▶ 示例:锁机制防重复执行

TEXT 📖 仅展示
#!/bin/bash

LOCK_FILE="/tmp/backup.lock"

# 尝试获取锁
exec 200>"$LOCK_FILE"
flock -n 200 || {
    echo "备份脚本已在运行 (锁文件: $LOCK_FILE)"
    exit 1
}

# ... 备份逻辑 ...

# 自动释放锁(脚本退出时)

▶ 示例:清理过期备份

TEXT 📖 仅展示
#!/bin/bash

cleanup_old_backups() {
    local backup_dir="$1"
    local retention_days="${2:-30}"
    
    log "INFO" "清理 $retention_days 天前的备份..."
    
    local count=0
    while IFS= read -r -d '' file; do
        rm -f "$file"
        count=$((count + 1))
    done < <(find "$backup_dir" -name "*.tar.gz" -type f -mtime +$retention_days -print0)
    
    if [ $count -gt 0 ]; then
        log "INFO" "清理了 $count 个过期文件"
    fi
}

▶ 综合示例:完整的备份工具脚本

BASH
#!/bin/bash
# backup.sh - 自动化备份工具
# 用法: ./backup.sh -s <源目录> -d <目标目录> [-r <保留天数>] [-e <排除模式>] [-v]

set -euo pipefail

# ====== 配置 ======
VERSION="1.0.0"
LOG_FILE="/var/log/backup.log"
LOCK_FILE="/tmp/backup.lock"
DEFAULT_RETENTION=30

# ====== 颜色 ======
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# ====== 函数 ======

usage() {
    cat << EOF
备份工具 v${VERSION}

用法: $0 -s <源目录> -d <目标目录> [选项]

必需参数:
  -s <源目录>      要备份的源目录
  -d <目标目录>    备份目标(本地路径或 user@host:/path)

选项:
  -r <天数>        备份保留天数 (默认: $DEFAULT_RETENTION)
  -e <模式>        排除模式 (可多次使用,如 -e node_modules -e .git)
  -v               详细输出
  -t               测试模式(不实际创建备份)
  -h               显示帮助
EOF
    exit 0
}

log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

info()    { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
warn()    { log "WARN" "${YELLOW}$1${NC}"; }
error()   { log "ERROR" "${RED}$1${NC}"; }

check_deps() {
    for cmd in rsync tar; do
        if ! command -v "$cmd" &> /dev/null; then
            error "所需命令 '$cmd' 未找到"
            exit 1
        fi
    done
}

check_disk_space() {
    local dir="$1"
    local min_gb="${2:-1}"
    
    if [[ "$dir" != *:* ]]; then
        local avail=$(df -BG "$dir" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
        if [ "$avail" -lt "$min_gb" ]; then
            error "磁盘空间不足: 可用 ${avail}G, 需要 ${min_gb}G"
            return 1
        fi
        info "磁盘空间: ${avail}G 可用"
    fi
}

cleanup_old() {
    local dir="$1"
    local days="$2"
    
    if [[ "$dir" != *:* ]]; then
        local count=$(find "$dir" -name "*.tar.gz" -type f -mtime +$days -delete -print | wc -l)
        if [ $count -gt 0 ]; then
            info "清理了 $count 个过期备份"
        fi
    fi
}

# ====== 锁机制 ======
exec 200>"$LOCK_FILE"
flock -n 200 || {
    error "备份脚本已在运行 (PID: $(cat $LOCK_FILE 2>/dev/null))"
    exit 1
}
echo $$ > "$LOCK_FILE"

# ====== 信号处理 ======
cleanup() {
    rm -f "$LOCK_FILE"
    info "备份脚本退出"
}
trap cleanup EXIT INT TERM

# ====== 参数解析 ======
SOURCE=""
DEST=""
RETENTION=$DEFAULT_RETENTION
EXCLUDE=()
VERBOSE=false
DRY_RUN=false

while getopts "s:d:r:e:vth" opt; do
    case $opt in
        s) SOURCE="$OPTARG" ;;
        d) DEST="$OPTARG" ;;
        r) RETENTION="$OPTARG" ;;
        e) EXCLUDE+=("$OPTARG") ;;
        v) VERBOSE=true ;;
        t) DRY_RUN=true ;;
        h) usage ;;
        *) usage ;;
    esac
done

# ====== 验证 ======
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
    error "必须指定 -s(源目录)和 -d(目标目录)"
    usage
fi

if [ ! -d "$SOURCE" ]; then
    error "源目录 '$SOURCE' 不存在"
    exit 1
fi

# ====== 主逻辑 ======
info "========================================"
info "备份工具 v${VERSION}"
info "源目录: $SOURCE"
info "目标: $DEST"
info "保留天数: $RETENTION"
if [ "$DRY_RUN" = true ]; then
    warn "测试模式 - 不会实际创建备份"
fi
info "========================================"

# 1. 检查依赖和空间
check_deps
check_disk_space "$DEST"

# 2. 生成文件名
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$(basename "$SOURCE")_${TIMESTAMP}.tar.gz"

# 3. 构建排除参数
EXCLUDE_ARGS=""
for pattern in "${EXCLUDE[@]}"; do
    EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern"
done

# 4. 创建压缩备份
if [ "$DRY_RUN" = false ]; then
    info "创建压缩备份: $BACKUP_NAME"
    
    # 本地备份
    if [[ "$DEST" != *:* ]]; then
        mkdir -p "$DEST"
        eval tar -czf "${DEST}/${BACKUP_NAME}" $EXCLUDE_ARGS "$SOURCE"
        success "✅ 本地备份完成: ${DEST}/${BACKUP_NAME}"
        
        # 同步到远程
        if [ "$VERBOSE" = true ]; then
            ls -lh "${DEST}/${BACKUP_NAME}"
        fi
    else
        # 远程备份 - 先本地压缩再 rsync 过去
        local_temp="/tmp/${BACKUP_NAME}"
        eval tar -czf "$local_temp" $EXCLUDE_ARGS "$SOURCE"
        info "传输到远程: $DEST"
        rsync -avz --progress "$local_temp" "${DEST}/${BACKUP_NAME}"
        rm -f "$local_temp"
        success "✅ 远程备份完成: ${DEST}/${BACKUP_NAME}"
    fi
else
    warn "测试模式: 跳过备份创建"
fi

# 5. 清理过期备份
if [ "$DRY_RUN" = false ] && [ $RETENTION -gt 0 ]; then
    cleanup_old "$DEST" $RETENTION
fi

# 6. 备份摘要
info "备份摘要:"
info "  源: $SOURCE"
info "  目标: $DEST"
if [ "$DRY_RUN" = false ]; then
    info "  文件: ${BACKUP_NAME}"
    if [[ "$DEST" != *:* ]]; then
        info "  大小: $(du -h "${DEST}/${BACKUP_NAME}" | cut -f1)"
    fi
fi
info "========================================"

success "✅ 备份操作完成"
TEXT 📖 仅展示
# 使用示例

# 1. 基本用法
./backup.sh -s /var/www/myapp -d /backup

# 2. 排除目录 + 保留 7 天
./backup.sh -s /var/www/myapp -d /backup -e node_modules -e .git -r 7

# 3. 远程备份
./backup.sh -s /var/www -d user@backup-server:/backups -v

# 4. 测试模式(不实际备份)
./backup.sh -s /var/www -d /backup -t

# 5. 加入 cron(每天凌晨 2 点)
crontab -e
# 添加:
0 2 * * * /opt/scripts/backup.sh -s /var/www/myapp -d /backup -e node_modules -r 30 > /dev/null 2>&1

❓ 常见问题

Q: 怎么防止备份脚本重复执行? A: 用文件锁(flock):脚本启动时尝试创建锁文件,如果锁已存在则退出。这样即使 cron 在备份未完成时再次触发,不会同时跑两个备份。

Q: 备份文件保留多久? A: 取决于存储空间和合规要求。常见策略:日备份保留 30 天、周备份保留 3 个月、月备份保留 1 年。用 --link-dest 或 crontab 的不同频率实现。

Q: 怎么校验备份的完整性? A: 可以用 sha256sum 生成校验文件:sha256sum backup.tar.gz > backup.sha256。恢复时用 sha256sum -c backup.sha256 验证。

Q: rsync 传输中断会重传吗? A: 会。rsync 默认增量传输,即使是正在传输的文件中断了,下次运行 rsync 会只传输未完成的部分。加上 --partial 选项保留部分传输的文件。

Q: 备份到远程服务器需要配置什么? A: 1)SSH 密钥认证(ssh-keygen + ssh-copy-id)。2)远程目标目录存在。3)远程用户有写入权限。4)可选:配置 SSH 免密登录。


📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个测试目录和文件,用 backup.sh 备份到 /tmp/test-backup/,然后添加 -e 排除 *.log 文件,验证排除生效
  2. 进阶题(难度⭐⭐):配置 cron 每小时执行一次备份(观察 3 次以上),并将备份改为远程 rsync(如果有多台机器可用)
  3. 挑战题(难度⭐⭐⭐):在脚本中添加邮件或系统通知(wallmail 或 webhook),并实现备份完整性校验(sha256sum),备份失败时自动告警
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏