欢迎光临

VPS数据备份与灾难恢复实战指南:从自动备份到异地容灾全解

为什么VPS备份如此重要?

VPS服务器数据备份与灾难恢复概念图
很多VPS用户都有过这样的惨痛经历:一个误操作执行了

1
rm -rf /

,或者数据库被勒索病毒加密,又或者云服务商突然发邮件说底层物理机故障需要重建实例——如果没有备份,几个月甚至几年的数据瞬间化为乌有。

根据2025年的一项针对中小型技术团队的调查,超过60%的VPS用户没有完善的自动备份策略,其中近三成的人曾经因数据丢失遭受过重大损失。本文将从零开始,手把手教你搭建一套完整、自动、可靠的VPS数据备份与灾难恢复体系。

无论你是个人开发者还是小团队运维,这套方案都能帮你做到:

  • 每天自动备份关键数据,无需人工干预
  • 支持本地快照、远程存储、异地容灾三级备份
  • 出现故障时能在15分钟内完整恢复
  • 备份数据加密传输,确保安全合规

备份策略设计:3-2-1原则

在动手操作之前,我们先来理解业界公认的备份黄金法则——3-2-1备份原则

要素 说明 我们的实现方案
3份副本 数据至少保留3个副本 生产数据 + 本地备份 + 远程备份
2种介质 使用至少两种不同的存储介质 VPS本地磁盘 + 对象存储(S3/OSS)
1份异地 至少一份备份在异地 跨区域对象存储或另一台VPS

针对不同类型的VPS数据,我们还需要采用不同的备份频率和保留策略:

数据类型 备份频率 保留周期 备份方式
数据库(MySQL/PostgreSQL) 每天一次全量 + 每6小时增量 30天
1
mysqldump

/

1
pg_dump
网站文件(/var/www) 每天一次 14天
1
rsync

+ tar压缩

配置目录(/etc) 每次变更后(或每天) 90天 etckeeper / tar
Docker数据卷 每天一次 7天 docker volume backup
系统快照 每周一次 4周 云商快照API / LVM快照

第一步:本地备份脚本

我们从一个通用的备份脚本开始,它涵盖了VPS上最常见的几类数据。创建

1
/usr/local/bin/backup.sh


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/bin/bash
# VPS通用备份脚本 v2.0
set -euo pipefail

BACKUP_ROOT="/var/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=${RETENTION_DAYS:-14}
BACKUP_NAME="backup_${DATE}"

# 颜色输出函数
info() { echo -e "\033[36m[INFO]\033[0m $1"; }
ok()   { echo -e "\033[32m[OK]\033[0m $1"; }
err()  { echo -e "\033[31m[ERROR]\033[0m $1" >&2; }

mkdir -p "${BACKUP_ROOT}/${BACKUP_NAME}"
BACKUP_DIR="${BACKUP_ROOT}/${BACKUP_NAME}"

# 1. 备份 MySQL 数据库
backup_mysql() {
    if ! command -v mysql &>/dev/null; then
        info "MySQL未安装,跳过数据库备份"
        return 0
    fi
    info "开始备份 MySQL 数据库..."
    MYSQL_CNF="${HOME}/.my.cnf"
    if [ ! -f "$MYSQL_CNF" ]; then
        err "未找到 MySQL 配置文件 ${MYSQL_CNF},跳过"
        return 1
    fi
    DATABASES=$(mysql --defaults-file="$MYSQL_CNF" -e \
        "SELECT schema_name FROM information_schema.schemata \
         WHERE schema_name NOT IN ('mysql','performance_schema','sys','information_schema');" \
        -B -N 2>/dev/null) || true

    for db in $DATABASES; do
        info "  备份数据库: ${db}"
        mysqldump --defaults-file="$MYSQL_CNF" \
            --single-transaction --quick --lock-tables=false \
            --routines --triggers --events \
            "${db}" | gzip > "${BACKUP_DIR}/mysql_${db}.sql.gz"
        ok "  ${db} 备份完成 ($(du -h "${BACKUP_DIR}/mysql_${db}.sql.gz" | cut -f1))"
    done
}

# 2. 备份 PostgreSQL 数据库
backup_postgres() {
    if ! command -v psql &>/dev/null; then
        info "PostgreSQL未安装,跳过"
        return 0
    fi
    info "开始备份 PostgreSQL 数据库..."
    PG_USER="${PG_USER:-postgres}"
    DATABASES=$(su - "${PG_USER}" -c "psql -t -c 'SELECT datname FROM pg_database WHERE datistemplate = false;'" 2>/dev/null | grep -v '^$' | tr -d ' ') || true
   
    for db in $DATABASES; do
        info "  备份数据库: ${db}"
        su - "${PG_USER}" -c "pg_dump ${db} --no-owner --compress=9" \
            > "${BACKUP_DIR}/pgsql_${db}.sql.gz" 2>/dev/null
        ok "  ${db} 备份完成 ($(du -h "${BACKUP_DIR}/pgsql_${db}.sql.gz" | cut -f1))"
    done
}

