前言

最近借鉴 Arch Linux 的 infrastructure 仓库中的 Ansible 代码,部署了本地基础设施用的备份系统,本文是对这个过程中的记录和思考。会包含实施前的考量,具体如何实施,以及备份系统本身如何监控。

备份系统

采用什么方案

采用了 borg 作为备份的解决方案。borg 是一个支持去重、压缩、加密的备份程序。

关于加密的意义:

备份系统本质上是将本地的数据拷贝一份存储到其他位置。备份加密指的是将备份的数据,在发送到其他位置前做一次加密,因为备份一般被存储在不可信的第三方,比如 Aliyun、AWS 的对象存储,所以需要加密。

关于去重和压缩的意义:

第三方存储一般是按照使用空间的大小收费的,占用更少的空间意味着更低的成本。

borg 创建备份的时候,会将大文件分为小的分块(chunk),chunk 的大小根据内容动态调整,每个 chunk 有一个哈希值用于去重,哈希值相同的块只存储一份,最后压缩得到的数据大小才是最终的备份大小。

如图所示,Original size 即原始大小 226.77G 的数据,Compressed size 压缩后的大小为 138.41G,Deduplicated size 去重后的大小为 56.10 GB,实际上每月只需支付 56.10 GB 数据的存储费用。

采用什么备份规则

备份是针对整个系统的,第一次是整个系统的全量备份,后续的备份是 borg chunking 机制提供的增量备份。

具体的备份规则见 Arch Linux infrastructure 仓库的 borg-backup.sh.j2 文件

大概的内容如下:

  • 判断需要备份的服务器上是否有数据库类的服务,如果有,基于官方的 dump 工具创建每个数据库的 dump 文件(确保数据库数据的一致性、版本兼容性)
  • 判断 filesystem 是否是 btrfs
    • 如果:服务器根目录是基于 btrfs 文件系统。由于 btrfs 制作快照的时候,子卷的快照是不会包含所有内嵌子卷的内容的,因此:
      • 遍历 / 及其下面所有的子卷
      • 在所有子卷内制作快照。比如:/ 的快照就是 /backup-snapshot,子卷 /home/jinmiaoluo 的快照就是 /home/jinmiaoluo/backup-snapshot
      • 把所有子卷的快照通过 mount -o bind 映射到 /backup 内,比如:
        • 子卷 / 的快照 /backup-snapshot 对应的路径就是 /backup
        • 子卷 /home/jinmiaoluo 的快照 /home/jinmiaoluo/backup-snapshot 对应的路径就是 /backup/home/jinmiaoluo
      • /backup 创建备份,即实现了对整个系统进行备份的目的
    • 否则:服务器根目录不是基于 btrfs 文件系统。直接对 / 创建备份
  • 基于 systemd timer 每天发起 1 次备份操作

需要指出的是:快照可以快速的实现本地的状态留存,就像拍了一张相片一样。但在创建远端的系统备份的时候,则是对整个系统的一次比对和拷贝。好在通过 borg chunking 机制,实际上从第二次开始,会被压缩、加密、发送并存储到远端的内容只是修改的内容,因此,实际的内网带宽压力并不大。比对、压缩的操作是在发起备份的服务器上进行的,这也在一定程度上把计算的负载很好的分摊了下去,即使是对一定规模的服务器集群进行备份,也不会对 borg 的服务端产生计算压力,borg 服务端只要准备好足够的存储空间即可。

采用什么清理策略

备份清理策略采用的是 GFS (Grandfather-father-son),对应每日、每周、每月的间隔应保留的备份的份数。比如:

--keep-daily=7 --keep-weekly=4 --keep-monthly=6

