Linux: Phase 4 Mini Project——自动化备份脚本
Phase 4 的验收项目。综合运用进程管理、包管理、压缩归档和 Bash 脚本的全部技能,编写一个可用的自动化备份工具。
📋 前置知识:需要先掌握以下内容
1. 你将学到
- 脚本架构设计(函数封装)
- 增量备份策略
- 远程 rsync 同步
- 日志和错误处理
- cron 定时执行
2. 小明的服务器硬盘坏了
(1) 痛点:硬盘坏了,数据没了
小明的服务器硬盘突然坏了,丢失了 3 天的数据。他意识到没有备份——而且手动备份太麻烦,总忘记做。
(2) 自动化备份方案
他用 Phase 4 学到的知识写了一个完整的备份工具:
# 备份到本地
./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) 备份脚本架构设计
一个好的备份脚本应该模块化:
backup.sh
├── 配置加载
├── 检查(目录/依赖/空间)
├── 压缩(源 → tar.gz)
├── 同步(本地或远程 rsync)
├── 清理(删除过期备份)
├── 日志记录
└── 通知(成功/失败)
(2) 增量备份策略
# 方式一: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 定时
# 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
▶ 示例:检查备份空间
#!/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
▶ 示例:日志函数
#!/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" "备份完成"
▶ 示例:锁机制防重复执行
#!/bin/bash
LOCK_FILE="/tmp/backup.lock"
# 尝试获取锁
exec 200>"$LOCK_FILE"
flock -n 200 || {
echo "备份脚本已在运行 (锁文件: $LOCK_FILE)"
exit 1
}
# ... 备份逻辑 ...
# 自动释放锁(脚本退出时)
▶ 示例:清理过期备份
#!/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
}
▶ 综合示例:完整的备份工具脚本
#!/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 "✅ 备份操作完成"
# 使用示例
# 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 免密登录。
📖 小节
- 备份脚本架构:检查 → 压缩 → 同步 → 清理 → 日志
flock防止重复执行rsync --delete同步并删除目标端多余文件- 保留策略:
find ... -mtime +N -delete清理过期备份 - cron 定时:
0 2 * * * /path/to/backup.sh
📝 作业
- 基础题(难度⭐):创建一个测试目录和文件,用
backup.sh备份到/tmp/test-backup/,然后添加-e排除*.log文件,验证排除生效 - 进阶题(难度⭐⭐):配置 cron 每小时执行一次备份(观察 3 次以上),并将备份改为远程 rsync(如果有多台机器可用)
- 挑战题(难度⭐⭐⭐):在脚本中添加邮件或系统通知(
wall、mail或 webhook),并实现备份完整性校验(sha256sum),备份失败时自动告警