备份与恢复
Rediacc 会备份加密仓库,并在同一台机器或另一台机器上恢复它们。备份之所以是加密的,是因为仓库本身就是加密的:离开机器的只有密文,恢复时需要仓库的 LUKS 凭据。
备份有两种方式,它们回答的是不同的问题。
- 快照到分块存储(
rdc backup snapshot)保留一段可以回溯的历史,这是主要路径。 - 在另一台机器上留一份副本(
rdc repo push、rdc repo pull),在你自己掌控的硬件上保留仓库当下的原样。这个过程不涉及任何云账户。
这两种方式相互独立。用一种方式备份过的仓库,并不算用另一种方式也备份过。
快照的工作原理
仓库镜像会在一个固定的网格上被切分成固定大小的单元格。每个单元格要么是一个”洞”(意味着那里从未写入过任何内容),要么保存在一个键下,而这个键就是该单元格密文的 SHA-256。
正是这一个决定,带来了下面所有的特性。
只有真正的变更才会产生成本。 第一次快照会上传所有已写入的单元格。之后每次运行都会向文件系统查询哪些区段(extent)被改动过,只读取并哈希这些区段,再只上传存储中尚不存在的单元格。一个数据几乎没怎么变化的仓库几乎不会上传任何内容,运行时间只需几分钟,而不是随镜像大小线性增长。
相同的数据只保存一次。 由于键就是内容的哈希,共享同一个单元格的两个快照会共享同一个对象;仓库和它的分支(fork)也是如此:一个分支家族是针对同一个血统进行备份的,而不是重复备份其父级。
恢复一个旧快照,并不比恢复一个新快照更慢。 这里没有一条必须按顺序回放的增量链。恢复操作会把快照解析成一份完整的单元格列表,然后直接获取这些单元格,因此恢复所需的时间取决于镜像大小和你的带宽,而不是你做备份已经做了多久。洞依然是洞,因此稀疏镜像恢复后依然稀疏;在镜像中多处出现的同一个单元格也只会被下载一次。
每个快照都能独立成立。 不存在那种”绝对不能丢”的”完整备份”,也不存在某个损坏的增量会让之后所有增量都失效的窗口期。列表中的任何一个快照都可以直接恢复。
校验靠的是重新哈希,而不是信任。 既然键就是内容的哈希,校验一份备份就意味着取回单元格并重新哈希它们。rdc backup verify 做抽样校验;rdc backup verify --deep 会对每一个记录在案的单元格重新哈希。
中断的运行不会白费。 上传会在恢复时跳过已经送达的单元格,不会重复发送;重新开始一次中断了一半的恢复,会对磁盘上已有的内容重新哈希并复用,而不是重新下载。
这会花费你什么
配额是按物理上唯一存储的字节数计算的:也就是去重之后实际占用的量,而不是所有快照在逻辑上代表的总量之和。一个变化缓慢的仓库,做三十次快照的成本接近于做一次。
rdc backup usage 会显示已存储字节数相对于配额的占比;配额是按订阅计的数值,Community 套餐从 10 GB 起步。
快照需要什么
快照上传要经过账户服务器,后者会根据仓库已安装的许可证为每次运行授权,并向机器下发一个短期有效的写入许可。因此这条路径需要机器能够访问到账户服务器,并且仓库要有有效许可证。少了这些,快照不会被悄悄跳过,而是会被拒绝,rdc backup manifests、rdc backup usage 和 rdc backup retention 也就没有可读取的内容。
这同样适用于 --dry-run。因为在判断本次运行究竟是在做计划还是在上传之前,系统就会先读取许可证,所以试运行是对实际工作的预览,而不是一种在没有凭据的情况下试用命令的方式。
机器到机器的 push 和 pull 两者都不需要。它们是两台已经在你配置中的机器之间的直接传输。
快照不做出的承诺
- 一个快照只覆盖一个仓库,不会一次性覆盖整台机器。 每个仓库都是在各自的那一瞬间被捕获的。如果两个仓库彼此依赖,它们的快照并不是一对协调好的整体。
- 这不是持续复制。 快照是你捕获的某一个时间点,自上次快照以来写入的所有内容都可能丢失,具体丢多少取决于你运行的频率。
- 存储的对象只写一次,不是经过认证的 WORM。 单元格是以”仅创建”的条件写入的,机器获得的许可无法删除任何东西,删除操作是按保留策略在服务器端执行的。这对”被攻陷的机器摧毁自己的备份”是一道真实的屏障,但它不是合规认证,也不会按合规标准接受审计。
rclone 存储路径已经不复存在
rdc repo push --to <storage> 及其相关命令过去会把整个备份文件复制到你自己注册的云提供商。现在它们会拒绝存储目标,并指出替代方案的名字。机器到机器的传输从来不经过 rclone,不受影响。如果你仍需要读取以那种方式写入的存档,请参阅读取废止之前写入的存档。
分块存储命令
# 上传一个快照。首次运行播种,之后只发送变化的单元格。
rdc backup snapshot my-app
# 只计划不上传:报告会移动哪些内容。
rdc backup snapshot my-app --dry-run
# 停止容器,冻结,重启,然后上传。
rdc backup snapshot my-app --cold
# 不信任本地锚点,重新上传完整清单。
# 这会重新上传所有内容并重新计入配额;仅在已确认
# 锚点损坏时使用。
rdc backup snapshot my-app --reseed
# 检查已存储的清单和你的配额。
rdc backup verify my-app
rdc backup manifests my-app
rdc backup usage
| 选项 | 描述 |
|---|---|
<repo-ref>(位置参数) | 要做快照的仓库 |
--dry-run | 仅计划:不上传。报告会移动哪些内容 |
--cold | 停止容器,冻结,重启,然后上传。不能与 --dry-run 同时使用 |
--reseed | 不信任本地锚点,上传完整清单。重新上传所有内容并重新计入配额 |
--debug | 启用详细输出 |
冷快照(--cold)
冷快照会在冻结之前先停止仓库,因此保存下来的镜像是应用一致的,而不是崩溃一致的。这个命令直接在机器本身上运行:
# 默认数据存储上的所有仓库。
sudo renet backup snapshot --cold
# 只处理你指定的仓库。--repo 接受一个仓库 GUID,可重复指定。
sudo renet backup snapshot --cold --repo <guid> --repo <guid>
--cold 不能与 --dry-run 同时使用。一个会停止容器的试运行就不算是”试”运行了,而一个不停止容器的运行也算不上”冷”,因此 renet 会直接拒绝这种组合,而不是替你决定它的含义。
一次冷运行会做什么
对每个被选中的仓库,按以下顺序执行:
- 停止其容器。
- 将仓库挂载点和数据存储刷写到磁盘。
- 确认容器确实已经停止。
- 对仓库镜像做一次写时复制(copy-on-write)reflink。
- 重新启动容器。
只有在此之后上传才会开始,此时所有仓库都已经重新启动完毕。
停机时间指的是冻结,而不是传输。reflink 只是元数据,因此无论仓库是 1 GB 还是 100 GB,所需时间都一样。上传就不是这样了:它会随着变化的字节数增长,而首次快照要上传整个非空清单。如果一直让容器保持停止状态直到上传完成,停机时间就会被拴在数据量上,而在首次播种时,这意味着要花上数小时,而不是几毫秒。
被选中的所有仓库是在一个统一的时间窗口内一起停止的,而不是逐个停止。这会让每个仓库的停机时间略微延长一些,但换来的是整个集合有一个统一的一致性时间点。
一个没有任何容器在运行的仓库本来就已经是安静的。它会在没有任何停机的情况下被拍下快照,这是一个正常的结果,而不是失败。
停机时间的成本
在一台真实机器上测得的总停机时间是 222 毫秒:
| 阶段 | 实测值 | 发生了什么 |
|---|---|---|
cold_down | 64 ms | 容器停止 |
cold_sync | 26 ms | 仓库挂载点和数据存储被刷写到磁盘 |
cold_verify | 31 ms | 确认容器已经停止 |
cold_stage | 0 ms | 仓库镜像的 reflink |
cold_up | 99 ms | 容器重新启动 |
占大头的是容器重启,而暂存(staging)几乎不花时间:reflink 在毫秒级分辨率下根本测不出来。不过,读这个”零”的时候不要孤立地看,要结合每个仓库各自的记录一起读。一次拒绝了所有仓库的运行同样会报告 cold_stage=0ms,只有这些记录才能告诉你到底是哪种情况。
这份细分数据是证据,不是装饰。这五个阶段没有一个会读取或发送仓库数据,因此备份越大,它们也不会随之增长。唯一会增长的部分是上传,而它是在停机结束之后才运行的。
renet 会在运行结束时打印同样的数字,这样你就能在自己的机器上实测,而不必只相信我们给出的数字:
Cold backup: <n> repositories quiesced, outage 222ms (cold_down=64ms cold_sync=26ms cold_verify=31ms cold_stage=0ms cold_up=99ms)
每个仓库的 JSON 记录也保存着同样的停机时间和各阶段数据,因此之后查阅的人无需靠猜测时间,就能分辨出一次快照是冷快照还是热快照。
什么时候选择冷快照
默认是热快照,对大多数仓库来说这也是正确的选择。热快照是崩溃一致的,相当于一次断电后仓库会处于的状态,而且完全不需要任何停机时间。大多数数据库和队列都能自行从这种状态恢复。
冷快照应该用于那些在写入过程中无法被安全捕获的数据。一个拥有自己的预写日志(write-ahead log)和内存内状态的数据库,就是最典型的例子。你是在用一段短暂的、可测量的停机时间,换取一个应用无需先执行恢复流程就能直接打开的快照。
一次冷运行会拒绝什么
拒绝正是这个功能的意义所在。一个号称”冷”但实际上什么都没有静止下来的备份,是一个要等到恢复时才会被发现的谎言,因此 renet 绝不会悄悄地把一次冷运行降级成热运行:
- 没有停止的容器。 停止操作之后,renet 会向仓库自己的 Docker socket 询问是否还有东西在运行。如果有,该仓库就会被拒绝,而不是被拍下快照。这项检查会朝安全的方向失败:如果无法连接 socket,或者无法读取容器列表,静止状态就会被视为”未确认”,而未确认就会被拒绝。
- 无法读取的许可证。 许可证检查是在停机之前进行的,而不是之后,因为一个许可证读不出来的仓库,原本就不可能上传任何内容。这样的仓库会被跳过,而不会被停止。如果所选的仓库中没有一个拥有可读的许可证,那么在任何一个容器被关闭之前,整次运行就会被拒绝。
- 同一个数据存储上的第二次冷运行。 锁覆盖的是整个数据存储,如果锁正被占用,第二次运行会立即被拒绝,不会停止任何东西。两次重叠的运行会各自停止自己认为归自己所有的容器,而第二次运行还会重启第一次运行仍在冻结中的仓库。跳过这次运行、等下一次再运行,要比那样好得多。
如果容器处于停止状态时运行被中断(比如 systemctl stop 或者重启),renet 会在退出之前把它们重新启动起来。机器一侧的恢复机制是最后一道防线:它会发现一个”主人失踪”的冷备份,并把这些仓库重新拉起来。
将备份推送到另一台机器
通过 SSH 将一个仓库复制到第二台机器:
rdc repo push my-app --to server-1
--to <machine> 会从你的配置中解析目标机器,--to-machine <machine> 则是明确指定同样的内容。存储名会被拒绝:那条路径已经废止。
加密镜像是以相同的 GUID 被复制的,因此这属于备份或迁移,而不是分支(fork)。如果需要一份独立的副本,请先执行 rdc repo fork,再推送这个分支。
第一次推送会传输整个镜像。此后每一次推送都只会针对两台机器上都保留的一个不可变基础镜像,发送发生变化的块,无需设置任何标志。如果你需要自行指定该基础镜像,可以使用 --delta-base <guid>。
被推送的副本会作为一个备份产物落在目标机器上,而不是一个正在运行的仓库。用 rdc backup restore 把它变成一个仓库:
rdc backup restore my-app@server-1 --as my-app --machine server-1 --up
如果需要按特定时间点做备份,请改用分块存储:rdc backup snapshot my-app 只上传发生变化的单元格,而 rdc backup restore my-app --at <snapshot> 可以将其中任意一个恢复回来。
| 选项 | 描述 |
|---|---|
<ref>(位置参数) | 要推送的仓库引用 |
--to <remote> | 目标机器或集群 |
--to-machine <machine> | 明确指定的目标机器 |
--provision <provider> | 如果目标机器不存在,通过此云提供商进行配置 |
--checkpoint | 推送前创建 CRIU 检查点(用于带有 rediacc.checkpoint=true 标签的容器)。目标会在 repo up 时自动恢复 |
--force | 覆盖已有的备份 |
--bwlimit <limit> | rsync 传输的带宽限制(例如 10M、500K) |
--delta-base <guid> | 只传输相对于该不可变基础 GUID 发生变化的块。省略则自动选择基础镜像 |
--strategy <strategy> | 使用增量基础时的块增量策略:auto、physical 或 shared |
--debug | 启用详细输出 |
--skip-router-restart | 操作后跳过路由服务器的重启 |
从另一台机器拉取备份
从保存着仓库的那台机器把它取回来:
rdc repo pull my-app --from server-1
添加 --up 可以在同一条命令中完成挂载和部署。若要改从分块存储恢复,请使用 rdc backup restore my-app --at <snapshot-id>。
Pull 会拒绝覆盖当前处于挂载状态的仓库。请先卸载,再执行 pull,然后用 rdc repo up 重新拉起。基于目录的仓库是例外:它们会在挂载状态下原地同步。
| 选项 | 描述 |
|---|---|
<ref>(位置参数) | 要拉取的仓库引用 |
--from <remote> | 源机器或集群 |
--from-machine <machine> | 明确指定的源机器 |
--force | 覆盖已有的本地备份 |
--up | 拉取后挂载并部署仓库 |
--bwlimit <limit> | rsync 传输的带宽限制(例如 10M、500K) |
--delta-base <guid> | 只接收相对于该不可变基础 GUID 发生变化的块 |
--strategy <strategy> | 使用增量基础时的块增量策略:auto、physical 或 shared |
--debug | 启用详细输出 |
--skip-router-restart | 操作后跳过路由服务器的重启 |
列出备份
列出分块存储中的快照:
rdc backup manifests my-app
每一行都是一个被保存下来的时间点:
| 列 | 含义 |
|---|---|
Repo | 从本地配置解析出的仓库名(不在配置中的仓库会回退显示 GUID) |
Snapshot | 快照 ID。这就是 rdc backup restore --at 所接受的值 |
Created | 拍摄快照的 UTC 时间 |
Total | 该快照所代表的仓库镜像大小 |
Added | 该快照相对于之前的快照,实际额外上传的字节数 |
Chunks | 它新增了多少个单元格 |
要查看某次 rdc repo push --to <machine> 在目标机器上留下了什么,可以询问那台机器它持有什么:
rdc repo list --machine server-1
被推送的副本会以它自己的名字出现。旁边如果有第二行显示一个原始的 GUID,那就是保留下来的增量基础(delta base),正是它让下一次推送到那台机器时能做增量传输,而不是完整传输。
rdc backup list --machine <machine> 读取的是定时运行写入的 hot/ 和 cold/ 文件夹,因此对于 push 留下的副本来说,这是个用错了地方的工具,它不会向你显示任何内容。
| 列 | 含义 |
|---|---|
Mode | hot 或 cold。这条记录所属的定时备份文件夹 |
Name | 从本地配置解析出的仓库名(不在配置中的仓库会回退显示 GUID) |
GUID | 磁盘上的仓库 GUID |
Size | 备份文件的可读大小 |
Modified | 该文件在机器上的 UTC 时间戳 |
列出存储后端的功能已随 rclone 分支一起废止;该命令会被拒绝,并指出下面这两个替代方案。
保留策略
服务器会对整个分块存储强制执行按仓库设置的保留策略,因此旧快照会被自动清理,无需你手动删除任何内容。如果没有声明任何策略,所有快照都会被保留。
# 当前正在强制执行的策略。
rdc backup retention my-app
# 保留一个滚动窗口:7 个每日、4 个每周、6 个每月。
rdc backup retention set my-app --keep-daily 7 --keep-weekly 4 --keep-monthly 6
# 恢复为保留一切。
rdc backup retention clear my-app
| 选项 | 描述 |
|---|---|
--keep-last <n> | 保留最近的这么多个快照 |
--keep-hourly <n> | 在这么多个小时中,各保留每小时最新的一个快照 |
--keep-daily <n> | 在这么多天中,各保留每天最新的一个快照 |
--keep-weekly <n> | 在这么多周中,各保留每周最新的一个快照 |
--keep-monthly <n> | 在这么多个月中,各保留每月最新的一个快照 |
--keep-yearly <n> | 在这么多年中,各保留每年最新的一个快照 |
至少要给出一条规则。不带规则的 set 不会被当作”什么都不保留”来处理,而是会被拒绝,因为清空策略是 clear 的职责。
恢复
rdc backup restore 会把一份备份变成一个正在运行的仓库,两条路径用的都是同一个动词。区别在于你指向的是什么。
# 分块存储中的某个时间点。
rdc backup restore my-app --as my-app-yesterday --at <snapshot-id> --up
# push 留在某台机器上的一个产物。
rdc backup restore my-app@server-1 --as my-app --machine server-1 --up
--at 接受一个来自 rdc backup manifests 的快照 ID,或者一个 RFC 3339 格式的时间,例如 2026-08-14T12:00:00Z,它会被解析为该时刻或之前拍摄的最新快照。如果指定的时间之前没有任何快照,该请求会被拒绝,而不是向后舍入。
用 --as 以新名称恢复不会覆盖任何东西,因此可以在一台正在运行的机器上安全地做恢复演练。恢复到一个已经存在的名称会被拒绝。
| 选项 | 描述 |
|---|---|
<artifact-ref>(位置参数) | 要恢复的对象。分块存储的快照用 repo,机器上的产物用 repo@place |
--as <name> | 恢复出的仓库的名称(默认使用产物本身的名称) |
-m, --machine <machine> | 要恢复到的机器 |
--datastore <name> | 恢复到这个具名数据存储,由与之关联的机器承载 |
--at <time> | 恢复某个时间点:快照 ID 或 RFC 3339 格式的时间 |
--up | 传输完成后部署恢复出的仓库 |
--health-window <seconds> | 观察已部署仓库健康状况的时长 |
--health-timeout <seconds> | 等待其变为健康状态的时长 |
-y, --yes | 跳过确认 |
--debug | 启用详细输出 |
恢复一个仓库需要用到你配置中保存的 LUKS 凭据。如果你启用了 config storage,该凭据会随配置一起在新机器上恢复。如果没有启用,请把配置的副本保存在不会随该机器故障一起消失的地方。
在每台机器上验证恢复
一台从未完整走过来回流程的机器,无论其上传看起来多么顺利,都不能算是有备份。上传和恢复失败的原因是不同的,后一种失败只有在你真正尝试恢复时才会显现。
在依赖这些备份之前,请在每台机器上都执行一次以下流程:
- 做一个快照:
rdc backup snapshot my-app。 - 确认它已被记录:
rdc backup manifests my-app。 - 用一个一次性名称恢复它:
rdc backup restore my-app --as my-app-drill --at <snapshot-id>。 - 将恢复出的仓库与源仓库比对,然后用
rdc repo delete my-app-drill --yes删除这份演练副本。
整个流程完全不会触碰正在运行的仓库,因此即使在正承载流量的机器上执行也是安全的。如果你正在从一套更老的备份方案迁移过来,请让旧方案继续运行,直到这套流程在该机器上至少成功通过一次为止。两条备份路径要付出的是存储成本;一条未经验证的路径要付出的则是数据本身。
一次同步一个仓库
Push 和 pull 都作用于以引用(name、name:tag 或 name@machine)指定的单个仓库。不存在”一次处理所有仓库”这种形式:请针对每个仓库各运行一次命令。
同时指定分支和机器的引用,和普通名称的工作方式相同:
rdc repo push shop:nightly@server-1 --to server-2
rdc repo pull shop:nightly@server-1 --from server-2
完整的选项列表在将备份推送到另一台机器和从另一台机器拉取备份中。
定时备份
Rediacc 使用具名的备份策略。每个策略定义一个时间表、备份模式、可选的带宽限制,以及文件过滤条件。你可以把策略名绑定到机器上,以控制哪些备份在哪里运行。
备份模式
| 模式 | 行为 | 停机时间 |
|---|---|---|
hot | 服务持续运行的同时冻结仓库镜像(崩溃一致) | 无 |
cold | 服务停止、拍摄快照、服务重启、快照上传(应用一致) | 每个仓库的停止+启动时间窗口,各仓库之间并行进行。参见下方”估算冷备份停机时间”。 |
对能容忍崩溃一致快照的服务,使用 hot。当你需要有保证的一致性,并且能接受短暂重启时,使用 cold。
冷备份的语义
一次冷备份对每个被纳入的仓库分三个阶段执行:停止 → 快照 → 启动。了解这些保证的边界在哪里,能帮你及早发现部分性的失败。
冷备份能保证的事:
- 在拍摄快照之前,每个被纳入的仓库中所有正在运行的容器,都会通过其 Rediaccfile 的
down()钩子被优雅地停止,该仓库专属的 Docker 守护进程也会被静止。因此这份快照不仅是崩溃一致的,更是应用一致的。 - 快照前正在运行的容器 ID 集合会被持久化到位于
/var/run/rediacc/cold-backup-<guid>.running.json的一个 sidecar 文件中。这就是”完成后应该重新运行哪些容器”的权威依据。 - 快照之后,会调用该仓库 Rediaccfile 的
up()钩子,以恢复完整的 compose 技术栈。 - 位于
/var/run/rediacc/cold-backup-<guid>.status.json的每次运行状态 sidecar,会记录每次尝试的阶段、结果以及任何错误。
冷备份不保证的事:
up()是尽力而为的。它可能因为超出冷备份控制范围的原因而失败(例如一个仍在等待的depends_on: service_healthy条件、一处 compose 文件语法错误、拉取镜像时一次瞬时的网络故障)。失败发生时,冷备份会以 error 级别记录该错误,写入状态 sidecar,然后继续处理下一个仓库。- 当
up()失败时,会启动一个兜底的直接重启:系统会读取运行状态 sidecar,并通过 Docker API 直接重启每个记录在案的容器 ID(不经过 compose)。这样即便 compose 流程出了问题,也能让服务重新运行起来,只是不会重新执行任何 Rediaccfile 钩子。 - 如果连兜底方案对某些容器 ID 也失败了(比如 Docker 守护进程本身已经宕机),该 sidecar 会保留原地,以便路由 watchdog 在每个 tick 都能继续重试。
Watchdog 恢复机制: 在每个 tick,watchdog 都会检查是否存在一个运行状态 sidecar。其中列出的任何容器 ID,如果当前处于停止状态,都会被重启,无论该容器保存的 restart_policy 是什么。这意味着即便是配置了 restart: on-failure(Docker 在一次干净停止后本不会重启)的服务,在冷备份之后也会被重新拉起。一旦列出的所有容器都恢复运行,该 sidecar 就会被删除。
运维人员发现故障的方式:
rdc machine status <machine> --containers会显示运行状态,可与预期的容器集合比对。- 机器上的
/var/run/rediacc/cold-backup-<guid>.status.json,可以通过rdc term connect <repo> -c "cat /var/run/rediacc/cold-backup-$GUID.status.json"查看。success: false加上一个过期的startedAt,意味着上一次备份没有干净地完成。 - renet 备份运行的日志(
journalctl -u renet-*,或者直接调用rdc backup schedule)会输出一行形如Cold backup: post-snapshot restart summary total=N compose_ok=N fallback_ok=N failed=N failed_repos=[...]的最终汇总。一个非空的failed_repos就是你要 grep 的目标。
估算冷备份停机时间
每个仓库只在其自身的 down() + up() 窗口内处于停机状态。在一台已预热的主机上,这通常是:
| 仓库规模 | 典型的停止+启动时间 |
|---|---|
| 小型(1-2 个容器,无数据库) | 5-15 秒 |
| 中型(Web 应用 + 缓存) | 20-45 秒 |
| 重型(数据库 + 队列 + 邮件) | 60-120 秒 |
冻结这一步是对仓库镜像做一次写时复制(copy-on-write)reflink。它只是元数据操作,因此无论仓库是 1 GB 还是 100 GB,所需时间都一样,而在一次实测运行中甚至没有达到毫秒级分辨率。一个仓库不会因为其他仓库正在冻结而被拖着一直处于停止状态。上传随后会针对已冻结的副本进行,而这期间所有仓库都已经重新恢复运行。
整次运行的总墙钟时间取决于有多少仓库在同时重启。renet 会根据主机情况推导出这个值:
concurrency = min(repoCount, max(2, NumCPU/2), 8)
示例:
| 主机 | 仓库数 | 并发数 | 墙钟重启时间 |
|---|---|---|---|
| 4 核虚拟机 | 5 个仓库,平均每个 30 秒 | 2 | 约 75 秒 |
| 16 核服务器 | 10 个仓库,平均每个 40 秒 | 8 | 约 80 秒 |
| 64 核集群节点 | 50 个仓库,平均每个 40 秒 | 8 | 约 4 分钟 |
通过环境变量覆盖: 在备份服务的环境中设置 REDIACC_COLD_BACKUP_CONCURRENCY=N(通常通过一个 systemd drop-in 配置)可以固定一个具体值。=1 会强制严格串行重启,在调试某个仓库 up() 钩子中的崩溃循环问题时很有用。
如果你运行的是一个对延迟敏感的仓库(公开的 Web 应用、邮件服务),它的停机时间只受限于它自身的停止+启动耗时(通常 30-90 秒),而不是整次运行的总时长。仓库会按被发现的顺序被分配到并发槽位,不存在优先级队列。如果需要更精细的调度,可以给重型仓库单独设置一个以 --include 限定范围的策略。
长时间运行的备份与重叠的调度
一次耗时超过其自身调度间隔的冷备份(例如,一个 500 GB 仓库的首次播种,在一条普通带宽的线路上合理地需要超过 24 小时,而在此期间夜间定时器又再次触发)不会把第二次运行排队或启动。systemd 的 Type=oneshot 单元是单实例的:当定时器触发、而服务已经处于 activating 状态时,systemd 会把这次启动合并到已有的任务中。不会启动新进程,也不会把这次运行排到之后。
具体来说,一次从周一 03:00 UTC 开始、到周四中午才结束的运行:
| 日期 | 03:00 UTC 触发 | 结果 |
|---|---|---|
| 周一 | 第一次触发 | 运行开始 |
| 周二 | 第二次触发 | 静默丢弃(前一次运行仍处于活动状态) |
| 周三 | 第三次触发 | 静默丢弃(前一次运行仍处于活动状态) |
| 周四 | 运行在中午结束 | 不会补跑;下一次运行是周五 03:00 UTC |
定时器的 Persistent=true 指令不会挽救这些被跳过的触发。Persistent=true 重放的是因为定时器本身处于非活动状态(系统关机、定时器被禁用)而错过的触发。因服务繁忙而被丢弃的触发则会彻底消失。
这个默认行为是刻意为之的。如果对同一个数据存储并行运行两次冷备份,会在冻结路径、上传过程,以及 /var/run/rediacc/cold-backup-<guid>.status.json 下每个仓库的 sidecar 上产生冲突。排在一个正在运行的实例后面等待,要好过从两个方向同时冲击同一份数据。数据存储锁强制执行了这一点:第二次冷运行发现锁被占用后,会立即被拒绝,不会停止任何东西。
对监控的影响。 一次挂起卡住的备份(例如,一次卡在网络黑洞中的上传)会静默丢弃之后所有的定时器触发。调度器不会发出任何警报。请关注 systemctl show <unit> -p ActiveEnterTimestamp:如果服务处于 activating 状态的时间超过了预期的运行时长(例如,对于一个夜间定时器超过了 48 小时),就应该去调查一下。
如果你需要每一次计划中的触发都被执行,请把定时器从 OnCalendar=<cron> 切换为 OnUnitInactiveSec=<间隔>。它会在上一次运行结束 N 小时之后触发,而不是按固定的墙钟时间表触发,因此长时间运行不会导致触发被丢弃,只会把下一次运行往后推。代价是调度会发生漂移:原本 03:00 的夜间任务会变成”上一次结束后 24 小时”。
快照、中断与存储池空间
每一次 push 都是基于一个瞬时的数据存储快照来工作的,因此即使仓库还在持续写入,上传的数据依然是一致的。在备份运行期间,该快照会持续引用它与在线仓库共享的每一个数据块:因此在这个周期结束、快照被删除之前,删除操作和 trim 释放出的存储池空间会变少。存储健康报告会显示备份快照当前占用了多少空间。
中断是安全的。停止服务(或重启机器)会让备份中止其传输,并在退出前删除自己的快照;下一次计划运行会从中断的地方继续,因为已经存储过的单元格不会被重复上传。如果进程被强制终止得过于粗暴,来不及清理(比如断电),孤立的快照会在几分钟内被存储维护进程自动检测并移除。
定义一个策略
默认配置是两种策略的组合:一个覆盖所有仓库的、快速的每小时热流,以及一个通过静止容器来获取应用一致快照的、较慢的每周冷流。两者都写入同一个分块存储,共享的数据块只会保存一份,而不是按流各存一份。
rdc backup strategy set hourly-hot \
--destination rediacc \
--cron "0 * * * *" \
--mode hot \
--bwlimit 20M \
--enable
rdc backup strategy set weekly-cold \
--destination rediacc \
--cron "15 3 * * 0" \
--mode cold \
--include shop --include mail \
--enable
--destination <name> 用来给策略内部的目标命名;这是你自己选择的一个标签,用来描述这个分块存储。--include 列出要备份的仓库,重复该参数可以添加更多。省略它时,该策略会覆盖数据存储上的每一个仓库。名称需要与本地配置中的仓库名匹配(不带 :tag)。
对于分块存储目标,--exclude 会被直接拒绝,而不是被悄悄忽略,因为底层的 backup snapshot 是通过指名来选择仓库的,并没有自己的排除机制。如果尊重这个参数,就意味着会把你要求排除在外的仓库也备份进去。请改用 --include 来限定策略的范围,这样一次计划运行覆盖了什么就是明文写下来的,而不是靠推断得出的。
| 选项 | 描述 |
|---|---|
<strategy>(位置参数) | 策略名(用于绑定机器) |
--destination <name> | 策略内部的目标名称。默认为分块存储 |
--storage <name> | 主动选用已废止的 rclone 目标类型。使用它的调度无法被部署 |
--cron <expression> | cron 表达式(例如 "0 2 * * *" 表示每天凌晨 2 点) |
--mode <hot|cold> | 备份模式 |
--bwlimit <limit> | 上传的带宽限制(例如 10M) |
--include <repos> | 该策略覆盖的仓库(可重复) |
--exclude <repos> | 要跳过的仓库(可重复)。在分块存储目标上会被拒绝 |
--folder <path> | rclone 存储桶内的子文件夹。在分块存储目标上会被拒绝 |
--enable / --disable | 启用或禁用该策略 |
查看策略
rdc backup strategy list
rdc backup strategy show weekly-cold
移除一个策略
rdc backup strategy remove weekly-cold
将策略绑定到机器
一个没有绑定到任何机器的策略永远不会被部署。将一个或多个策略绑定到机器:
rdc backup strategy bind hourly-hot --machine hostinger
rdc backup strategy bind weekly-cold --machine hostinger
rdc backup strategy unbind weekly-cold --machine hostinger
绑定关系会作为机器上的一个列表被记录在你的配置中,rdc backup schedule 会读取它来决定要部署哪些单元:
{
"machines": {
"hostinger": {
"backupStrategies": ["hourly-hot", "weekly-cold"]
}
}
}
绑定只存在于本地配置中。 定义一个策略并把它绑定到某台机器,并不会对那台机器做任何操作。要部署 systemd 定时器,请运行
rdc backup schedule -m <machine>(参见将调度部署到机器),并在每次修改策略或绑定关系之后重新运行它。
选择热还是冷,以及按仓库过滤
热与冷一览
| 热 | 冷 | |
|---|---|---|
| 一致性 | 崩溃一致(运行中冻结镜像) | 应用一致(停止 → 冻结 → 启动) |
| 停机时间 | 无 | 每个仓库的停止+启动时间窗口(通常 5-120 秒) |
| 适用频率 | 高(例如每小时) | 低(例如每天或每周) |
| 典型用途 | 高频的安全网 | 计划中的、有一致性保证的备份 |
热是高频运行的正确默认选择。在拍摄快照期间服务持续运行,因此对你的应用来说完全没有停机时间。快照是崩溃一致的:相当于一次非正常关机之后你会得到的状态。对大多数现代数据库和消息队列来说,这已经足够了。
冷适用于你需要有保证的应用一致快照,并且能接受每个仓库短暂重启的场景。服务会在快照之前停止,并在上传开始之前重启,因此一次缓慢或失败的上传永远不会拖长停机窗口。完整的保证模型请参见冷备份的语义。
两种模式都写入同一个分块存储。模式决定的是镜像被冻结期间如何对待仓库,而不是数据落在哪里。一个同时被每小时热调度和每周冷调度覆盖的仓库,共享的单元格只会保存一份,而不是两份。
按策略确定仓库范围
一个没有 --include 的策略会覆盖数据存储上的每一个仓库。重复使用 --include 会把范围收窄到你指名的那些仓库,匹配依据是本地配置中的仓库名(不带 :tag)。
# 热策略:每小时备份所有内容
rdc backup strategy set hourly-hot \
--destination rediacc \
--cron "0 * * * *" \
--mode hot \
--bwlimit 6M \
--enable
# 冷策略:每周,只针对需要静止的仓库
rdc backup strategy set weekly-cold \
--destination rediacc \
--cron "15 3 * * 0" \
--mode cold \
--include shop --include mail \
--enable
什么时候应该把某个仓库排除在高频热策略之外
在以下情况下,应该指名你想纳入高频运行的仓库,而不是让它接纳一切:
- 一个仓库体积很大,并且能从卷上已有的源数据完全重新生成,以至于每次每小时的备份都在消耗带宽,却没有带来实质的恢复价值。
- 按可用的上传速度计算,该次备份运行会超出它自身的调度间隔。
示例。 一个 analytics-demo 仓库保存着约 114 GB 派生自 Postgres 的表,这些表可以从保存在同一个卷内的原始 CSV 转储重建出来。在 6 MB/s 的上传限速下,该仓库的第一次快照就需要超过 5 小时。如果每小时运行一次,就意味着下一次触发到来时上一次运行还没结束,因此之后每一次触发都会被静默丢弃(参见长时间运行的备份与重叠的调度)。把其他仓库列在 hourly-hot 中、把 analytics-demo 留给 weekly-cold,就意味着它每周会被备份一次,而不是完全没有备份。
如果数据是纯粹可再生的,不妨考虑一下是否真的需要备份它。一种替代方案是只备份原始的源输入(在这个例子中就是 CSV 转储),完全跳过派生出来的副本。对源输入做每周一次的冷备份体积要小得多,而且完全足以支撑恢复。
一个同时被两种策略覆盖的仓库,会同时获得每小时的崩溃一致快照和每周的应用一致快照。rdc backup manifests <repo> 会把它们一起显示出来,共享的单元格只会保存一份。
备份操作
将调度部署到机器
将已绑定的策略以 systemd 定时器的形式推送到某台机器:
rdc backup schedule -m server-1
rdc backup schedule -m server-1 --dry-run
部署是一个状态协调器。它会读取机器上当前的单元文件和 systemd 状态,与配置本应生成的内容进行比对(逐文件计算 SHA-256),然后只触碰内容确实发生了变化的单元。在配置没有变化的情况下重新运行是一次空操作:不写入任何内容,不执行 daemon-reload,不产生任何定时器扰动。
--dry-run 会打印每个策略的计划(created、updated (service, timer, env)、unchanged、removed),而不会触碰机器。结合 --debug 使用还会打印生成的单元内容(凭据部分会被隐去)。分块存储单元本来就不携带任何凭据:机器用自己已签名的仓库许可证进行身份验证,服务器再返回一个短期有效的许可,因此单元文件中不会写入任何敏感信息。
如果你即将更新或移除的某个策略当前正有备份在运行,部署会立即失败,并提示你取消该运行或传入 --force。使用 --force 时,正在运行的那次调用会保留其内存中的单元,新配置会在下一次定时器 tick 时生效,因此正在运行的备份永远不会被强制终止。
--reset-failed 是可选启用的。传入该参数时,它会在部署成功后清除被触碰服务的 systemd failed 状态。默认关闭,这样之前的失败信号会继续在告警中保持可见。
立即运行一次备份
无需等待定时器,立即触发一次备份。即使还没有部署任何定时器也能工作,它使用 systemd-run 进行临时执行:
rdc backup run -m server-1
rdc backup run weekly-cold -m server-1
查看备份状态
显示备份定时器的当前状态以及最近几次任务的结果:
rdc backup status -m server-1
rdc backup status hourly-hot -m server-1
取消一次正在运行的备份
rdc backup cancel -m server-1
rdc backup cancel weekly-cold -m server-1
仓库迁移
将一个仓库从一台机器移动到另一台机器:
rdc repo migrate my-app@server-1 --to server-2
| 选项 | 描述 |
|---|---|
<ref>(位置参数) | 要迁移的仓库引用;其中的 @machine 指明源机器 |
--to <place> | 目标机器或集群 |
--provision <provider> | 通过此云提供商自动配置目标机器(例如 hetzner、linode) |
--checkpoint | 迁移前创建 CRIU 检查点,以便进程内存也一并迁移 |
--delta-base <guid> | 用于切换阶段增量的不可变基础 GUID。默认使用第一阶段的基础 |
--strategy <strategy> | 切换阶段所用的块增量策略:auto、physical 或 shared |
--skip-dns | 迁移后跳过 DNS 记录更新 |
--keep-source | 迁移成功后保留源镜像 |
--bwlimit <limit> | 传输的带宽限制(例如 50M) |
迁移会分两个阶段通过 rsync 传输加密的仓库数据:先是在仓库继续运行时进行一次批量传输,然后再短暂停止以完成增量部分。迁移会移动仓库,因此一旦迁移成功,源镜像就会被删除。传入 --keep-source 可以保留它们。这就是 repo migrate 和 repo push 的区别:push 会让源仓库继续运行,不做任何改动。
读取废止之前写入的存档
rdc storage 是 rclone 分支残留下来的部分,而且是只读的。它已经不能再作为备份目标,但仍然可以用来访问以那种方式写入的存档。
# 注册一个你已经为 rclone 配置好的远程存储。
rdc storage import rclone.conf
rdc storage list
# 查看里面有什么。这会运行你 PATH 中的 rclone。
rdc storage browse my-storage
import 会读取一个 rclone 配置文件,并把其中的远程存储记录到你的配置中;支持的类型有 S3、B2、Google Drive、OneDrive、Mega、Dropbox、Box、Azure Blob 和 Swift。
browse 要求你的 PATH 中存在 rclone。 它运行的是你正在操作的这台机器上安装的 rclone;不再内置任何副本。如果没有安装,它会明确告知你这一点,不会再做其他任何事。
向存储后端推送、拉取、列出和恢复的功能均已废止;每一个都会被拒绝,并指出替代它的命令。
最佳实践
- 为关键数据的应用一致副本安排每日冷快照
- 对需要零停机的高频运行使用热快照
- 定期测试恢复。
rdc backup restore --as <new-name>不会覆盖任何东西,因此在正在运行的机器上做演练是安全的 - 设置保留策略,而不是手动清理,这样你保留的时间窗口就是明文记录下来的
- 如果希望在你自己掌控的硬件上留一份副本,除了快照之外还应保留一份机器到机器的副本
- 妥善保管凭据;备份是加密的,但恢复时仍需要 LUKS 凭据