假设我们执行备份的频率是每天一次,并且今天是 2023 年 6 月 27 日,那么按照 --keep-daily=7 --keep-weekly=4 --keep-monthly=6 的策略,我们实际保留的备份数量如下:

  • --keep-daily=7:保留最近 7 天的每日备份,即有 7 份备份。

  • --keep-weekly=4:保留最近 4 周的每周备份。这里的关键在于,这 4 周的备份并不包括已经由 --keep-daily=7 保留的备份。因此,如果我们在每周日进行一次周备份,我们将保留的是 4 周前的 4 次周日的备份,即有 4 份备份。

  • --keep-monthly=6:保留最近 6 个月的每月备份。类似地,这 6 个月的备份并不包括已经由 --keep-daily=7--keep-weekly=4 保留的备份。因此,如果我们在每月的第一天进行一次月备份,我们将保留的是 6 个月前的 6 次月份的备份,即有 6 份备份。

所以,总共我们将保留 7(日备份)+ 4(周备份)+ 6(月备份)= 17 份备份。最老的备份可能是 2022 年 11 月 30 日的备份,大概是半年前。

备份策略可以确保成本合理可控、备份数据有效。越老的备份,恢复数据时丢失的改动越多,因此备份越老,数量越少。

如何管理权限

borg 权限管理基于 SSH 的认证,因此 borg 创建、读取备份的权限控制,实际上就是对 SSH authorized_keys 文件内容的管理。

有两类公钥会被添加到 borg 备份的远端 Authorized_keys 文件内:

  1. 每台需要备份的服务器的公钥
  2. 所有运维开发者团队成员的公钥

关于服务器的访问:

首先,任何一台服务器在初始化的时候,会给 root 用户创建一份 SSH 的公私钥,通过添加服务器 root 用户下的公钥到 borg 远端的 Authorized_keys 内,格式如下:

command="borg serve --restrict-to-path /srv/borg/m7.jinmiaoluo.com",restrict ssh-rsa AAAA...CQ== root@m7.jinmiaoluo.com

配合 SSH options 限定每台服务器只能读取自己的备份文件夹 borg serve --restrict-to-path /srv/borg/m7.jinmiaoluo.com,其中 m7.jinmiaoluo.com 是备份的主机名字。不同服务器的数据被保存在 /srv/borg/ 路径下,主机名相关的文件夹内。

Arch Linux Team 封装了 borg 二进制文件,为每台需要备份的服务器预设了远端的地址信息。见 borg.j2,borg.j2 会被渲染成 /usr/local/bin/borg 文件并赋予执行权限

在特定服务器上的 borg 操作,默认就只会对应该服务器的远端仓库。

关于团队成员的访问:

类似服务器,只是在 --restrict-to-path /srv/borg/m7.jinmiaoluo.com 部分放松了路径的限制为 --restrict-to-path /srv/borg/

command="borg serve --restrict-to-path /srv/borg/",restrict ssh-rsa AAAA...YQ== jinmiaoluo@gmail.com

团队成员主要是在灾难发生、故障复查等场景下,需要读取特定文件/文件夹旧状态的时候,会访问备份系统。

团队成员访问备份系统步骤:

  1. 每台服务器的备份数据在发送到远端时,会被本地的 borg 密钥加密。因此需通过 Ansible Task 抓取所有需要备份的服务器的 borg 密钥,存储到本地以便团队成员使用,见 fetch-borg-keys.yml

  2. 有了密钥,并确保自己的 SSH 公钥已经写入到远端的 Authorized_keys 后,传入需要访问的仓库地址,见: borg.sh 即可访问,效果如下:

  3. 如果要基于备份恢复数据,通过 borg 的 mount 功能,将备份以类似磁盘的方式挂载到本地 mnt 目录,然后就可以像访问本地文件/文件夹的方式访问该备份:

假设我要从备份中恢复 jira 服务器。

首先执行下面的命令获取所有服务器备份文件的解密密钥:

ansible-playbook playbooks/tasks/fetch-borg-keys.yml

上面的命令会将所有备份的解密密钥从各自服务器拷贝到本地代码仓库根目录的 borg-keys 目录下:

