在资源有限的情况下,很多开发者通常是单机服务器,那么如何做数据备份就成了一个问题。

数据库备份看起来是一件简单的事情:定时导出数据库,将备份文件上传到对象存储,需要时再下载恢复。

但真正放到生产环境中,会遇到不少问题:

  • 数据库正在写入,直接复制数据文件是否会产生不一致?
  • 数据库备份文件越来越大,如何减少存储空间和上传流量?
  • 服务器意外关机,错过的定时任务能否自动补执行?
  • 备份文件存放在 OSS 或 R2 中,如何保证数据安全?
  • 备份任务显示成功,是否意味着数据库真的可以恢复?

本文介绍一套基于 Restic + systemd Timer + 对象存储 的自动化数据库备份方案。

数据库以 PostgreSQL、MySQL 和 Redis 为例,远程存储分别使用 Cloudflare R2 和阿里云 OSS。通过数据库原生工具生成备份文件,再交给 Restic 进行加密、去重和快照管理,最后利用 systemd Timer 定时执行。

这套方案适合单机或少量 Linux 服务器上的数据库备份,也可以作为进一步建设企业备份体系的基础。

一、整体架构与技术选型

1.1 整体架构

整个备份流程分为四个部分:

  1. 数据库备份:使用数据库原生工具生成一致性备份文件。
  2. 本地暂存:将备份文件写入本地临时目录,并进行完整性检查。
  3. Restic 备份:对备份文件进行加密、数据去重和 Snapshot 管理。
  4. 对象存储:将 Restic 仓库保存到 Cloudflare R2 或阿里云 OSS。

整体架构如下:

                 systemd Timer
                       |
                       v
                数据库备份脚本
                       |
          +------------+------------+
          |            |            |
          v            v            v
      PostgreSQL      MySQL        Redis
       pg_dump       mysqldump     BGSAVE
          |            |            |
          +------------+------------+
                       |
                       v
                  本地备份目录
                       |
                       v
                    Restic
              +--------+--------+
              |                 |
              v                 v
         Cloudflare R2       阿里云 OSS
          Restic 仓库         Restic 仓库

这里需要特别说明:Restic 不是数据库备份工具,而是负责备份文件的存储和生命周期管理。

数据库原生工具负责保证备份内容符合数据库自身的数据一致性要求;Restic 负责将这些备份文件可靠地保存到远程存储。

1.2 技术组件

组件主要职责
PostgreSQL pg_dumpPostgreSQL 逻辑备份
MySQL mysqldumpMySQL 逻辑备份
Redis BGSAVE生成 Redis RDB 快照
systemd Timer定时调度备份任务
Restic加密、去重、快照管理
Cloudflare R2S3 兼容对象存储
阿里云 OSSS3 兼容对象存储

1.3 为什么选择 Restic?

Restic 是一个开源的跨平台备份工具,支持本地目录、SFTP、S3 兼容对象存储等多种存储后端。

它比较适合这类备份场景,主要有以下特点:

  • 增量备份:通过内容寻址和数据去重,避免重复保存相同的数据块。
  • 端到端加密:备份数据在上传前加密,对象存储服务无法直接读取数据库备份内容。
  • Snapshot 管理:每次备份生成一个快照,可以查看、比较和恢复历史版本。
  • 多种存储后端:支持 Cloudflare R2、阿里云 OSS 等 S3 兼容服务。
  • 自动清理:通过 forget 和 prune 管理历史快照和不再引用的数据块。

需要注意,Restic 的增量备份并不意味着每次都只读取发生变化的文件。对于发生变化的大型压缩文件,Restic 仍然需要扫描内容并进行分块处理。

1.4 为什么选择 systemd Timer?

Linux 上常见的定时任务工具是 Cron,不过对于服务器备份任务,systemd Timer 也有不少优势:

  • 可以与 systemd Service 配合,独立管理任务执行。
  • 能够通过 journalctl 查看任务日志。
  • 可以通过 Persistent=true 处理错过的日历定时触发。
  • 支持随机延迟,避免多台服务器同时启动备份。
  • 可以通过 Service 配置执行用户、资源限制和运行超时。

Restic 本身不负责定时调度,因此使用 systemd Timer 来管理备份周期比较合适。

二、安装与基础环境准备

以下示例以 Ubuntu 22.04/24.04、Debian 12 等使用 systemd 的 Linux 发行版为参考。

2.1 安装 Restic

Ubuntu / Debian 可以通过包管理器安装:

apt update
apt install -y restic

检查版本:

restic version

生产环境建议固定经过验证的 Restic 版本,不要在备份服务器上无计划地自动升级。