# 3. 备份网站文件和配置
backup_files() {
    info "开始备份网站文件和系统配置..."
   
    WEBSITES=("/var/www" "/etc/nginx" "/etc/apache2")
    for dir in "${WEBSITES[@]}"; do
        if [ -d "$dir" ]; then
            local name=$(echo "$dir" | tr '/' '_')
            tar czf "${BACKUP_DIR}/files${name}.tar.gz" \
                --exclude="cache" --exclude="*.log" --exclude="node_modules" \
                -C / "${dir#/}" 2>/dev/null
            ok "  ${dir} 备份完成 ($(du -h "${BACKUP_DIR}/files${name}.tar.gz" | cut -f1))"
        fi
    done
}

# 4. 备份 Docker 数据卷
backup_docker_volumes() {
    if ! command -v docker &>/dev/null; then
        info "Docker未安装,跳过"
        return 0
    fi
    info "开始备份 Docker 数据卷..."
   
    VOLUMES=$(docker volume ls -q)
    for vol in $VOLUMES; do
        info "  备份卷: ${vol}"
        docker run --rm -v "${vol}:/source:ro" \
            -v "${BACKUP_DIR}:/backup:rw" alpine \
            tar czf "/backup/docker_vol_${vol}.tar.gz" \
            -C /source . 2>/dev/null
        ok "  ${vol} 备份完成 ($(du -h "${BACKUP_DIR}/docker_vol_${vol}.tar.gz" | cut -f1))"
    done
}

# 5. 生成备份清单
generate_manifest() {
    echo "# 备份清单 - ${DATE}" > "${BACKUP_DIR}/MANIFEST.txt"
    echo "# 主机: $(hostname)" >> "${BACKUP_DIR}/MANIFEST.txt"
    echo "# IP: $(curl -s ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')" >> "${BACKUP_DIR}/MANIFEST.txt"
    echo "---" >> "${BACKUP_DIR}/MANIFEST.txt"
    ls -lh "${BACKUP_DIR}" | grep -v MANIFEST >> "${BACKUP_DIR}/MANIFEST.txt"
    ok "备份清单已生成"
}

# 6. 清理过期备份
cleanup_old() {
    info "清理 ${RETENTION_DAYS} 天前的旧备份..."
    find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "backup_*" -mtime "+${RETENTION_DAYS}" -exec rm -rf {} \;
    local removed=$(find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "backup_*" -mtime "+${RETENTION_DAYS}" 2>/dev/null)
    if [ -z "$removed" ]; then
        ok "旧备份清理完成"
    fi
}

# 主流程
info "===== 开始 VPS 备份 (${DATE}) ====="
backup_mysql
backup_postgres
backup_files
backup_docker_volumes
generate_manifest
cleanup_old
ok "===== 备份完成 ====="
echo "备份目录: ${BACKUP_DIR}"
du -sh "${BACKUP_DIR}"

保存后赋予执行权限并测试:


1
2
3
chmod +x /usr/local/bin/backup.sh
# 首次运行测试
sudo /usr/local/bin/backup.sh

第二步:远程备份到对象存储

本地备份只解决了”2份副本”中的一部分,我们还需要异地存储来实现完整的3-2-1策略。这里推荐使用 rclone——一个支持几乎所有云存储协议的命令行工具。

安装与配置rclone


1
2
3
4
5
6
7
8
9
10
11
12
# 安装 rclone
sudo apt install rclone -y   # Debian/Ubuntu
# sudo yum install rclone -y  # CentOS/RHEL

# 交互式配置(选择你的存储提供商)
rclone config

# 支持的提供商包括:
# - Backblaze B2(最便宜,$0.006/GB/月)
# - Wasabi($0.0069/GB/月,无出站费)
# - AWS S3 / Cloudflare R2 / 阿里云OSS / 腾讯COS
# - Google Drive / OneDrive 等个人云盘

配置完成后,我们来写一个上传脚本

1
/usr/local/bin/sync_remote.sh


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
# 远程同步脚本 - 在本地备份完成后调用
set -euo pipefail

BACKUP_ROOT="/var/backups"
RCLONE_REMOTE="backup-vps"        # rclone remote 名称
RCLONE_PATH="vps-backups"         # 远程存储路径
ENCRYPT_PASSWORD="${ENCRYPT_PASSWORD:-}"  # 加密密码(可选)

