随着自己 Self-host 的服务越来越多,服务器上的数据也越来越重要。虽然服务器很少有完全 crash 掉的可能性,但是如果某天真的有什么不可抗力,后果是不堪设想的……所以一个自动化且白嫖的「容灾备份」方案就十分重要。

真正提醒我异地「容灾备份」的重要性的是某天看到的一个 meme……

Server has crashed...

把自己服务器的备份放在自己的服务器上,就是相当于没有备份 =_=

于是我找到了 Storj、Google Drive 两个白嫖的备份去处,记录一下遇到的一些坑。

Restic + Rclone

我选择了 Restic 作为备份的软件。Restic 是一个开源的轻量级备份工具,使用起来非常方便。

为了实现远程的存储,我使用 Rclone 作为 Restic 的后端。Rclone 是一个用于管理云存储文件的命令行程序,支持非常多的云服务。

Restic、Rclone 的基本用法可以参阅官网,这里不赘述。

Storj

Storj 是一家云存储厂商,免费计划提供 150G 的云存储可以白嫖,拿来存放备份绰绰有余。Rclone 也支持 Storj。

目前 Storj 没有被墙,即使服务器在国内也可以正常使用。

Rclone 挂载

Storj 有个概念叫做 Access Grant,一开始我也有点迷糊。

原始的 API key + passphrase 的认证方式是在被逐步弃用的。所谓 Access Grant 是一个长长的字符串,作用相当于 API key 但是无需其他验证,通过这个可以直接证明对自己指定 Bucket 的访问权限。在 Storj 后台可以生成 Access Grants。

所以在 rclone config 到这一步的时候:

Option provider.
Choose an authentication method.
Choose a number from below, or type in your own string value.
Press Enter for the default (existing).
 1 / Use an existing access grant.
   \ (existing)
 2 / Create a new access grant from satellite address, API key, and passphrase.
   \ (new)

应该选择 1,然后粘贴后台生成的 Access Grant。

Restic with Rclone

初始化 Restic 存储库,记得设置一个随机的猜不到的密码:

restic -r rclone:storj:server-dr/backup init

其中 storjrclone config 时设置的 remote 名称,server-dr 是 Storj 上的 Bucket 名称,/backup 才是自己指定的目录。

这一步完成后,浏览器进入 Storj 的后台文件管理,就可以看见 server-dr 这个 Bucket 中多了一个 backup 文件夹,其中就是初始化的 Restic 备份仓库。

进行备份:

restic backup /data -r rclone:storj:server-dr/backup --verbose

删除过期快照,只保留最近三天的备份:

restic forget --keep-daily 3 --prune -r rclone:storj:server-dr/backup

第一次备份会比较慢,共 3G 左右的内容,我的第一次备份用了三个多小时……之后的备份就都会是增量备份,会很快。

Google Drive

Rclone 也支持 Google Drive,免费空间 15G,也是一种绝佳的白嫖方案。只不过如果是国内的服务器的话要开代理。

类似的选择还有 Onedrive(空间小,免费计划只有 5G)、Dropbox(也要开代理),Rclone 也都支持,可以尝试。

使用和上述 Storj 类似,rclone config 之后;

restic -r rclone:gdrive:/backup init

restic backup /data -r rclone:gdrive:/backup --verbose --password-file /root/backup/password_gdrive > /root/backup/backup_gdrive.log
restic forget --keep-daily 3 --prune -r rclone:gdrive:/backup

自己的 client-id

在创建 Google Drive 的 remote 时,rclone 推荐你去申请自己的 client id,参照这个文档

有一个小坑点,根据这个文档申请成功之后,回到 Rclone 的登录步骤时,由于我的服务器是 headless 的,我就用本地的 rclone 进行登录操作,这时候终端 rclone 也要开启代理,否则即使登录成功了 get code 操作也是拿不到 code 的(也就是显示 got code 但是却没有输出 code,困惑了我好久)。

另一台服务器(SFTP)

如果要备份到另一台服务器,无需 Rclone,Restic 自带了 SFTP 协议连接,只要目标主机开启了 SSH 就行。

restic -r sftp:user@host:/srv/restic-repo init

如果自己搭建了 NAS,也可以用 SFTP 备份。

软备份数据库

对于 MySQL 数据库,采用「硬备份」或称为「物理备份」的方式(即直接备份整个 MySQL 数据存放的文件夹)是有一定的风险的,可能导致数据损坏。

为了安全性,我们再增加一个「软备份」,用 mysqldump 导出 MySQL 的数据到一个 .sql 文件里,再备份这个文件。