如果发行版仓库中的版本较旧,可以从 Restic 官方发布渠道安装对应版本。

2.2 创建备份目录

创建统一的备份目录:

mkdir -p /backup/database
mkdir -p /var/log/database-backup
mkdir -p /etc/restic

建议目录结构:

/backup/
└── database/
    ├── postgresql/
    ├── mysql/
    └── redis/

/etc/restic/
├── r2.env
└── oss.env

/usr/local/bin/
├── backup-postgresql.sh
├── backup-mysql.sh
├── backup-redis.sh
└── backup-all.sh

其中:

  • /backup/database:存放数据库备份文件。
  • /etc/restic:存放 Restic 仓库和认证配置。
  • /usr/local/bin:存放备份脚本。

备份目录应当使用专用文件系统或预留足够的磁盘空间,避免数据库备份占满系统盘。

2.3 配置备份权限

备份目录包含数据库数据,必须限制访问权限。

chmod 700 /backup
chmod 700 /backup/database
chmod 700 /etc/restic
chmod 700 /usr/local/bin/backup-*.sh

如果使用专门的备份用户运行任务,需要根据数据库连接方式、文件所有权和 Restic 缓存目录配置相应权限。

不要为了方便直接给备份目录设置 777 权限。

三、配置 Restic 对接 Cloudflare R2 和阿里云 OSS

Cloudflare R2 和阿里云 OSS 都提供 S3 兼容 API,因此可以使用 Restic 的 S3 后端。

但是,两者的 Endpoint 和 Region 配置不同。

3.1 Cloudflare R2 配置

首先在 Cloudflare 控制台创建 R2 Bucket。

然后创建用于 S3 API 访问的 API Token,建议:

  • 只授予 Object Read & Write 权限。
  • 将权限限制在指定 Bucket。
  • 不使用账户级别的过大权限。
  • 将 Access Key ID 和 Secret Access Key 保存到安全位置。

R2 的 S3 API Endpoint 格式为:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com

其中 <ACCOUNT_ID> 是 Cloudflare 账户 ID。

创建配置文件:

vim /etc/restic/r2.env

内容如下:

RESTIC_REPOSITORY="s3:https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-backup-bucket/database"

AWS_ACCESS_KEY_ID="你的R2_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY="你的R2_SECRET_ACCESS_KEY"
AWS_DEFAULT_REGION="auto"

RESTIC_PASSWORD_FILE="/etc/restic/password"

这里的 my-backup-bucket 是 R2 Bucket 名称,database 是仓库路径前缀。

R2 的 S3 API 使用 auto 作为 Region。

3.2 阿里云 OSS 配置

首先在阿里云 OSS 控制台创建 Bucket。

然后创建 RAM 用户,并为其配置专用访问权限。

建议将权限限制在备份专用 Bucket,避免使用拥有整个阿里云账户管理权限的 AccessKey。

以杭州地域为例:

vim /etc/restic/oss.env

配置内容:

RESTIC_REPOSITORY="s3:https://oss-cn-hangzhou.aliyuncs.com/my-backup-bucket/database"

AWS_ACCESS_KEY_ID="你的OSS_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY="你的OSS_ACCESS_KEY_SECRET"
AWS_DEFAULT_REGION="cn-hangzhou"

RESTIC_PASSWORD_FILE="/etc/restic/password"

注意:

  • oss-cn-hangzhou.aliyuncs.com 是杭州地域的 OSS Endpoint 示例。
  • cn-hangzhou 是对应的地域标识。
  • Bucket 名称和地域必须匹配。
  • 实际使用时应以 Bucket 所属地域和官方 S3 兼容接口文档为准。

Restic 官方文档提供了阿里云 OSS 的 S3 兼容配置方式,必要时可以显式指定 s3.bucket-lookup=dns 和 s3.region。

例如:

restic \
  -o s3.bucket-lookup=dns \
  -o s3.region=cn-hangzhou \
  snapshots

如果使用这组参数初始化仓库,后续相关操作也应保持相同的后端配置。

3.3 创建 Restic 仓库密码

Restic 使用独立的仓库密码加密备份数据。

创建密码文件:

openssl rand -base64 48 > /etc/restic/password

设置权限:

chmod 600 /etc/restic/password
chmod 600 /etc/restic/r2.env
chmod 600 /etc/restic/oss.env

这里有两个不同的凭据:

凭据用途
R2 / OSS AccessKey访问对象存储
RESTIC_PASSWORD解密 Restic 仓库数据

二者不能混为一谈。

特别注意:Restic 仓库密码丢失后,通常无法恢复仓库中的备份数据。

