深色模式
备份脚本
摘要:备份脚本只有三件事:打包、传走、清理旧备份。但每一步都有坑——打包要校验完整性、上传要验证可下载、清理要防止误删全部。本文给出一个带校验与保留策略的完整脚本。
适用环境
bash
which tar gzip mysqldump aws || echo "按需安装"
mkdir -p /data/backup /var/log/ops
printf 'db.sql\n' > /tmp/demo.sql1
2
3
2
3
操作步骤
一、设计:命名、保留、三要素
text
命名格式:<应用>-<类型>-<YYYYmmdd-HHMMSS>.tar.gz
保留策略:本地保留 7 天,对象存储按生命周期分层(见云存储章节)
三要素 :打包完整性校验 + 上传后校验 + 过期清理1
2
3
2
3
二、打包:压缩并留校验值
bash
#!/usr/bin/env bash
# backup.sh - 打包 + 上传 + 清理
set -euo pipefail
APP_NAME="${APP_NAME:-app}"
SRC_DIR="${SRC_DIR:-/data/app}"
BACKUP_DIR="${BACKUP_DIR:-/data/backup}"
KEEP_DAYS="${KEEP_DAYS:-7}"
BUCKET="${BUCKET:?需要设置 BUCKET 环境变量}"
TS="$(date +%Y%m%d-%H%M%S)"
FILE="${BACKUP_DIR}/${APP_NAME}-${TS}.tar.gz"
mkdir -p "$BACKUP_DIR"
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
# 1) 打包(排除缓存与临时目录)
log "打包 $SRC_DIR -> $FILE"
tar --exclude='*/cache/*' --exclude='*.tmp' \
-czf "$FILE" -C "$(dirname "$SRC_DIR")" "$(basename "$SRC_DIR")"
# 2) 完整性校验:先本地解压测试,再记录校验值
log "校验压缩包"
tar -tzf "$FILE" >/dev/null
sha256sum "$FILE" | tee "${FILE}.sha256"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
注意
tar -czf 退出码为 0 不代表压缩包一定完好。tar -tzf 做一次列目录测试能立刻发现「打包过程中文件被改写」或「磁盘写满导致截断」等问题,成本极低。
三、数据库备份:先导出再打包
bash
# MySQL:--single-transaction 保证一致性,不锁表
mysqldump --single-transaction --routines --triggers \
-h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" \
| gzip > "${BACKUP_DIR}/${APP_NAME}-db-${TS}.sql.gz"
# PostgreSQL
pg_dump -h "$DB_HOST" -U "$DB_USER" "$DB_NAME" \
| gzip > "${BACKUP_DIR}/${APP_NAME}-db-${TS}.sql.gz"
# 校验导出非空(导出失败时可能只有 0 字节)
size=$(stat -c %s "${BACKUP_DIR}/${APP_NAME}-db-${TS}.sql.gz")
(( size > 1024 )) || { log "导出文件过小(${size}B),判定失败"; exit 1; }1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
危险
-p"$DB_PASS" 会让密码出现在 ps 输出里。更安全的方式是把密码写入权限为 600 的 ~/.my.cnf,或用 MYSQL_PWD 环境变量传入,并在脚本结束后 unset。
四、上传到对象存储
bash
log "上传到 s3://${BUCKET}/${APP_NAME}/"
aws s3 cp "$FILE" "s3://${BUCKET}/${APP_NAME}/" --only-show-errors
aws s3 cp "${FILE}.sha256" "s3://${BUCKET}/${APP_NAME}/" --only-show-errors
# 上传后校验:比对远端与本地的大小、ETag/校验值
remote_size=$(aws s3api head-object --bucket "$BUCKET" \
--key "${APP_NAME}/$(basename "$FILE")" --query ContentLength --output text)
local_size=$(stat -c %s "$FILE")
[[ "$remote_size" == "$local_size" ]] || { log "大小不一致: local=$local_size remote=$remote_size"; exit 1; }
log "上传校验通过 ($local_size bytes)"1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
五、清理过期备份(本地)
bash
log "清理 ${KEEP_DAYS} 天前的本地备份"
# 先预览再删除,避免误删
find "$BACKUP_DIR" -type f -name "${APP_NAME}-*.tar.gz" -mtime "+${KEEP_DAYS}" -print
find "$BACKUP_DIR" -type f -name "${APP_NAME}-*.tar.gz" -mtime "+${KEEP_DAYS}" -delete
find "$BACKUP_DIR" -type f -name "*.sha256" -mtime "+${KEEP_DAYS}" -delete1
2
3
4
5
2
3
4
5
危险
find -delete 不可逆。必须满足三点才可用:① 删除范围用 -name 严格限定为本脚本产生的文件名模式;② 先用 -print 预览;③ 远端已存在可用副本。绝不要用 find / -delete 这类宽泛条件。
六、远端保留交给生命周期
bash
# 本地只留 7 天,远端靠桶生命周期规则转低频/归档/过期
aws s3api get-bucket-lifecycle-configuration --bucket "$BUCKET" \
--query "Rules[].{ID:ID,Expiration:Expiration,Transitions:Transitions}" --output table1
2
3
2
3
七、运行与定时
bash
export BUCKET=my-backup-bucket
./backup.sh
# 每天凌晨 2:10 执行
10 2 * * * BUCKET=my-backup-bucket /usr/local/bin/backup.sh >> /var/log/ops/backup.log 2>&11
2
3
4
5
2
3
4
5
八、最重要的一步:恢复演练
bash
# 从远端拉回最新备份并解包验证
LATEST=$(aws s3 ls "s3://${BUCKET}/${APP_NAME}/" | awk '{print $4}' | grep '\.tar\.gz$' | sort | tail -1)
aws s3 cp "s3://${BUCKET}/${APP_NAME}/${LATEST}" /tmp/
mkdir -p /tmp/restore-check && tar -xzf "/tmp/${LATEST}" -C /tmp/restore-check
find /tmp/restore-check -type f | head -5
sha256sum -c "/tmp/${LATEST%.tar.gz}.sha256" 2>/dev/null || \
sha256sum "/tmp/${LATEST}"1
2
3
4
5
6
7
2
3
4
5
6
7
没做过恢复演练的备份,在真正需要时往往是不可用的。建议每月演练一次并记录耗时。
验证
- [ ]
tar -tzf校验通过,.sha256文件已生成 - [ ] 上传后远端大小与本地一致
- [ ] 把
KEEP_DAYS设为 0 后运行,旧备份被清理且未误删其它文件 - [ ] 从远端拉回一份备份能成功解压
- [ ] 脚本失败时退出码非零,cron 日志中有可读错误
常见坑
- 只备份不校验:等需要恢复时才发现文件是空或损坏。
- 备份写到同一块盘:磁盘故障会同时毁掉源数据和备份,必须异地/对象存储。
- 清理条件太宽:
-mtime +7配-name '*'会删掉别人的文件。 - 忽略磁盘空间:打包前不检查剩余空间,打一半失败留下残缺文件。
- 备份脚本没有告警:连续失败一个月无人知晓,直到真出事。