# 备份指定数据库
mysqldump -u username -p db_name > mydb.sql
# 备份所有数据库
mysqldump -u username -p --all-databases > alldb.sql
# 备份 Docker 容器中的数据库
bash -c "docker exec mariadb mysqldump -u username --password=password --all-databases" > mysql_all.sql

自动化脚本

可以把以上操作写到一个 backup.sh 脚本里,设置以下 Crontab 规则每天凌晨 3:00 定时执行备份:

0 3 * * * /root/backup/backup.sh > /root/backup/backup.log

我写的脚本如下:

#!/bin/bash
# Author: SkyWT

# 已经初始化的 RESTIC 仓库
RESTIC_REPO=(
  "rclone:storj:server-dr/backup"
  "rclone:gdrive:/backup"
#  "sftp://root@localhost:7051//data/tc_backup"
)
# 各个仓库是否需要代理
RESTIC_PROXY_NEEDED=(
  "false"
  "true"
)
# 各个仓库的密码
RESTIC_PASS=(
  "YourPassWord"
  "YourPassWord"
)

# 设置网络代理的地址
BACKUP_PROXY="http://127.0.0.1:10800"

# 备份 MySQL 的相关配置
BACKUP_MYSQL=true
MYSQL_CONTAINER_NAME=mariadb
MYSQL_ROOT_NAME=root
MYSQL_ROOT_PASS=YourPassWord

# 备份文件的相关配置
BACKUP_FILES=true
# 需要备份的文件/文件夹
FILES_TARGET=(
  "/data"
)

# 清理旧快照的相关配置
BACKUP_FORGET=true
# 快照的保留策略,可参阅 Restic 文档
FORGET_POLICY_ARG="--keep-daily 3"

# ----- END OF CONFIG -----

set_proxy(){
    export http_proxy=$BACKUP_PROXY
    export https_proxy=$BACKUP_PROXY
}

unset_proxy(){
    unset http_proxy
    unset https_proxy
}

backup_mysql(){
    echo -e "\e[32m --- Backup MySQL Start. --- \e[0m"
    repo=$1
    bash -c "docker exec $MYSQL_CONTAINER_NAME mysqldump -u $MYSQL_ROOT_NAME --password=$MYSQL_ROOT_PASS --all-databases" > mysql_all.sql
    restic -r $repo --verbose backup mysql_all.sql --password-file /tmp/.tmp_restic_pass
    echo -e "\e[32m --- Backup MySQL OK. --- \e[0m"
}

backup_files(){
    echo -e "\e[32m --- Backup files Start. --- \e[0m"
    repo=$1
    for target in ${FILES_TARGET[@]}
    do
        restic -r $repo --verbose backup $target --password-file /tmp/.tmp_restic_pass
    done
    echo -e "\e[32m --- Backup files OK. --- \e[0m"
}

backup_forget(){
    echo -e "\e[32m --- Forget Start. --- \e[0m"
    repo=$1
    bash -c "restic -r $repo forget $FORGET_POLICY_ARG --password-file /tmp/.tmp_restic_pass --prune"
    echo -e "\e[32m --- Forget OK. --- \e[0m"
}

start_time=$(date +%s)
for ((i=0; i<${#RESTIC_REPO[@]}; i++));
do
    echo -e "\e[32m --- Starting a repo. --- \e[0m"
    repo=${RESTIC_REPO[$i]}
    echo ${RESTIC_PASS[$i]} > /tmp/.tmp_restic_pass
    if ${RESTIC_PROXY_NEEDED[$i]}; then set_proxy; fi

    if $BACKUP_MYSQL; then backup_mysql $repo; fi
    if $BACKUP_FILES; then backup_files $repo; fi
    if $BACKUP_FORGET; then backup_forget $repo; fi

    if ${RESTIC_PROXY_NEEDED[$i]}; then unset_proxy; fi
    rm -rf /tmp/.tmp_restic_pass
    echo -e "\e[32m --- Ending a repo. --- \e[0m"
done
end_time=$(date +%s)
cost_time=$[ $end_time-$start_time ]

echo -e "\e[32m Done in $cost_time s.  \e[0m"

结语

对于 Self-hosted 玩家来说,没有什么比数据更宝贵了。虽然折腾起来有点麻烦,但是如果没有合适的备份方案,等服务器 crash 的时候真的会追悔莫及。

虽然我没有经历过,但是我说实话没办法保证服务器在下一秒会不会因为什么不可抗力而 crash,导致数据丢失。有太多东西是我们无法控制的。我们只能做好我们能控制的一切。