建议将仓库密码通过独立的密码管理系统或离线安全介质保存,不能只保存在被备份的服务器上。

3.4 初始化 Restic 仓库

分别初始化 R2 和 OSS 仓库。

初始化 R2:

set -a
source /etc/restic/r2.env
set +a

restic init

初始化 OSS:

set -a
source /etc/restic/oss.env
set +a

restic \
  -o s3.bucket-lookup=dns \
  -o s3.region=cn-hangzhou \
  init

初始化成功后,可以查看仓库快照:

restic snapshots

这里需要强调一个设计问题:

R2 和 OSS 应当是两个独立的 Restic 仓库。

如果需要同时备份到两个存储服务,应当分别向两个仓库执行备份操作,而不是认为 Restic 会自动完成双写或跨仓库同步。

四、PostgreSQL 数据库备份

PostgreSQL 提供了 pg_dump、pg_dumpall、pg_basebackup 等多种备份方式。

对于中小型数据库,逻辑备份通常比较容易实施。

4.1 使用 pg_dump 进行逻辑备份

pg_dump 可以在数据库正常运行期间导出一致性快照,不需要停止数据库服务。

推荐使用 Custom 格式:

pg_dump \
  -Fc \
  -d my_database \
  -f /backup/database/postgresql/my_database.dump

其中:

  • -Fc:使用 PostgreSQL Custom Archive 格式。
  • -d:指定数据库。
  • -f:指定输出文件。

Custom 格式可以通过 pg_restore 进行选择性恢复,也支持并行恢复。

如果数据库较大,可以使用 Directory 格式:

pg_dump \
  -Fd \
  -j 4 \
  -d my_database \
  -f /backup/database/postgresql/my_database

-Fd 支持并行导出,适合需要缩短备份时间的场景。

4.2 备份整个 PostgreSQL 实例

需要注意,pg_dump 一次只备份一个数据库,不包含整个 PostgreSQL 实例的所有全局对象。

如果需要备份角色、表空间等全局对象,可以额外执行:

pg_dumpall \
  --globals-only \
  > /backup/database/postgresql/globals.sql

如果实例中存在多个业务数据库,需要分别执行 pg_dump,或者使用 pg_dumpall 导出整个数据库集群的逻辑内容。

生产环境建议将数据库备份和全局对象备份分开管理。

4.3 PostgreSQL 备份脚本

创建:

vim /usr/local/bin/backup-postgresql.sh

示例:

#!/usr/bin/env bash

set -Eeuo pipefail

BACKUP_DIR="/backup/database/postgresql"
DATE="$(date +%Y%m%d_%H%M%S)"

mkdir -p "$BACKUP_DIR"

DB_NAME="my_database"

DUMP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.dump"
GLOBALS_FILE="${BACKUP_DIR}/globals_${DATE}.sql"

cleanup() {
    rm -f "$DUMP_FILE" "$GLOBALS_FILE"
}

trap cleanup EXIT

# 备份数据库
pg_dump \
    -Fc \
    -d "$DB_NAME" \
    -f "$DUMP_FILE"

# 备份全局对象
pg_dumpall \
    --globals-only \
    > "$GLOBALS_FILE"

# 检查备份文件
test -s "$DUMP_FILE"
test -s "$GLOBALS_FILE"

pg_restore --list "$DUMP_FILE" > /dev/null

echo "PostgreSQL backup completed: ${DATE}"

配置 PostgreSQL 连接认证时,推荐使用 .pgpass 文件或受保护的密码管理机制。

例如:

localhost:5432:my_database:backup_user:your_password

设置权限:

chmod 600 ~/.pgpass

不要直接将数据库密码写在脚本的命令行参数中。

另外,pg_dump 客户端版本应与目标 PostgreSQL 服务端版本匹配,至少要满足官方支持的版本兼容要求。生产环境建议使用与目标数据库相同主版本的客户端。

4.4 PostgreSQL 如何恢复?

恢复全局对象:

psql -X -d postgres -f globals.sql

恢复数据库:

createdb my_database

pg_restore \
  -d my_database \
  my_database.dump

如果需要并行恢复,可以使用:

pg_restore \
  -j 4 \
  -d my_database \
  my_database.dump

需要注意,恢复前应准备好目标数据库、角色和必要的扩展。

4.5 PostgreSQL 生产环境注意事项

pg_dump 是逻辑备份工具,不等于完整的 PostgreSQL 灾难恢复方案。

如果数据库规模较大,或者业务要求较低的 RPO(恢复点目标),建议考虑:

  • pg_basebackup:进行物理基础备份。
  • WAL 归档:保存预写日志。
  • pgBackRest 或 Barman:管理物理备份、增量备份和时间点恢复。

