经典的三二一备份规则要求三份数据,分布在两类存储介质上,其中一份位于异地。正在使用的数据本身算一份,因此还需要两份备份。规则的作用是开始拆分共同故障影响,并不保证任意三个文件名都能抵抗硬件故障、误删、管理凭据被盗,或者被同步传播到所有副本的错误。

坏掉的样子

副本之间共享的可能不仅是内容。两个目录可能在同一块盘上,两个虚拟卷可能依赖同一个存储阵列,若干远端备份也可能都能被已经失陷的生产管理员删除。因此,设备、地理位置和访问权限边界需要分别检查。异地降低了本地灾害的共同影响,却不会自动使数据变成离线或不可修改状态。

硬链接是“只数名字”的直接反例。它给同一个底层文件增加目录入口,原地修改文件时,两个名字所看到的内容都会变化。单独复制出来的文件可以抵抗这一种修改,保留较早字节,但仍然可能与原文件共享设备和管理权限风险。因此,“不是同一个文件”和“具备独立灾难恢复能力”之间还有很大距离。

同步也可能造成误判。一个迅速传播删除的镜像,会把恢复所需的旧版本一起移除。保留时间与版本选择,和目标数量一样重要。校验和可以发现内容与可信预期值不符,但若攻击者可以同时替换文件和校验值,同一权限范围内保存的校验和本身并不能证明没有发生恶意替换。

快速验证

把以下内容保存为 copies_lab.py,在支持硬链接的文件系统上用普通 Python 运行,并保持断言开启。脚本只使用标准库和临时文件。两个目录名模拟位置,实际上仍在同一个本地文件系统中。这是一次修改隔离与恢复演练,不是这台机器已经拥有真正异地备份介质的证明。

import hashlib
import json
import os
import shutil
import tempfile
from pathlib import Path

def digest(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

with tempfile.TemporaryDirectory(prefix="xbcatm-copies-") as directory:
    root = Path(directory)
    primary_site = root / "site-a"
    other_site = root / "site-b-simulation"
    primary_site.mkdir()
    other_site.mkdir()
    primary = primary_site / "account.json"
    primary.write_text('{"account":"example","balance_cents":2500}\n')
    expected = digest(primary)
    alias = primary_site / "hardlink.json"
    os.link(primary, alias)
    independent = other_site / "snapshot.json"
    shutil.copy2(primary, independent)
    assert os.path.samefile(primary, alias)
    assert not os.path.samefile(primary, independent)
    primary.write_text('{"account":"example","balance_cents":0}\n')
    assert digest(alias) != expected
    assert digest(independent) == expected
    print("hardlink_changed_with_primary")
    print("independent_copy_kept_original_bytes")
    primary_site.rename(root / "site-a-unavailable")
    assert not primary.exists()
    restored = root / "restored.json"
    shutil.copy2(independent, restored)
    assert digest(restored) == expected
    assert json.loads(restored.read_text()) == {"account": "example", "balance_cents": 2500}
    print("restored_without_original_path", 2500)
    assert independent.stat().st_dev == restored.stat().st_dev
    print("same_filesystem_lab_not_real_offsite_protection")

夹具最初保存一个余额为两千五百分的账户。随后创建硬链接名字和单独复制的快照,并在修改源文件之前记录预期摘要。文件身份检查明确哪一个路径仍指向同一文件。一次原地写入把源文件改为零,硬链接看到的值也随之变化;独立复制的文件则继续保持预期字节。

前两个输出标记确认了这种差别,证明增加第二个文件名没有提供抵抗本次修改的另一份可恢复版本。这不表示文件系统快照等同于硬链接;快照实现有自己的写时复制与保留语义,不能因为都表现成一个可访问路径,就把它们当作同一种存储机制。这里验证的是代码明确创建的硬链接与普通副本。

接着,脚本重命名模拟主站点目录,使原始路径不可用。恢复过程只读取独立快照,写入新文件,检查摘要,并解析业务数值。恢复金额必须仍为两千五百分。最后一个标记再次确认实验仍在同一文件系统中。完成后临时文件会清理,不修改真实备份保留策略,也不把路径改名当成物理机房真的发生故障。

把规则落实为恢复设计

盘点工作副本和两份备份时,应记录设备或服务、实际位置、谁有删除权限、保留哪些版本,以及恢复依赖什么。传统的两类介质用于降低共同设备和格式故障;现代部署仍然需要有意义的独立性,单纯给云资源或文件夹起不同名字并不能建立这种独立性。应说明每一条分隔究竟准备抵抗哪一种单点故障。

根据实际威胁模型,保留合适的离线副本或受到修改保护的副本。通过加密保护机密性,同时安排故障时仍能取得必要密钥的恢复路径,避免全部依赖已经损坏的生产环境。还要验证值班人员在真正的事件条件下,能够使用当时可获得的权限和工具取回正确版本,而不是仅在平时管理员全部在线时才恢复得出来。

从每个准备承担恢复职责的来源实际演练,记录业务状态、数据年龄以及恢复到可用状态所需的时间。较新的本地副本可以提供快速恢复,较旧的远端副本可能覆盖更大的故障,但两者承诺的数据恢复位置不同。三二一最终应形成一组具备已知边界、可以独立使用的恢复路径,并由演练支持,而不是只形成一张文件数量已经达标的表格。

延伸阅读

来源