用 Borg 备份磁盘镜像并区分一致性层级

Borg 可以高效保存整块磁盘的镜像:即使镜像文件很大,去重仍能只保存与已有数据不同的块。恢复时,Borg 还提供可选的简单稀疏文件支持。但“备份命令完成”和“这份镜像能够可靠恢复”之间,隔着磁盘身份、读取时点、应用一致性以及恢复目标等一系列条件。

本文译编自 BorgBackup 1.4.5 官方文档 Backing up entire disk images,归属 Borg 文档贡献者、The Borg Collective 与 Jonas Borgström。全文按 2026 年 10 月 5 日获取的版本核对。下面保留原文技术流程,并把静态审查发现的危险边界附在相应步骤旁;本次没有读写磁盘设备、创建备份、执行清零或恢复。

Borg备份应先获得停止写入的源或一致快照,再创建仓库归档;恢复先提取并检查镜像,最后另行核对目标磁盘。崩溃一致、应用一致是不同层级
从稳定数据源到可审核恢复的流程。未完纪自绘示意图;不表示实际完成过恢复测试。

先准确确定要读的磁盘

不要把易变化的 /dev/sdX 或 /dev/nvmeXnY 名字当作磁盘身份。原文建议先查询序列号,再从 /dev/disk/by-id/ 找到设备:

# 原文示例:序列号只是示例值
# udevadm info --query=property --name=nvme1n1 \
#   | grep ID_SERIAL_SHORT | cut -d '=' -f 2