如果需要恢复到某个具体时间点,仅依赖每天执行一次 pg_dump 是不够的,需要结合物理备份和连续 WAL 归档。

五、MySQL 数据库备份

MySQL 常见备份方式包括逻辑备份和物理备份。

本文使用 mysqldump 演示中小型数据库的逻辑备份。

5.1 使用 mysqldump 备份

示例:

mysqldump \
  --single-transaction \
  --quick \
  --routines \
  --triggers \
  --events \
  --all-databases \
  > /backup/database/mysql/mysql.sql

主要参数:

参数作用
--single-transaction使用事务快照进行一致性导出
--quick逐行读取数据,减少客户端内存占用
--routines导出存储过程和函数
--triggers导出触发器
--events导出事件
--all-databases导出所有数据库

这里有一个重要限制:

--single-transaction 主要适用于 InnoDB 等支持事务一致性读的存储引擎。MyISAM、MEMORY 等非事务表无法获得相同的一致性保证。

此外,在备份期间应避免执行 ALTER TABLE、DROP TABLE、RENAME TABLE 等 DDL 操作,否则可能导致备份结果不一致或失败。

如果数据库启用了 GTID,还需要根据 gtid_mode、gtid_purged 和当前 MySQL 版本检查备份所需权限及相关参数,不能直接照搬所有环境的命令。

5.2 配置 MySQL 认证

建议使用 MySQL 客户端配置文件保存备份账号凭据。

例如:

[client]
user=backup_user
password=your_password
host=127.0.0.1
port=3306

保存为:

/root/.my.cnf

设置权限:

chmod 600 /root/.my.cnf

实际生产环境应创建专用备份账号,并根据备份范围授予必要权限,而不是直接使用 root 账号。

5.3 MySQL 备份脚本

创建:

vim /usr/local/bin/backup-mysql.sh

示例:

#!/usr/bin/env bash

set -Eeuo pipefail

BACKUP_DIR="/backup/database/mysql"
DATE="$(date +%Y%m%d_%H%M%S)"

mkdir -p "$BACKUP_DIR"

BACKUP_FILE="${BACKUP_DIR}/mysql_${DATE}.sql.gz"

cleanup() {
    rm -f "$BACKUP_FILE"
}

trap cleanup EXIT

# 导出并压缩数据库
mysqldump \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    --all-databases \
    | gzip -c > "$BACKUP_FILE"

# 检查文件是否存在
test -s "$BACKUP_FILE"

# 检查 gzip 文件完整性
gzip -t "$BACKUP_FILE"

echo "MySQL backup completed: ${DATE}"

这里使用了:

set -Eeuo pipefail

其中 pipefail 非常重要。

如果 mysqldump 执行失败,不能只根据 gzip 是否正常退出判断整个备份成功。启用 pipefail 后,管道中的失败可以正确传递给脚本。

5.4 MySQL 如何恢复?

先解压,再导入:

gunzip -c mysql_20261103_020000.sql.gz \
  | mysql

恢复前应确认目标实例版本、字符集、用户权限和系统变量等条件。

5.5 MySQL 生产环境注意事项

mysqldump 属于逻辑备份方式,优点是可移植性较好,但数据库规模较大时,备份和恢复时间可能比较长。

对于较大的 MySQL 数据库,可以考虑:

  • Percona XtraBackup:进行物理热备份。
  • MySQL Enterprise Backup:MySQL 官方商业备份方案。
  • MySQL Shell Dump Utilities:支持并行导出、压缩和更丰富的导入能力。

如果业务要求通过 Binlog 实现时间点恢复,还需要单独保存和管理 Binlog 文件。

不要将每天一次的 mysqldump 误认为已经具备完整的 PITR(Point-in-Time Recovery)能力。

六、Redis 数据备份

Redis 与 PostgreSQL、MySQL 的备份方式有所不同。

Redis 本身提供 RDB 和 AOF 两种持久化机制:

  • RDB:将某个时间点的数据集保存为快照文件。
  • AOF:记录写操作,通过重放命令恢复数据。

对于 Restic 备份场景,推荐优先使用 RDB 文件。

6.1 使用 BGSAVE 生成 RDB

Redis 可以通过以下命令生成后台 RDB 快照:

redis-cli BGSAVE

Redis 会创建子进程生成快照,主进程继续处理客户端请求。

RDB 文件生成完成后,会通过原子重命名替换旧文件。因此,复制已经生成完成的 RDB 文件是安全的,不需要停止 Redis 服务。

可以通过以下命令查看持久化状态:

redis-cli INFO persistence

重点关注:

rdb_bgsave_in_progress
rdb_last_bgsave_status
rdb_last_save_time

判断快照完成时,应确认:

rdb_bgsave_in_progress:0
rdb_last_bgsave_status:ok

不要在 BGSAVE 尚未完成时直接复制临时文件。

6.2 确认 RDB 文件位置

Redis 的 RDB 文件位置由配置决定。

查看相关配置:

redis-cli CONFIG GET dir
redis-cli CONFIG GET dbfilename

例如:

dir
/var/lib/redis

dbfilename
dump.rdb

那么 RDB 文件路径就是:

/var/lib/redis/dump.rdb

如果使用 Docker、Redis Sentinel 或其他部署方式,实际路径可能不同,应以运行实例的配置为准。

6.3 Redis 备份脚本

创建:

vim /usr/local/bin/backup-redis.sh

示例:

#!/usr/bin/env bash

set -Eeuo pipefail

BACKUP_DIR="/backup/database/redis"
DATE="$(date +%Y%m%d_%H%M%S)"

REDIS_CLI="/usr/bin/redis-cli"

mkdir -p "$BACKUP_DIR"

# 获取 RDB 文件位置
REDIS_DIR="$(
    "$REDIS_CLI" --raw CONFIG GET dir | tail -n 1
)"

RDB_NAME="$(
    "$REDIS_CLI" --raw CONFIG GET dbfilename | tail -n 1
)"

RDB_FILE="${REDIS_DIR}/${RDB_NAME}"
BACKUP_FILE="${BACKUP_DIR}/dump_${DATE}.rdb"

# 触发后台 RDB 快照
"$REDIS_CLI" BGSAVE

# 等待快照完成
for i in $(seq 1 300); do
    STATUS="$(
        "$REDIS_CLI" --raw INFO persistence
    )"

    if echo "$STATUS" | grep -q '^rdb_bgsave_in_progress:0'; then
        if echo "$STATUS" | grep -q '^rdb_last_bgsave_status:ok'; then
            break
        fi

        echo "Redis BGSAVE failed"
        exit 1
    fi

    sleep 1
done

# 超时检查
STATUS="$("$REDIS_CLI" --raw INFO persistence)"

if ! echo "$STATUS" | grep -q '^rdb_bgsave_in_progress:0'; then
    echo "Redis BGSAVE timeout"
    exit 1
fi

if ! echo "$STATUS" | grep -q '^rdb_last_bgsave_status:ok'; then
    echo "Redis RDB status is not OK"
    exit 1
fi

# 检查 RDB 文件
test -s "$RDB_FILE"

# 复制已经完成的 RDB 文件
cp "$RDB_FILE" "$BACKUP_FILE"

test -s "$BACKUP_FILE"

echo "Redis backup completed: ${DATE}"

这个脚本是单机 Redis 的基础示例,实际生产环境还需要根据 Redis 部署方式补充认证、TLS、连接地址和实例选择参数。

例如 Redis 使用 ACL 时,可以通过受保护的凭据文件进行认证,不要把密码直接写进脚本。

另外,脚本中应当增加对 BGSAVE 已经在执行时的处理逻辑,避免重复触发快照。

6.4 Redis 如何恢复?

恢复 RDB 文件时,首先确认 Redis 的版本兼容性和数据目录配置。

一种常见方式是:

  1. 停止目标 Redis 实例。
  2. 备份现有 RDB 文件。
  3. 将恢复的 RDB 文件放入 Redis 配置指定的数据目录。
  4. 设置正确的文件权限。
  5. 启动 Redis。
  6. 检查数据是否正常加载。

例如:

systemctl stop redis

cp dump_20261103_020000.rdb /var/lib/redis/dump.rdb

chown redis:redis /var/lib/redis/dump.rdb

systemctl start redis

恢复前必须确认 Redis 实际使用的 dir 和 dbfilename,不能直接假定数据目录一定是 /var/lib/redis。

6.5 Redis 生产环境注意事项

RDB 是某个时间点的数据快照,并不代表所有最新写入都已经保存。

如果业务对数据丢失比较敏感,建议根据业务要求启用 AOF,并合理配置 appendfsync。

Redis 7 及之后版本的 AOF 采用多部分文件机制,包含基础文件、增量文件和 Manifest,不能简单地只复制一个 AOF 文件就认为完成了完整备份。

另外,如果使用 Redis Cluster,需要针对每个数据分片分别设计备份和恢复流程,不能只备份其中一个节点的 RDB 文件。

七、使用 Restic 备份数据库文件

完成数据库原生备份后,就可以将本地备份目录交给 Restic。

