物理主机备份仓库完整性:用 restic 验证读取、恢复与损坏检测

物理主机备份仓库完整性:用 restic 验证读取、恢复与损坏检测

备份任务返回成功,恢复时却读不出文件,问题可能藏在备份完成后的存储损坏中。只核对快照数量,适合确认任务有没有运行,却不能证明数据包仍然完整。存储管理员需要三类独立证据:仓库结构正确、数据能够全部读出、恢复后的内容与基线一致。

本文使用 Debian 12、restic 与 Python 3,在临时目录中完成正常检查和损坏检测。不读取生产数据,不格式化磁盘,也不修改现有备份仓库。实验适用于物理主机上的文件备份;运行中的数据库应先通过数据库自身机制生成一致性备份,再进入文件备份流程。

一、先确定检查究竟证明什么

restic 将文件内容组织为加密的数据包,并维护快照、索引与引用关系。普通 check 主要检查仓库结构;check --read-data 进一步读取并校验全部数据包。两者不能互相替代,恢复测试也不能省略。

恢复比对关注的是选中的快照能否在空目录中重新形成预期文件,而不是仓库中每个历史数据包是否完整。反过来,全量读取成功,也不能证明备份时选对了目录、排除规则没有误伤,或应用数据本身一致。

镜像解决部分设备故障下的连续访问,备份保留历史版本,完整性检查发现异常;三者都不能代替实际恢复。

OnuCloud 的 AMD 物理主机标配两块 1TB NVMe,各自直连,没有硬件 RAID。把源目录与备份仓库分别放在同一台机器的两块盘上,可以隔离部分单盘故障,却不是独立故障域中的异地备份。以下本地仓库只用于验证操作流程。

二、建立可重复的隔离样本

先安装工具。随后在同一个 Bash 会话中运行实验命令,避免临时变量丢失。安装需要管理员权限,实验本身使用普通账户即可。

sudo apt-get update
sudo apt-get install restic python3

umask 077
LAB=$(mktemp -d /var/tmp/restic-lab.XXXXXXXX)
export LAB
export RESTIC_REPOSITORY="$LAB/repository"
export RESTIC_PASSWORD_FILE="$LAB/password"

mkdir -p "$LAB/source"
python3 - <<'PY'
import os
import secrets
from pathlib import Path

root = Path(os.environ["LAB"])
(root / "password").write_text(secrets.token_hex(32) + "\n")
(root / "source" / "record.txt").write_text(
    "backup verification\n", encoding="utf-8"
)
(root / "source" / "payload.bin").write_bytes(os.urandom(1024 * 1024))
PY

restic version
restic init
(
  cd "$LAB"
  restic backup source
)
restic snapshots

随机文件避免样本全部被压缩成很小的数据,也保证仓库中存在实际内容。密码文件位于仓库目录之外,复制仓库时不会顺带复制解密凭据。

这里的快照只有一个,后续可以使用 latest。生产环境应记录明确的快照 ID,否则检查期间有新备份完成时,恢复目标可能发生变化。不要把临时实验密码误认为已经完成正式密钥托管;生产仓库需要独立保存且能够取回的解密凭据。

三、分别验收全量读取与恢复内容

先执行结构检查,再读取所有数据包。不要把输出直接接到未经退出码处理的管道,否则最后一个命令成功可能掩盖检查失败。

restic check
restic check --read-data
restic restore latest --target "$LAB/restored"
diff -r -- "$LAB/source" "$LAB/restored/source"

四步都应以零退出码结束,diff 正常时没有差异输出。当前样本只含普通文件,因此内容与目录比较足够;真实备份还应单独核对所有者、权限、链接关系及应用所需的扩展属性。

恢复必须落到新的空目录,不覆盖源文件。否则旧文件可能掩盖漏备份,新恢复的数据也可能污染线上目录。完成后保留快照 ID、工具版本、检查输出与比对结果;仅留一张“任务成功”的截图,不足以复查具体恢复对象。

如果 check 已经失败,应停止后续正常验收并保存现场。若结构检查成功、全量读取失败,则应重点调查数据包读取和内容完整性,而不是因为快照列表正常就继续使用仓库。

四、只对副本注入损坏并确认拒绝

负向测试用于证明检查链真的能够发现问题。先停止对实验仓库的写入,再复制一个独立副本。禁止使用硬链接复制;否则修改副本可能同时修改原仓库。以下命令明确关闭写时复制,实验体量很小,便于理解两份数据的边界。

cp -a --reflink=never "$LAB/repository" "$LAB/damaged"

python3 - <<'PY'
import os
from pathlib import Path

data = Path(os.environ["LAB"]) / "damaged" / "data"
pack = next(p for p in sorted(data.rglob("*")) if p.is_file())
with pack.open("r+b") as f:
    f.seek(pack.stat().st_size // 2)
    pos = f.tell()
    original = f.read(1)
    if not original:
        raise RuntimeError("Empty data pack")
    f.seek(pos)
    f.write(bytes([original[0] ^ 1]))
print(pack.name)
PY

if restic -r "$LAB/damaged" check --read-data \
  >"$LAB/damaged-check.log" 2>&1; then
  printf '%s\n' 'UNEXPECTED_SUCCESS'
else
  printf '%s\n' 'EXPECTED_FAILURE'
fi
cat "$LAB/damaged-check.log"
restic check --read-data

预期副本检查非零退出,日志指出数据包损坏;原仓库检查仍成功。不能只看脚本打印的失败标记,还要确认失败原因确实是损坏,而非密码错误、路径错误或权限不足。

这一步证明的是损坏能够被发现,不是损坏能够被修复。数据包中的某个字节改变,可能影响其中一个或多个对象;不要据此推算生产事故的影响比例,也不要将这份副本重新作为备份目标。

五、把实验结论变成生产检查边界

全量读取会消耗磁盘吞吐,远程仓库还会消耗网络带宽。执行前记录仓库大小、检查范围与业务负载,选择低负载时段,并观察业务延迟。不限流量不等于传输速度无限,也不意味着完整性扫描没有资源成本。

大型仓库可以分批读取数据包,但必须记录每次覆盖范围,确保最终覆盖全部数据;一次抽样成功不能写成全仓库通过。检查任务还应避免与清理、修复及其他改变仓库内容的操作无计划地并发。

发现损坏后,先暂停修改仓库的任务,保留日志和副本,再核对文件系统、设备与传输链。不要立即执行 forget、prune 或修复命令,以免减少可恢复证据。

最终验收应同时具备三项结果:正常仓库可完整读取,指定快照可恢复并通过内容比对,受损副本被明确拒绝。实验文件可在记录保存后人工核对路径再删除;密码、日志和恢复样本都属于需要受控处理的材料。

常见问题

restic check 成功是否说明所有备份数据都已读过?

不是。普通 check 主要检查仓库结构和引用关系;需要加上 --read-data 才会读取并校验全部数据包。即使全量检查成功,也应另做隔离恢复与内容比对。

发现仓库损坏后可以直接执行清理吗?

不应立即执行 forget、prune 或修复操作。先暂停修改仓库的任务,保留日志与仓库副本,排查存储和传输链,再从独立副本验证可恢复的数据。

AMD EPYC 7543P 独享物理主机

$472/月起

配置 OnuCloud 物理主机 核对 OnuCloud 租赁价格

物理主机配置,按负载登记

AMD EPYC 7543P 独享整机,存储冗余与数据备份由管理员自行规划。