DISK_SERIAL="7VS0224F"
DISK_ID=$(readlink -f /dev/disk/by-id/*"${DISK_SERIAL}")

mapfile -t PARTITIONS < <(
  lsblk -o NAME,TYPE -p -n -l "$DISK_ID" \
    | awk '$2 == "part" {print $1}'
)
echo "Partitions of $DISK_ID:"
echo "${PARTITIONS[@]}"
echo "Disk Identifier: $DISK_ID"

# 原文列出的备份方式,均需先核对设备与一致性:
# borg create --read-special /path/to/repo::{now} "$DISK_ID"
# borg create --read-special /path/to/repo::{now} "${PARTITIONS[@]}"

--read-special 让 Borg 读取明确指定的特殊设备内容,而不是只记录设备节点。备份整盘会包括分区表与各分区;只备份分区则不能自然替代完整磁盘布局的备份。

静态审查补充:上面的通配符可能匹配零个或多个路径,序列号也可能与分区或别名产生歧义。应核对最终路径、序列号、容量、分区表和挂载状态,确认它们指向唯一目标。不要将 --read-special 用到整个 /dev,也不要随意带入普通全盘目录备份。原文使用 mapfile 与进程替换,因此它是 Bash 代码,不是任意 POSIX shell 都能执行。

为什么删掉文件后镜像仍然很大

未压缩的整盘镜像与磁盘容量相当。多数文件系统删除文件时,移除的是指向数据的目录或元数据条目,旧数据本身仍留在磁盘。若磁盘曾经接近写满,之后即使删除大量文件,镜像在压缩和去重后也不一定明显缩小。

原文介绍两条按文件系统选择的路线:NTFS 可以用 ntfsclone 的专用镜像格式保存有效内容;ext2、ext3、ext4 可以考虑 zerofree 处理未使用的数据块。二者的工作方式不同,不能仅因为都能节省空间就互换。

NTFS:分别保存磁盘头与分区内容

ntfsclone 要求文件系统日志处于已清理状态,典型条件是正常关闭的机器。这限制了它在运行中虚拟机快照上的直接使用。满足条件时,它的特殊镜像格式可以比“把空闲区清零后再去重”更节省空间。

原文先保存磁盘前部,再按分区类型分别处理。下面是原文流程,$DISK 在这一段必须是已经核对过的源设备;它与前一段的 $DISK_ID 命名不同,不能直接拼接执行:

# 原文备份流程;保留其变量名以便核对
HEADER_SIZE=$(sfdisk -lo Start $DISK | grep -A1 -P 'Start$' | tail -n1 | xargs echo)
PARTITIONS=$(sfdisk -lo Device,Type $DISK | sed -e '1,/Device\s*Type/d')
dd if=$DISK count=$HEADER_SIZE | borg create repo::hostname-partinfo -
echo "$PARTITIONS" | grep NTFS | cut -d' ' -f1 | while read x; do
    PARTNUM=$(echo $x | grep -Eo "[0-9]+$")
    ntfsclone -so - $x | borg create repo::hostname-part$PARTNUM -
done
# 非 NTFS 分区:
echo "$PARTITIONS" | grep -v NTFS | cut -d' ' -f1 | while read x; do
    PARTNUM=$(echo $x | grep -Eo "[0-9]+$")
    borg create --read-special repo::hostname-part$PARTNUM $x
done

这里的 ntfsclone -so - 把特殊镜像写到标准输出,Borg 用末尾的 - 接收标准输入。非 NTFS 分区则使用 --read-special 读取原始设备。

这段脚本不能作为通用生产脚本直接使用。静态核对可见:部分变量没有引用;用人类可读的 sfdisk 输出、空格与类型字符串解析分区容易受格式和本地化影响;靠设备名末尾数字推断分区号依赖命名方式;dd count 按输入块数计数,原文没有显式设定块大小,不能把分区起点扇区数无条件当作该值。512 字节扇区、4 KiB 逻辑扇区及 GPT 布局需要分别核实;只保存“第一分区前”的区域也不能替代对完整分区表和备份 GPT 位置的检查。

原管道还没有设置 pipefail。上游读盘或 ntfsclone 失败时,下游归档进程有可能正常结束而只保存部分输入。使用 pipefail 有助于发现失败,但不会自动补全归档,更不会把不一致的数据变成一致数据。

恢复不是把管道方向倒过来就结束

原文的恢复逻辑是:先恢复磁盘头,刷新分区信息,再遍历分区归档;NTFS 用 ntfsclone 还原特殊镜像,其他分区用 dd 写回。以下代码是危险的原文恢复示例,仅供逐行审阅;写入目标一旦选错,会覆盖现有数据。

# 危险:原文代码会直接覆盖 $DISK 及其分区,不应直接复制运行
borg extract --stdout repo::hostname-partinfo | dd of=$DISK && partprobe
PARTITIONS=$(sfdisk -lo Device,Type $DISK | sed -e '1,/Device\s*Type/d')
borg list --format {archive}{NL} repo | grep 'part[0-9]*$' | while read x; do
    PARTNUM=$(echo $x | grep -Eo "[0-9]+$")
    PARTITION=$(echo "$PARTITIONS" | grep -E "$DISKp?$PARTNUM" | head -n1)
    if echo "$PARTITION" | cut -d' ' -f2- | grep -q NTFS; then
        borg extract --stdout repo::$x | ntfsclone -rO $(echo "$PARTITION" | cut -d' ' -f1) -
    else
        borg extract --stdout repo::$x | dd of=$(echo "$PARTITION" | cut -d' ' -f1)
    fi
done

除了未经引用的变量和文本解析,这里还存在一个容易误判的退出状态问题:默认 shell 管道通常报告最后一个程序的状态,dd 成功不代表上游 borg extract 完整成功。即使随后用 && partprobe,也不能据此认定整个提取成功。grep 中拼接的磁盘路径还是正则表达式,不能当作精确设备匹配。

原恢复代码里的 "$DISKp?$PARTNUM" 还有一个具体的变量展开错误:shell 会把 DISKp 视为变量名,而不是把字母 p 接在 DISK 后面。即使明确变量边界,也仍需解决正则转义、精确匹配和设备命名差异,不能仅修正这一处就直接写盘。

编辑建议与原文的差异:把恢复拆成可审核的两个阶段。先将明确选定的归档成员完整提取到容量充足、访问受限的普通文件,检查退出状态、预期大小以及可用的校验和或文件系统检查结果;保留这份提取结果,再在独立恢复环境中核对目标序列号、容量、分区布局与未挂载状态。最后的设备覆盖命令必须按实际盘型和镜像格式另行确定。本稿不把“提取成功”写成“恢复已验证”。

原文若面对普通镜像文件而不是块设备,会用 losetup -Pf --show /path/to/disk/image 建立 loop 设备,并把返回值赋给 DISK;完成后用 losetup -d $DISK 分离。它会改变宿主的设备映射,通常需要相应权限;只用于实验副本,保留准确的映射关系和清理记录。此处也没有执行这些操作。

ext 文件系统:zerofree 会修改原分区

zerofree 查找文件系统内未使用的数据块并清零。与生成另一份特殊镜像的 ntfsclone 相比,它会原地修改源分区。原文流程是先处理识别为 Linux 的分区,再让 Borg 读取整盘:

# 危险:原文示例会原地清零空闲数据块
sfdisk -lo Device,Type $DISK | sed -e '1,/Device\s*Type/d' | grep Linux | cut -d' ' -f1 | xargs -n1 zerofree
borg create --read-special repo::hostname-disk $DISK

# 危险:原文恢复命令会覆盖设备
borg extract --stdout repo::hostname-disk | dd of=$DISK

grep Linux 并不能证明分区就是受支持的 ext 文件系统。执行前必须确认具体文件系统、卸载或满足工具要求的只读状态,以及已有可恢复副本。对加密设备、LVM 或复杂层叠设备,不能只凭分区表的类型标签判断。

原文还反对一种传统做法:从 /dev/zero 写满临时文件,再删除它。这既慢,又会让稀疏磁盘镜像暂时增长到最大范围,还会耗尽空闲空间,导致其他并发写入失败。不要为了压缩比而把运行中的文件系统写满。

虚拟机镜像的一致性必须由外部机制提供

Borg 看待虚拟机镜像的方式与普通文件相同。它不负责暂停来宾,也不自动建立文件系统快照。如果备份期间来宾持续写镜像,Borg 读到的不同区域可能来自不同时刻,最终归档会出现不一致甚至损坏。因此,使用 Borg 这类自身不生成快照的工具时,最直接的办法是在整个读取期间关闭虚拟机。

需要备份运行中的虚拟机时,可以先在宿主侧建立文件系统快照,再从快照读取。这通常提供的是崩溃一致性:就像在某一时刻断电,镜像中的日志型文件系统可能需要回放日志才能重新挂载。它不自动保证每个文件的业务内容都一致,因为大多数日志文件系统并不对所有文件数据做日志记录。

原文提到 ext4 的 data=journal 模式、ZFS,以及未使用 nodatacow 的 btrfs,在文件内容处理上有不同保证。但具体可恢复性仍取决于整个存储栈和应用;不能把文件系统名称当作完整的应用一致性证明。

PostgreSQL、SQLite、Borg 仓库等按崩溃恢复设计的应用,通常能够从满足其前提的崩溃一致快照恢复到一致状态。其他应用可能需要停止写入、刷新缓冲、协调事务或执行专用备份流程。多个磁盘或多个卷还需要协调同一时点,逐个独立快照不能自然保证跨盘事务一致。

能够捕获大部分虚拟机状态的虚拟化平台快照,也可以作为备份源。视应用而定,它可能比纯宿主文件系统快照更容易保留需要的状态。但应确认快照覆盖的内存、磁盘及外部依赖范围,而不是假定所有状态都被捕获。

Borg 官方明确把这些与平台、应用深度相关的问题留给对应机制处理。实际方案通常是把 Borg 的去重归档与平台快照、来宾静默或应用备份功能组合起来,再用隔离恢复演练验证目标。本文完成的是源文与代码静态审查,没有提供恢复成功、数据完整或无漏洞的实测结论。

完整许可声明

Copyright (C) 2015-2026 The Borg Collective (see AUTHORS file)
Copyright (C) 2010-2014 Jonas Borgström <jonas@borgstrom.se>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in
    the documentation and/or other materials provided with the
    distribution.
 3. The name of the author may not be used to endorse or promote
    products derived from this software without specific prior
    written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容