这里采用一个比较实用的设计:

  • 数据库备份脚本负责生成本地备份文件。
  • Restic 负责将文件上传到远程仓库。
  • R2 和 OSS 分别使用独立仓库。
  • 备份完成后清理本地临时文件。

7.1 编写 Restic 备份脚本

创建:

vim /usr/local/bin/backup-restic.sh

示例:

#!/usr/bin/env bash

set -Eeuo pipefail

BACKUP_DIR="/backup/database"

# 根据参数选择远程仓库
TARGET="${1:-r2}"

case "$TARGET" in
    r2)
        ENV_FILE="/etc/restic/r2.env"
        RESTIC_OPTIONS=()
        ;;

    oss)
        ENV_FILE="/etc/restic/oss.env"
        RESTIC_OPTIONS=(
            -o s3.bucket-lookup=dns
            -o s3.region=cn-hangzhou
        )
        ;;

    *)
        echo "Usage: $0 {r2|oss}"
        exit 1
        ;;
esac

# 加载 Restic 配置
set -a
source "$ENV_FILE"
set +a

# 执行备份
restic "${RESTIC_OPTIONS[@]}" backup \
    "$BACKUP_DIR" \
    --tag database \
    --host "$(hostname)"

echo "Restic backup to ${TARGET} completed"

设置执行权限:

chmod 700 /usr/local/bin/backup-restic.sh

这里的 OSS 参数需要根据实际 OSS Endpoint 和地域调整。如果使用其他地域,不要继续使用杭州的配置。

7.2 分别备份到 R2 和 OSS

备份到 Cloudflare R2:

/usr/local/bin/backup-restic.sh r2

备份到阿里云 OSS:

/usr/local/bin/backup-restic.sh oss

如果希望两个存储都保存一份:

/usr/local/bin/backup-restic.sh r2

/usr/local/bin/backup-restic.sh oss

两个命令分别写入不同的 Restic 仓库。

需要注意,双存储备份会增加存储容量和网络传输成本,但能够降低单个对象存储服务出现故障时的风险。

7.3 备份完成后清理本地文件

如果服务器磁盘空间有限,可以在远程备份全部成功后清理本地文件。

例如:

/usr/local/bin/backup-restic.sh r2

/usr/local/bin/backup-restic.sh oss