# 查找最新备份目录
LATEST=$(ls -td "${BACKUP_ROOT}"/backup_* 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
    echo "[ERROR] 没有找到备份目录"
    exit 1
fi

echo "[INFO] 同步 ${LATEST} 到远程存储..."

if [ -n "$ENCRYPT_PASSWORD" ]; then
    # 加密后上传(推荐)
    tar czf - -C "$LATEST" . | \
        openssl enc -aes-256-cbc -salt -pass "pass:${ENCRYPT_PASSWORD}" | \
        rclone rcat "${RCLONE_REMOTE}:${RCLONE_PATH}/$(basename ${LATEST}).enc.tar.gz"
    echo "[OK] 加密上传完成"
else
    # 直接上传(不加密,适合信任的存储商)
    rclone copy "$LATEST" "${RCLONE_REMOTE}:${RCLONE_PATH}/$(basename ${LATEST})" \
        --progress --transfers=4 --checkers=8
    echo "[OK] 直接上传完成"
fi

# 清理远程过期备份(保留30天)
rclone delete "${RCLONE_REMOTE}:${RCLONE_PATH}" \
    --min-age 30d --verbose 2>&1 | grep -v "Couldn't find"
echo "[OK] 远程过期备份已清理"

1
2
3
chmod +x /usr/local/bin/sync_remote.sh
# 测试上传
sudo /usr/local/bin/sync_remote.sh

第三步:自动备份定时任务

手动运行备份不是长久之计,我们用 systemd timer 来替代传统的cron——更加可靠,且支持日志记录、依赖管理和失败通知。

创建 systemd service

创建

1
/etc/systemd/system/vps-backup.service


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=VPS Daily Backup Service
Documentation=https://tbr8.org/vps-backup
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/local/bin/sync_remote.sh
StandardOutput=journal
StandardError=journal
# 超时保护:超过2小时的备份任务自动终止
TimeoutStartSec=7200
# OOM保护
OOMScoreAdjust=-500

创建 systemd timer

创建

1
/etc/systemd/system/vps-backup.timer


1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Daily VPS Backup Timer
Requires=vps-backup.service

[Timer]
# 每天凌晨3点执行(避开业务高峰期)
OnCalendar=daily
FixedRandomDelay=30m
Persistent=true

[Install]
WantedBy=timers.target

启用定时器:


1
2
3
4
5
6
7
8
9
10
11
12
13
sudo systemctl daemon-reload
sudo systemctl enable vps-backup.timer
sudo systemctl start vps-backup.timer

# 检查状态
sudo systemctl status vps-backup.timer
# 查看下次执行时间
sudo systemctl list-timers vps-backup.timer

# 手动触发测试
sudo systemctl start vps-backup.service
# 查看执行日志
sudo journalctl -u vps-backup.service -f

第四步:灾难恢复手册

备份的价值只有在恢复时才能体现。这里我们准备一份完整的灾难恢复流程,建议打印出来贴在工位上。

场景一:数据库损坏


1
2
3
4
5
6
7
8
9
# 从本地恢复
gunzip < /var/backups/backup_20260711/mysql_mydb.sql.gz | mysql -u root -p mydb

# 从远程加密备份恢复
rclone cat backup-vps:vps-backups/backup_20260711.enc.tar.gz | \
    openssl enc -d -aes-256-cbc -salt -pass "pass:${PASSWORD}" | \
    tar xz -C /tmp/restore
# 然后导入
gunzip < /tmp/restore/mysql_mydb.sql.gz | mysql -u root -p mydb

场景二:整机迁移

当需要迁移到新的VPS时,完整流程如下:

  1. 在新VPS上安装相同版本的操作系统和软件栈(Nginx/MySQL/PHP等)
  2. 在新VPS上安装rclone并配置相同的remote
  3. 拉取最新的加密备份:
    1
    rclone copy backup-vps:vps-backups/backup_latest.enc.tar.gz ./
  4. 解密并解压到临时目录
  5. 按需恢复数据库、网站文件、配置文件
  6. 修改DNS记录指向新VPS的IP
  7. 验证服务正常运行后,旧VPS保留一周再下线

场景三:文件被勒索加密


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 立即隔离受感染VPS
sudo systemctl stop nginx php-fpm mysql

# 2. 从远程备份恢复(不连接本地,避免二次感染)
rclone copy backup-vps:vps-backups/backup_20260710.enc.tar.gz /tmp/recovery/
cd /tmp/recovery
openssl enc -d -aes-256-cbc -salt -pass "pass:${PASSWORD}" \
    -in backup_20260710.enc.tar.gz | tar xz

# 3. 恢复到干净目录
cp -a /tmp/recovery/files_var_www.tar.gz /var/
cd /var && tar xzf files_var_www.tar.gz

# 4. 恢复数据库
gunzip < /tmp/recovery/mysql_mydb.sql.gz | mysql -u root -p mydb

# 5. 重启服务并检查
sudo systemctl start mysql nginx php-fpm

第五步:监控与告警

自动备份如果没有监控,就等于没有备份——你可能直到需要恢复时才发现已经连续两周备份失败了。

集成告警通知

在备份脚本末尾添加通知逻辑,使用

1
curl

发送到多个渠道:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在 backup.sh 末尾添加
notify_success() {
    local msg="[VPS备份成功] $(hostname) | 大小: $(du -sh ${BACKUP_DIR} | cut -f1) | 时间: $(date)"
   
    # Telegram Bot 通知
    curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
        -d "chat_id=${TELEGRAM_CHAT_ID}&text=${msg}" &>/dev/null
   
    # 钉钉/企业微信 Webhook
    curl -s -X POST "${DINGTALK_WEBHOOK}" \
        -H "Content-Type: application/json" \
        -d "{"msgtype":"text","text":{"content":"${msg}"}}" &>/dev/null
}

notify_failure() {
    local err_msg="[VPS备份失败] $(hostname) | 错误: $1"
    # 同上渠道发送告警
    # 建议同时发送邮件或用 Pushover/Slack
}

备份健康检查脚本

创建一个独立的健康检查脚本

1
/usr/local/bin/check_backup_health.sh

,每天在备份后运行:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
# 备份健康检查
BACKUP_ROOT="/var/backups"
WARN_IF_OLDER_THAN=28  # 小时

LATEST_BACKUP=$(ls -td ${BACKUP_ROOT}/backup_* 2>/dev/null | head -1)

if [ -z "$LATEST_BACKUP" ]; then
    echo "CRITICAL: 没有任何备份存在!"
    exit 2
fi

BACKUP_TIME=$(stat -c %Y "$LATEST_BACKUP")
CURRENT_TIME=$(date +%s)
AGE_HOURS=$(( (CURRENT_TIME - BACKUP_TIME) / 3600 ))

if [ $AGE_HOURS -gt $WARN_IF_OLDER_THAN ]; then
    echo "WARNING: 最新备份已超过 ${AGE_HOURS} 小时"
    exit 1
fi

BACKUP_SIZE=$(du -sb "$LATEST_BACKUP" | cut -f1)
if [ "$BACKUP_SIZE" -lt 1024 ]; then  # 小于1KB视为异常
    echo "CRITICAL: 备份大小异常(仅 ${BACKUP_SIZE} 字节)"
    exit 2
fi

echo "OK: 备份正常,${AGE_HOURS}小时前创建,大小: $(numfmt --to=iec $BACKUP_SIZE)"
exit 0

进阶:数据库实时备份(Binlog)

对于关键业务数据库,每天一次的备份可能还不够。我们可以开启MySQL的binlog来实现任意时间点恢复(PITR)

开启Binlog

编辑

1
/etc/mysql/mysql.conf.d/mysqld.cnf

(或

1
/etc/my.cnf

):


1
2
3
4
5
6
[mysqld]
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 100M
server-id = 1

重启MySQL后,我们可以结合全量备份和binlog实现任意时间点恢复:


1
2
3
4
5
6
7
8
9
10
# 1. 先恢复最近的完整备份
mysql -u root -p mydb < full_backup.sql

# 2. 应用binlog到指定时间点
mysqlbinlog --stop-datetime="2026-07-11 14:30:00" \
    /var/log/mysql/mysql-bin.000045 | mysql -u root -p mydb

# 3. 或者恢复到指定位置
mysqlbinlog --stop-position=1234567 \
    /var/log/mysql/mysql-bin.000045 | mysql -u root -p mydb

总结

一套完善的VPS备份体系应该包含以下要素:

  • 自动化:定时任务自动执行,无需人工操作
  • 分层存储:本地快照 + 远程加密存储 + 异地容灾
  • 监控告警:备份失败必须能第一时间通知到人
  • 定期演练:每月至少做一次恢复测试,确保备份可用
  • 文档化:把恢复流程写成文档,紧急时照做即可

最后分享一个血泪教训:没有经过恢复测试的备份等于没有备份。很多人配置好备份后就再也不管了,直到某天需要恢复才发现备份文件损坏、加密密码丢失、存储空间已满——请务必每月做一次完整的恢复演练。

如果你按照本文的步骤完成配置,那么你的VPS数据安全等级已经超过了95%的同类用户。数据无价,备份无价。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » VPS数据备份与灾难恢复实战指南:从自动备份到异地容灾全解
分享到: 更多 (0)