有了解密密钥,我们就可以在本地,通过 misc/borg.sh 列出 jira 服务器所有可用的备份:

misc/borg.sh list ssh://borg@borg.jinmiaoluo.com:22/srv/borg/jira.jinmiaoluo.com

执行完上面的命令我们就能看到 jira 服务器所有可用的备份:

上图中的每一行对应 jira 服务器在该日期的一次完整的备份。假设我们要恢复 2023 年 9 月 2 号的备份中的数据(即截图中的:20230902-201349

通过下面的命令,我们可以像磁盘一样将该备份挂载到本地的 mnt 目录内:

mkdir -p mnt
misc/borg.sh mount -o ignore_permissions ssh://borg@borg.jinmiaoluo.com:22/srv/borg/jira.jinmiaoluo.com::20230902-201349 mnt

挂载完毕后,就可以访问备份中的数据了。

mnt/backup 目录相当于 jira 服务器在 2023 年 9 月 2 号时的根目录。

接下来可以通过 rsync 或者其他方式从备份中恢复整个系统或者特定的文件。结束后执行下面的命令卸载即可:

umount mnt

如何监控备份系统

备份系统包含以下监控指标:

  • 备份系统总容量
  • 备份系统可用空间
  • 每台服务器的已用空间

备份系统包含以下告警指标:

  • 每台服务器在备份失败时告警

总体指标的处理:

因为我本地的 borg 服务端是一台虚拟机,备份的数据实际上存储到了虚拟机的一块虚拟磁盘上,因此,我只需通过 node_exporter 提供的该虚拟磁盘的数据,即可得出总容量(该虚拟磁盘的总容量)和可用空间(该虚拟磁盘的可用空间):

每台服务器的已用空间:

每台服务器的已用空间实际上是通过 /usr/local/bin/borg info 实现的,见:borg-textcollector.sh

该脚本通过 node_exporter 的 textfile collector 让 Prometheus 采集到最后一次备份的时间戳和仓库的大小

有了监控指标,就可以得到每台服务器备份占用的空间了:

备份相关数据生成的时机:

备份是通过 borg-backup.timer 每天触发 borg-backup.sh.j2 实现的。

prometheus-borg-textcollector.service(里面是对 borg-textcollector.sh 的调用) 定义了:

[Install]
WantedBy=borg-backup.service

这个弱依赖,因此,本机备份所用的空间大小、最后一次备份的时间戳这两个监控指标会在每天备份完成的时候被更新。

备份失败告警:

有了最后一次备份的时间戳,通过简单的计算:

- name: borg
  interval: 60s
  rules:
    - alert: BorgMissingBackup
      expr: time() - borg_hetzner_last_archive_timestamp > 86400 * 1.5
      for: 2m
      labels:
        severity: critical
      annotations:
        summary: 'Borg missing backup (instance {{ $labels.instance }})'
        description: 'Borg has not backuped for more than 24 hours. Last backup was made {{ $value | humanizeDuration }} ago'

就可以得出超过 24 小时没有备份的服务器,即:可能备份失败的服务器。

末尾的只言片语

本文是我在阅读 Arch Linux Infrastructure 仓库的实现后,在本地实践备份系统的一次记录。为了避免太过详细导致晦涩难懂,有些细节被刻意省略。另外,因为实践是在我本地虚拟化进行,有些地方跟 Arch Linux 的实践并不一致,比如:Arch Linux 实际上采用了 On-Site/Off-Size 双备份,他们的服务器在 hetzner 上,除了使用 hetzner 的 storagebox 作为备份存储外,还用了另外一个供应商:rsync.net 作为备份存储供应商,从而避免单一供应商可能带来的数据丢失风险。

本文为大家今后实践基于 borg 的备份系统提供一个思路,具体如何操作应亲自去阅读代码:infrastructure,最后感谢每一个为 Arch Linux 社区的持续稳定而做出贡献的人。