rm -rf /backup/database/*

但这段命令不能直接用于所有生产环境。

更安全的做法是:

  1. 先确认数据库备份文件生成成功。
  2. 确认 R2 备份成功。
  3. 确认 OSS 备份成功。
  4. 根据需要检查两个仓库的快照。
  5. 只有所有目标存储都成功后,才清理本地文件。

如果 R2 成功而 OSS 失败,不应立即删除本地备份文件。

对于生产环境,还建议保留最近一份本地备份,或者使用独立的本地备份保留策略。

八、使用 systemd Timer 定时执行

接下来使用 systemd Timer 管理备份任务。

这里建议将数据库备份和 Restic 上传封装在一个统一脚本中,由 systemd Service 负责执行。

8.1 编写统一备份脚本

创建:

vim /usr/local/bin/backup-all.sh

示例:

#!/usr/bin/env bash

set -Eeuo pipefail

echo "========== Backup Started =========="

# PostgreSQL
/usr/local/bin/backup-postgresql.sh

# MySQL
/usr/local/bin/backup-mysql.sh

# Redis
/usr/local/bin/backup-redis.sh

# 上传到 R2
/usr/local/bin/backup-restic.sh r2

# 上传到 OSS
/usr/local/bin/backup-restic.sh oss

echo "========== Backup Completed =========="

设置权限:

chmod 700 /usr/local/bin/backup-all.sh

这个示例默认三种数据库都部署在同一台服务器上。

实际部署时,应当只启用当前服务器实际运行的数据库备份任务。如果数据库分布在不同机器上,可以分别部署备份脚本和 Timer。

8.2 创建 systemd Service

创建:

vim /etc/systemd/system/database-backup.service

内容:

[Unit]
Description=Database Backup Service
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup-all.sh

# 限制任务最长执行时间
TimeoutStartSec=6h

# 进程异常时清理服务进程
KillMode=control-group

这里使用 Type=oneshot,表示 Service 执行一次任务后退出。

TimeoutStartSec=6h 是示例值,应根据数据库规模、网络速度和备份窗口调整。

如果任务超时,systemd 会终止对应服务进程,因此不要将超时时间设置得过短。

8.3 创建 systemd Timer

创建:

vim /etc/systemd/system/database-backup.timer

内容:

[Unit]
Description=Daily Database Backup Timer

[Timer]
OnCalendar=*-*-* 02:00:00

# 错过定时触发后补执行一次
Persistent=true

# 在 5 分钟范围内随机延迟
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

参数说明:

参数作用
OnCalendar指定日历定时规则
Persistent补执行错过的日历定时触发
RandomizedDelaySec随机延迟执行,避免同时触发

这里的 Persistent=true 不是任务队列,不会将关机期间错过的每一天都逐个补齐,而是在重新激活 Timer 时补触发一次。

8.4 启用定时任务

重新加载 systemd:

systemctl daemon-reload

启用 Timer:

systemctl enable --now database-backup.timer

查看定时任务:

systemctl list-timers database-backup.timer

手动执行一次备份:

systemctl start database-backup.service

查看执行状态:

systemctl status database-backup.service

查看日志:

journalctl -u database-backup.service

查看最近 100 行日志:

journalctl -u database-backup.service -n 100

如果需要查看本次启动以来的日志:

journalctl -u database-backup.service -b

九、Restic 快照管理与自动清理

Restic 每次备份都会生成一个 Snapshot。

随着备份周期增加,历史快照会越来越多,因此需要设计合理的保留策略。

9.1 查看备份快照

查看 R2 仓库:

source /etc/restic/r2.env

restic snapshots

查看 OSS 仓库:

source /etc/restic/oss.env

restic \
  -o s3.bucket-lookup=dns \
  -o s3.region=cn-hangzhou \
  snapshots

也可以按照标签过滤:

restic snapshots --tag database

9.2 配置快照保留策略

例如:

restic forget \
  --tag database \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6

表示保留:

  • 最近 7 个每日快照。
  • 最近 4 个每周快照。
  • 最近 6 个每月快照。

这里的保留策略是按 Restic 快照分组和时间规则计算的,并不意味着一定精确保留最近 7×24 小时内的每一个快照。

9.3 清理不再使用的数据块

执行:

restic forget \
  --tag database \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --prune

其中:

  • forget:删除不再保留的快照引用。
  • prune:清理不再被任何快照引用的数据块。

生产环境建议将 forget 和 prune 分开安排。

例如,每天执行备份,每周日凌晨执行一次清理:

restic forget \
  --tag database \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --dry-run

先通过 --dry-run 检查清理范围,确认没有问题后再去掉该参数执行。

prune 可能涉及对象读取、数据包重写和大量网络 I/O,应安排在低峰期,并确保维护期间没有其他任务同时操作同一个仓库。

另外,R2 和 OSS 是两个独立仓库,必须分别执行清理操作。

十、生产环境必须考虑的可靠性问题

前面的配置已经能够组成一套自动化备份流程,但真正用于生产环境,还需要补充一些保障措施。

10.1 数据库备份一致性

这是最重要的一点。

数据库推荐备份方式需要注意
PostgreSQLpg_dump / pg_basebackup全局对象、WAL、PITR
MySQLmysqldump / XtraBackupInnoDB 一致性、Binlog
RedisBGSAVE + RDBRDB 快照时间点、AOF

不要直接使用 Restic 备份正在运行的数据库数据目录。

Restic 只能保证它读取到的文件被保存下来,不能替代数据库自身的事务一致性和恢复机制。

10.2 备份任务幂等与并发控制

systemd Timer 不应被当成分布式任务调度系统。

虽然同一个 Service 通常不会同时启动多个实例,但仍然建议在脚本中增加锁机制,避免人工执行、其他调度器或重复触发造成并发备份。

例如:

flock -n /run/lock/database-backup.lock \
  /usr/local/bin/backup-all.sh

如果需要在多台服务器之间协调备份任务,应使用分布式锁或具备协调能力的调度系统。

另外,Restic 仓库自身也有锁机制,不建议使用 --no-lock 绕过仓库锁。

10.3 备份失败告警

备份脚本退出码为非零时,systemd 会将 Service 标记为失败。

可以通过:

systemctl is-failed database-backup.service

检查最近一次执行状态。

但仅依赖 systemd 状态还不够,生产环境建议增加:

  • 备份失败通知。
  • 备份成功时间监控。
  • 最近一次成功快照时间监控。
  • 备份文件大小异常监控。
  • OSS / R2 连接异常监控。
  • 磁盘空间不足告警。

尤其要关注最后一次成功备份的时间。

例如,备份任务每天凌晨执行,如果连续两天没有生成新的快照,就应该触发告警,而不是等到真正需要恢复时才发现问题。

10.4 检查 Restic 仓库

Restic 提供了仓库检查命令:

restic check

它可以检查仓库结构和数据索引等信息。

如果需要读取并验证仓库中的数据:

restic check --read-data

--read-data 会读取仓库中的数据,耗时和对象存储请求成本可能较高。

建议根据数据规模安排周期性完整检查,而不是每次备份都执行。

10.5 恢复演练

备份体系最重要的验收标准不是备份任务是否成功,而是能否在故障时恢复数据。

建议定期进行恢复演练:

  1. 从对象存储下载指定 Snapshot。
  2. 恢复到独立测试目录或测试服务器。
  3. 启动 PostgreSQL、MySQL 或 Redis。
  4. 检查数据库能否正常启动。
  5. 验证关键表、关键数据和业务查询。
  6. 记录实际恢复耗时。

同时,应当记录 RPO 和 RTO:

  • RPO(Recovery Point Objective):可以接受丢失多长时间的数据。
  • RTO(Recovery Time Objective):可以接受多长时间内完成恢复。

例如,每天凌晨备份一次,并不意味着 RPO 一定是 24 小时。实际 RPO 还取决于备份开始时间、备份完成时间以及数据库恢复机制。

如果业务要求更低的 RPO,就需要考虑更高频率的备份、Binlog、WAL 归档等方案。

10.6 对象存储安全

备份数据存放在对象存储中,也要考虑误删除、账号泄露和勒索软件攻击。

建议:

  • 使用独立的 R2 API Token 和 OSS RAM 用户。
  • 限制凭据只能访问备份专用 Bucket。
  • 不让业务服务器持有对象存储管理权限。
  • 为备份仓库密码建立独立的安全保管机制。
  • 根据服务能力配置版本控制、保留策略或防删除保护。
  • 对关键备份保留独立账号或独立账户下的第二份副本。

需要特别注意:Restic 的 prune 会删除不再需要的数据对象。如果对象存储启用了版本控制或防删除策略,可能会产生额外的存储费用,也可能影响实际清理效果。

因此,不能简单认为开启对象版本控制就等于完成了不可变备份设计。

十一、最终方案与选型建议

经过上面的配置,整个备份体系可以归纳为:

              systemd Timer
                    |
                    v
              backup-all.sh
                    |
        +-----------+-----------+
        |           |           |
        v           v           v
    PostgreSQL    MySQL       Redis
     pg_dump     mysqldump    BGSAVE
        |           |           |
        +-----------+-----------+
                    |
                    v
              本地备份目录
                    |
                    v
                  Restic
                    |
             +------+------+
             |             |
             v             v
       Cloudflare R2    阿里云 OSS
         仓库 A           仓库 B

不同规模和业务要求,可以选择不同的备份策略:

场景推荐方案
小型 PostgreSQLpg_dump + Restic
大型 PostgreSQLpgBackRest / Barman + WAL 归档
小型 MySQLmysqldump + Restic
大型 MySQLXtraBackup + Binlog
Redis 缓存数据根据数据重要性配置 RDB / AOF
Redis 持久化业务数据RDB + AOF + 定期异地备份
多存储备份Restic 分别写入 R2 和 OSS

对于普通业务服务器,我比较推荐从以下方案开始:

  1. PostgreSQL 使用 pg_dump -Fc。
  2. MySQL 使用 mysqldump --single-transaction,前提是业务表主要使用 InnoDB。
  3. Redis 使用 BGSAVE 生成 RDB。
  4. systemd Timer 每天凌晨触发备份。
  5. Restic 分别将备份文件上传到 Cloudflare R2 和阿里云 OSS。
  6. 每周执行一次快照清理。
  7. 定期执行仓库检查和数据库恢复演练。

这套方案的优势是组件少、职责清晰、维护成本较低,并且后续可以独立替换数据库备份工具、调度器或对象存储。

十二、总结

Restic、systemd Timer 和对象存储组合起来,是一套适合 Linux 服务器的自动化备份方案。

它们分别解决不同的问题:

  • systemd Timer 负责定时调度。
  • PostgreSQL、MySQL、Redis 原生工具负责生成符合数据库要求的备份。
  • Restic 负责加密、去重、快照管理和远程存储。
  • Cloudflare R2、阿里云 OSS 负责保存异地备份数据。

但需要记住:备份成功不等于可以恢复,远程存储也不等于具备完整的灾难恢复能力。

真正可靠的备份体系,需要同时考虑数据一致性、凭据安全、异地存储、历史版本、失败告警和恢复演练。

如果你的服务器目前还没有自动化备份机制,可以先从 PostgreSQL / MySQL 的逻辑备份和 Redis RDB 快照开始,再逐步增加双对象存储、物理备份、WAL / Binlog 归档和自动恢复验证。

把备份流程真正纳入日常运维,而不是等到数据丢失时才想起备份,才是这套方案的核心价值。

参考资料