结论: 服务器数据定时备份的标准做法是"脚本 + 调度 + 轮转 + 异地 + 校验"五件套:用 rsync、tar 或数据库自带工具生成备份,交给 crontab 或 systemd timer 定时触发,按日/周/月做保留轮转,副本遵循 3-2-1 原则异地存放,并定期做恢复演练验证备份真的能还原。
引言:定时备份的难点不在"定时",而在"可靠"
给服务器加一条 crontab 很容易,五分钟就能跑起来。难的是三个月后你真的需要恢复时,发现备份目录里只有最近两天的数据、tar 包解压报 CRC 错误、或者备份文件和源数据在同一块盘上——盘坏了,两边一起没。
定时备份(Scheduled Backup)真正要解决的问题有四个:它会不会按时跑(调度可靠性)、跑失败了有人知道吗(告警)、保留多久(存储成本与恢复点权衡)、能不能还原(备份有效性)。本文以 Ubuntu 24.04 LTS 与 Rocky Linux 9 为例(截至 2026 年二者分别默认使用 cron/systemd timer 与 cronie/systemd timer),给出一套从策略到脚本、从轮转到校验的完整落地方案。
如果你的关注点是"备份体系怎么设计、RPO/RTO 怎么定、恢复演练怎么做",可以配合本站《服务器数据备份与恢复教程》一起看;本文的重心是调度与自动化——怎么让备份在没有人工干预的情况下稳定、可验证地跑下去。
一、先定策略:备份类型、副本布局与保留周期
在写脚本之前必须先回答三个问题,否则脚本写得再漂亮也是错的。
1.1 备份类型:全量、增量、差异怎么选
| 类型 | 含义 | 备份速度 | 恢复速度 | 存储占用 | 适用场景 |
|---|---|---|---|---|---|
| 全量(Full) | 每次都完整复制全部数据 | 最慢 | 最快(只需 1 份) | 最大 | 数据量 < 50 GB、恢复时效要求高 |
| 增量(Incremental) | 只备份上次任意类型备份之后的变化 | 最快 | 最慢(需全量 + 全部增量链) | 最小 | 数据量大、变更少、备份窗口短 |
| 差异(Differential) | 只备份上次全量备份之后的变化 | 中等 | 中等(全量 + 最新差异) | 中等 | 折中方案,恢复只需 2 份 |
| 永久增量(合成) | 首次全量,之后只传增量,服务端合成全量视图 | 快 | 快(服务端已合成) | 小(去重) | restic / borg / 商业备份软件 |
中小规模站点最省心的组合是"每周一次全量 + 每天一次增量",或者直接上支持去重的工具(restic、borgbackup),它用起来像永久全量、存储开销却接近增量。
1.2 副本布局:3-2-1 原则
3-2-1 原则是备份界的通用基线:3 份数据副本、2 种不同存储介质、1 份异地(Off-site)。2026 年面对勒索软件,通常升级为 3-2-1-1-0:
- 3 份副本:生产数据 + 本地备份 + 异地备份。
- 2 种介质:例如本地 SSD/NFS + 对象存储(S3 兼容)。
- 1 份异地:跨机房或跨云,地理隔离。
- 1 份不可变(Immutable)或离线:对象存储开启对象锁/版本控制,或磁带、离线硬盘,防勒索加密。
- 0 错误:定期自动校验,确保恢复零错误。
1.3 保留周期:一份可直接抄的基线
| 备份层级 | 频率 | 保留份数 | 作用 |
|---|---|---|---|
| 日备份 | 每天 02:00 | 保留 7 份 | 覆盖误删、误改的最近 7 天 |
| 周备份 | 每周日 03:00 | 保留 4 份 | 覆盖一个月内的逻辑错误 |
| 月备份 | 每月 1 日 04:00 | 保留 12 份 | 覆盖审计、合规与长期归档 |
| 数据库 binlog/WAL | 实时/每 5 分钟 | 保留 7~14 天 | 把 RPO 从 24 小时压缩到分钟级 |
注意保留周期与磁盘容量的关系:日备份保留 7 份 × 全量大小,很容易吃满磁盘。先算容量再定保留:假设全量 40 GB、日增量 3 GB,日备 7 份约 40 + 3×6 = 58 GB,周备 4 份约 160 GB,月备 12 份约 480 GB,总计约 700 GB,备份盘至少留 1 TB 并预留 30% 余量。
二、调度器选型:cron 与 systemd timer 怎么选
定时执行有两种主流方式,选择标准不是"哪个新",而是"你的备份需要什么能力"。
| 对比项 | crontab | systemd timer |
|---|---|---|
| 语法 | 分 时 日 月 周 五字段,简单 | 需写 .service + .timer 两个文件 |
| 错过补跑 | 不支持,机器关机就跳过 | 支持 Persistent=true,开机后补跑 |
| 日志 | 需自己重定向,默认发邮件 | 统一进 journald,journalctl -u xxx 查 |
| 超时控制 | 无 | 支持 TimeoutStartSec、RuntimeMaxSec |
| 资源限制 | 无 | 支持 CPUQuota、MemoryMax、IOWeight |
| 随机延迟 | 无 | RandomizedDelaySec 避免多机同时打备份存储 |
| 依赖管理 | 无 | 支持 After=、Requires= 等 |
结论:简单的文件同步用 crontab 足够;需要补跑、资源限制、统一日志的正式备份任务,用 systemd timer。
crontab 的写法与几个易错点:
# 编辑当前用户的计划任务
crontab -e
# 查看 / 删除
crontab -l
crontab -r
# 每天 02:00 执行备份脚本,输出与错误都追加到日志
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup/cron.log 2>&1
# 每 15 分钟同步一次数据库 binlog(注意 % 在 crontab 里是特殊字符,需转义)
*/15 * * * * /usr/local/bin/binlog_sync.sh >> /var/log/backup/binlog.log 2>&1cron 的四个高频坑:
- 环境变量缺失:cron 的 PATH 只有
/usr/bin:/bin,mysqldump、rclone装在/usr/local/bin会找不到。解决办法是在脚本里显式写export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,或用绝对路径。 - 不设置
SHELL与MAILTO:建议脚本首行写SHELL=/bin/bash、MAILTO=""避免邮件堆积。 - 并发重叠:备份耗时超过间隔时会叠加执行。用
flock加锁解决:flock -n /tmp/backup.lock -c '/usr/local/bin/backup.sh'。 - 没有失败告警:cron 失败是静默的,必须在脚本里主动上报。
systemd timer 的标准写法(以每日全量备为例):
# /etc/systemd/system/backup-daily.service
[Unit]
Description=Daily server data backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=2h
# 限制备份进程最多吃 2 核 CPU 与 2G 内存,避免影响线上业务
CPUQuota=200%
MemoryMax=2G
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
StandardOutput=journal
StandardError=journal# /etc/systemd/system/backup-daily.timer
[Unit]
Description=Run daily backup at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true # 关机错过则开机后补跑
RandomizedDelaySec=600 # 0~10 分钟随机延迟,避免多机并发打爆备份存储
Unit=backup-daily.service
[Install]
WantedBy=timers.targetsystemctl daemon-reload
systemctl enable --now backup-daily.timer
systemctl list-timers backup-daily.timer # 查看下次触发时间
journalctl -u backup-daily.service -f # 查看执行日志
systemctl start backup-daily.service # 手动触发一次(不打断计划)三、备份脚本实战:文件、数据库、配置
一个合格的备份脚本要包含:变量区、前置检查、备份动作、校验、清理、告警六段。下面给出可直接改造使用的骨架。
3.1 文件级备份:rsync 增量同步
rsync 是文件备份的主力,只在本地做硬链接快照时配合 --link-dest 可以实现"看起来是全量、实际只占增量空间"。
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SRC="/data/www /data/uploads /etc"
DEST="/backup"
DATE=$(date +%F-%H%M%S)
SNAP="$DEST/snapshots/$DATE"
LATEST="$DEST/snapshots/latest"
LOG="/var/log/backup/rsync-$DATE.log"
mkdir -p "$(dirname "$LOG")" "$DEST/snapshots"
log(){ echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
# 1. 前置检查:备份盘剩余空间不足 20% 直接失败
usage=$(df -P "$DEST" | awk 'NR==2{gsub("%","",$5); print $5}')
if [ "$usage" -gt 80 ]; then
log "ERROR: backup disk usage ${usage}% > 80%, abort"
exit 1
fi
# 2. 基于上一次快照做硬链接,未变化文件不占新空间
if [ -d "$LATEST" ]; then
LINK_DEST="--link-dest=$LATEST"
else
LINK_DEST=""
fi
mkdir -p "$SNAP"
# -a 归档模式保留权限/时间戳;-z 传输压缩(本地可去掉以省 CPU)
# --delete 让快照与源一致;--exclude 排除缓存与日志
rsync -a --delete $LINK_DEST \
--exclude='*.log' --exclude='cache/' --exclude='node_modules/' \
$SRC "$SNAP/" 2>&1 | tee -a "$LOG"
# 3. 校验:比对文件数量(更严格可加 -c 做 checksum 比对)
src_cnt=$(find $SRC -type f 2>/dev/null | wc -l)
dst_cnt=$(find "$SNAP" -type f | wc -l)
log "source files=$src_cnt, backup files=$dst_cnt"
[ "$dst_cnt" -lt "$src_cnt" ] && { log "ERROR: file count mismatch"; exit 1; }
# 4. 滚动 latest 指针
rm -f "$LATEST" && ln -s "$SNAP" "$LATEST"
log "OK: snapshot $SNAP finished"要点说明:
--link-dest是精髓:重复文件以硬链接指向上一份快照,10 天的日备看上去是 10 个全量目录,实际磁盘占用约等于 1 个全量 + 9 次增量。--delete有风险:若源目录被误删或勒索加密,同步会把"删除"也复制过去。因此备份目标必须是只追加的快照结构(每次一个日期目录),而不是一个被--delete反复覆盖的单目录。- 本地快照务必配合异地副本,否则硬盘故障就全没了。
3.2 数据库备份:mysqldump 与 pg_dump
数据库的定时备份不能用拷贝文件的方式(会拿到不一致的快照),必须用逻辑备份工具或物理备份工具。
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
BK_DIR="/backup/db"
DATE=$(date +%F-%H%M%S)
mkdir -p "$BK_DIR"
umask 077 # 备份文件权限 600,避免其他用户读取
# ---- MySQL 8.4 / MariaDB ----
# 凭据写入 ~/.my.cnf(权限 600),避免在命令行暴露密码
# [client]
# user=backup
# password=xxxxxx
mysqldump --defaults-file=/root/.my.cnf \
--single-transaction --quick --routines --triggers --events \
--set-gtid-purged=OFF --databases appdb \
| gzip > "$BK_DIR/mysql-appdb-$DATE.sql.gz"
# ---- PostgreSQL 16 ----
# 凭据用 ~/.pgpass(权限 600)或 PGPASSWORD 环境变量
PGPASSWORD='your_password' pg_dump -h 127.0.0.1 -U backup -Fc -d appdb \
> "$BK_DIR/pg-appdb-$DATE.dump"
# ---- 校验:gzip 完整性 + 非空 ----
for f in "$BK_DIR/mysql-appdb-$DATE.sql.gz"; do
gzip -t "$f" || { echo "ERROR: gzip corrupt $f"; exit 1; }
[ "$(stat -c%s "$f")" -lt 1024 ] && { echo "ERROR: dump too small"; exit 1; }
done
# ---- 保留 7 天 ----
find "$BK_DIR" -name '*.sql.gz' -mtime +7 -delete
find "$BK_DIR" -name '*.dump' -mtime +7 -delete关键参数说明:
--single-transaction:InnoDB 下通过一致性快照导出,不锁表,生产库必加。--routines --triggers --events:否则存储过程、触发器、事件不会被导出。pg_dump -Fc:自定义格式,支持pg_restore选择性恢复单表,比纯 SQL 文本更灵活。umask 077:数据库备份含全部业务数据,权限必须是 600。
3.3 打包归档:tar 与校验值
需要把备份整体归档投递到异地时,用 tar 打包并生成校验值:
# 打包(--exclude 排除无需备份的目录)
tar --exclude='/data/www/cache' -czpf /backup/full-$(date +%F).tar.gz /data/www /etc/nginx /etc/systemd/system
# 生成 SHA256 校验值,恢复前用它验证完整性
cd /backup && sha256sum full-$(date +%F).tar.gz > full-$(date +%F).tar.gz.sha256
# 校验
sha256sum -c full-$(date +%F).tar.gz.sha256
# 可选:用 GPG 对称加密(密码另存于密码管理器,不要写在脚本里)
gpg --batch --yes --symmetric --cipher-algo AES256 \
--passphrase-file /root/.backup-pass full-$(date +%F).tar.gz四、保留与轮转:自动清理过期备份
没有轮转的备份一定会撑爆磁盘。轮转策略要同时满足"保留数量足够"和"磁盘不会被写满",下面这个脚本实现了按日/周/月的三级轮转。
#!/usr/bin/env bash
# /usr/local/bin/backup-rotate.sh
set -Eeuo pipefail
BK_ROOT="/backup/snapshots"
LOG="/var/log/backup/rotate.log"
mkdir -p "$(dirname "$LOG")"
log(){ echo "[$(date '+%F %T')] $*" >> "$LOG"; }
# 1. 删除 7 天以上的日快照
find "$BK_ROOT" -maxdepth 1 -type d -name '20*' -mtime +7 -print -exec rm -rf {} + >> "$LOG" 2>&1
# 2. 保留每周日的快照为周备(保留 4 周)
# 把当天是周日的快照打标记,免于被日清理误删
for d in $(find "$BK_ROOT" -maxdepth 1 -type d -name '20*'); do
base=$(basename "$d")
dow=$(date -d "${base:0:10}" +%u 2>/dev/null || echo 0)
if [ "$dow" = "7" ]; then
touch "$d/.keep-weekly"
fi
dom=$(date -d "${base:0:10}" +%d 2>/dev/null || echo 0)
if [ "$dom" = "01" ]; then
touch "$d/.keep-monthly"
fi
done
# 3. 清理带标记但已超期的周备/月备
find "$BK_ROOT" -maxdepth 2 -name '.keep-weekly' -mtime +28 -print -delete >> "$LOG" 2>&1
find "$BK_ROOT" -maxdepth 2 -name '.keep-monthly' -mtime +365 -print -delete >> "$LOG" 2>&1
# 4. 容量兜底:超过 85% 时从最老开始删,直到降到 80% 以下
usage=$(df -P "$BK_ROOT" | awk 'NR==2{gsub("%","",$5); print $5}')
while [ "$usage" -gt 85 ]; do
oldest=$(find "$BK_ROOT" -maxdepth 1 -type d -name '20*' | sort | head -1)
[ -z "$oldest" ] && break
log "disk ${usage}% > 85%, remove oldest snapshot $oldest"
rm -rf "$oldest"
usage=$(df -P "$BK_ROOT" | awk 'NR==2{gsub("%","",$5); print $5}')
done
log "rotate done, current usage ${usage}%"轮转的三条原则:清理动作必须打日志(否则误删无据可查)、必须有容量兜底(防止保留策略算错导致磁盘满)、删除前先确认最新一份备份已成功(脚本应读取上一次备份的状态标记,失败时不清理)。
五、异地备份:本地快照再加一份远程副本
本地备份只能防误删和软件故障,防不了机房火灾、硬盘报废和勒索软件。异地副本是 3-2-1 里的那个"1"。
# 方案 A:rclone 同步到 S3 兼容对象存储(最通用,支持阿里云 OSS、腾讯云 COS、MinIO)
rclone config # 交互式创建 remote,命名为 remote-oss
# 首次全量,之后增量(只传变化文件)
rclone sync /backup/snapshots remote-oss:server-backup/snapshots \
--transfers 8 --checkers 16 --fast-list \
--log-file /var/log/backup/rclone.log --log-level INFO
# 开启服务端加密(crypt remote)后,即使对象存储泄露也读不到明文
rclone config # 新建 crypt 类型 remote,指向 remote-oss:server-backup
# 方案 B:restic(去重 + 加密 + 快照,推荐用于"既要增量又要省空间"的场景)
restic init --repo s3:https://s3.example.com/bucket/restic
export RESTIC_PASSWORD='强密码'
restic -r s3:https://s3.example.com/bucket/restic backup /data/www /etc \
--tag daily --exclude='/data/www/cache'
# 保留策略:7 天 / 4 周 / 12 月,--prune 真正释放空间
restic -r s3:https://s3.example.com/bucket/restic forget \
--keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
# 校验(只抽查部分数据,全量校验去掉 --read-data-subset)
restic -r s3:https://s3.example.com/bucket/restic check --read-data-subset 10%
# 方案 C:borgbackup(同样去重加密,SSH 远程更方便)
borg init --encryption=repokey-blake2 ssh://backup@10.0.0.99:22/./repo
borg create --stats --compression zstd ssh://backup@10.0.0.99:22/./repo::{now:%Y-%m-%d} /data/www
borg prune --keep-daily=7 --keep-weekly=4 --keep-monthly=12 ssh://backup@10.0.0.99:22/./repo异地备份的四个安全细节:
- 权限最小化:备份用的对象存储子账号只给写入与列举权限,不要给删除权限,否则攻击者入侵后可顺手删掉你的备份。
- 开启版本控制或对象锁:勒索软件会尝试加密/覆盖远端备份,开启后即使被覆盖也能回滚到历史版本。
- 加密密钥与备份分开放:密钥放密码管理器或离线保管,不要放在同一台服务器上,也不要硬编码进脚本。
- 异地也要告警:
rclone/restic失败时同样要发通知,建议在脚本末尾根据退出码调用 webhook。
六、校验与恢复演练:备份唯一的合法性证明
没做过恢复验证的备份,等于没有备份。 定时备份体系最后一块拼图是"可验证"。
#!/usr/bin/env bash
# /usr/local/bin/backup-verify.sh —— 每周一次自动恢复演练
set -Eeuo pipefail
LATEST=$(readlink -f /backup/snapshots/latest)
TESTDIR=/tmp/restore-test-$$
mkdir -p "$TESTDIR"
# 1. 解压/还原到临时目录
if [ -f "$LATEST/../full-$(date +%F).tar.gz" ]; then
tar -xzf "$LATEST/../full-$(date +%F).tar.gz" -C "$TESTDIR"
fi
# 2. 抽样比对:随机挑 20 个文件做 checksum 对比
mismatch=0
for f in $(find "$TESTDIR" -type f | shuf -n 20 2>/dev/null); do
rel=${f#$TESTDIR}
[ -f "$rel" ] || { continue; }
a=$(sha256sum "$f" | awk '{print $1}')
b=$(sha256sum "$rel" 2>/dev/null | awk '{print $1}')
[ "$a" = "$b" ] || { echo "MISMATCH: $rel"; mismatch=$((mismatch+1)); }
done
# 3. 数据库还原演练(还原到临时实例,不要覆盖生产库)
zcat /backup/db/mysql-appdb-$(date +%F)*.sql.gz 2>/dev/null | head -50 | grep -q "CREATE TABLE" \
&& echo "mysql dump structure OK" || echo "mysql dump structure CHECK FAILED"
rm -rf "$TESTDIR"
echo "verify done, mismatch=$mismatch"
[ "$mismatch" -eq 0 ] && exit 0 || exit 1演练频率建议:核心业务每月一次完整恢复演练(还原到独立环境并跑通业务冒烟测试),普通数据每季度一次。演练必须记录:耗时(对应 RTO)、数据丢失窗口(对应 RPO)、失败原因。
企业QQ咨询




