服务器数据怎么定时备份

结论: 服务器数据定时备份的标准做法是"脚本 + 调度 + 轮转 + 异地 + 校验"五件套:用 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 怎么选

定时执行有两种主流方式,选择标准不是"哪个新",而是"你的备份需要什么能力"。

对比项crontabsystemd timer
语法分 时 日 月 周 五字段,简单需写 .service + .timer 两个文件
错过补跑不支持,机器关机就跳过支持 Persistent=true,开机后补跑
日志需自己重定向,默认发邮件统一进 journald,journalctl -u xxx 查
超时控制无支持 TimeoutStartSec、RuntimeMaxSec
资源限制无支持 CPUQuota、MemoryMax、IOWeight
随机延迟无RandomizedDelaySec 避免多机同时打备份存储
依赖管理无支持 After=、Requires= 等

结论:简单的文件同步用 crontab 足够;需要补跑、资源限制、统一日志的正式备份任务,用 systemd timer。

crontab 的写法与几个易错点:

bash
# 编辑当前用户的计划任务
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>&1

cron 的四个高频坑:

  1. 环境变量缺失:cron 的 PATH 只有 /usr/bin:/bin,mysqldump、rclone 装在 /usr/local/bin 会找不到。解决办法是在脚本里显式写 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,或用绝对路径。
  2. 不设置 SHELL 与 MAILTO:建议脚本首行写 SHELL=/bin/bash、MAILTO="" 避免邮件堆积。
  3. 并发重叠:备份耗时超过间隔时会叠加执行。用 flock 加锁解决:flock -n /tmp/backup.lock -c '/usr/local/bin/backup.sh'。
  4. 没有失败告警:cron 失败是静默的,必须在脚本里主动上报。

systemd timer 的标准写法(以每日全量备为例):

ini
# /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
ini
# /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.target
bash
systemctl 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 可以实现"看起来是全量、实际只占增量空间"。

bash
#!/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

数据库的定时备份不能用拷贝文件的方式(会拿到不一致的快照),必须用逻辑备份工具或物理备份工具。

bash
#!/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 打包并生成校验值:

bash
# 打包(--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

四、保留与轮转:自动清理过期备份

没有轮转的备份一定会撑爆磁盘。轮转策略要同时满足"保留数量足够"和"磁盘不会被写满",下面这个脚本实现了按日/周/月的三级轮转。

bash
#!/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"。

bash
# 方案 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

异地备份的四个安全细节:

  1. 权限最小化:备份用的对象存储子账号只给写入与列举权限,不要给删除权限,否则攻击者入侵后可顺手删掉你的备份。
  2. 开启版本控制或对象锁:勒索软件会尝试加密/覆盖远端备份,开启后即使被覆盖也能回滚到历史版本。
  3. 加密密钥与备份分开放:密钥放密码管理器或离线保管,不要放在同一台服务器上,也不要硬编码进脚本。
  4. 异地也要告警:rclone / restic 失败时同样要发通知,建议在脚本末尾根据退出码调用 webhook。

六、校验与恢复演练:备份唯一的合法性证明

没做过恢复验证的备份,等于没有备份。 定时备份体系最后一块拼图是"可验证"。

bash
#!/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)、失败原因。