沿着 pg_hardstorage 看懂 PostgreSQL 备份如何走向恢复

2026-07-02 25 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

对 PostgreSQL 管理员、DBA、SRE 和平台团队来说,备份“成功完成”只是第一层答案。真正需要在故障前搞清楚的是:base backup 去了哪里,WAL 如何保存,仓库里哪些文件负责描述数据,校验失败时能不能定位,恢复时又如何把这些碎片重新拼成一个可启动的数据库。pg_hardstorage 的 Storage and Recovery Guide 有价值的地方,正在于它不是只列命令,而是沿着一次备份从 PostgreSQL 到恢复的生命周期,把仓库结构和恢复逻辑串起来看。

不只看备份结果,要看数据路径

很多备份系统的问题,不是在备份当天暴露,而是在恢复当天爆炸。常见追问包括:

  • 这份备份依赖哪些 WAL?
  • 仓库里的对象是否可以被独立校验?
  • 去重后的 chunk 如何知道属于哪个备份?
  • manifest 损坏、对象缺失或垃圾回收误删时,系统能否发现?
  • 恢复流程到底是读元数据、拉 chunk、重组文件,还是直接复制目录?

pg_hardstorage 的文档思路是“跟随数据移动”。从 PostgreSQL 产生 base backup 和 WAL 开始,继续看数据如何进入 repository,再看 chunk、manifest、metadata 怎样组织,最后看恢复时如何利用这些组件重建数据库目录。

这对平台团队尤其重要:如果你要评审备份架构,不能只问“有没有压缩、有没有加密、有没有定时任务”,还要问“仓库是否可解释、可校验、可恢复”。

仓库设计的核心:内容寻址、切块和 manifest

从摘要看,pg_hardstorage 的仓库设计重点包括 content-addressed storage、chunking、deduplication、manifest、metadata、integrity verification、corruption handling、crash safety 和 garbage collection。这些词单独看都熟,但放在 PostgreSQL 备份场景里,关键是它们如何协作。

可以把一次备份粗略理解为三层:

  1. 数据对象层:真实文件内容被切成 chunk,chunk 可以按内容哈希寻址。相同内容不必重复存储,这就是去重的基础。
  2. 描述层:manifest 记录一次备份包含哪些文件、文件由哪些 chunk 组成、大小和校验信息是什么。
  3. 仓库治理层:metadata 记录备份集合、WAL 范围、引用关系、垃圾回收边界,以及用于完整性检查和恢复选择的信息。

这种设计的好处是:恢复时不必相信“目录看起来还在”,而是可以通过 manifest 和校验信息确认“这份备份所需对象确实存在且内容匹配”。代价是实现复杂度更高:写入必须考虑 crash safety,删除必须考虑引用关系,校验必须能区分元数据问题、chunk 缺失和内容损坏。

可以这样实践:用一个迷你仓库理解 chunk 与 manifest

下面这个 Python 示例不是 pg_hardstorage 的真实实现,而是一个可运行的极简模型,用来帮助团队理解“内容寻址 chunk + manifest + restore”的关系。你可以把它当成设计评审或内部培训的小实验。

运行前准备一个目录,例如 demo_pgdata,里面放几个文本文件即可。

mkdir -p demo_pgdata/base demo_pgdata/global
printf 'users table page A\n' > demo_pgdata/base/12345
printf 'users table page A\n' > demo_pgdata/base/12346
printf 'cluster metadata\n' > demo_pgdata/global/pg_control.txt

保存下面脚本为 mini_hardstorage.py

#!/usr/bin/env python3
import hashlib
import json
import os
import shutil
import sys
from pathlib import Path

CHUNK_SIZE = 8


def sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def backup(source: Path, repo: Path, backup_name: str) -> None:
    objects = repo / "objects"
    manifests = repo / "manifests"
    objects.mkdir(parents=True, exist_ok=True)
    manifests.mkdir(parents=True, exist_ok=True)

    manifest = {"backup": backup_name, "files": []}

    for file_path in sorted(p for p in source.rglob("*") if p.is_file()):
        rel_path = file_path.relative_to(source).as_posix()
        chunks = []
        with file_path.open("rb") as f:
            while True:
                data = f.read(CHUNK_SIZE)
                if not data:
                    break
                digest = sha256(data)
                object_path = objects / digest
                if not object_path.exists():
                    object_path.write_bytes(data)
                chunks.append({"sha256": digest, "size": len(data)})
        manifest["files"].append({"path": rel_path, "chunks": chunks})

    manifest_path = manifests / f"{backup_name}.json"
    manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    print(f"backup written: {manifest_path}")


def verify(repo: Path, backup_name: str) -> None:
    manifest = json.loads((repo / "manifests" / f"{backup_name}.json").read_text())
    for item in manifest["files"]:
        for chunk in item["chunks"]:
            object_path = repo / "objects" / chunk["sha256"]
            if not object_path.exists():
                raise SystemExit(f"missing chunk: {chunk['sha256']}")
            data = object_path.read_bytes()
            if sha256(data) != chunk["sha256"]:
                raise SystemExit(f"corrupt chunk: {chunk['sha256']}")
    print("verify ok")


def restore(repo: Path, backup_name: str, target: Path) -> None:
    if target.exists():
        shutil.rmtree(target)
    target.mkdir(parents=True)

    manifest = json.loads((repo / "manifests" / f"{backup_name}.json").read_text())
    for item in manifest["files"]:
        output_path = target / item["path"]
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with output_path.open("wb") as out:
            for chunk in item["chunks"]:
                data = (repo / "objects" / chunk["sha256"]).read_bytes()
                out.write(data)
    print(f"restored to: {target}")


if __name__ == "__main__":
    command = sys.argv[1]
    if command == "backup":
        backup(Path(sys.argv[2]), Path(sys.argv[3]), sys.argv[4])
    elif command == "verify":
        verify(Path(sys.argv[2]), sys.argv[3])
    elif command == "restore":
        restore(Path(sys.argv[2]), sys.argv[3], Path(sys.argv[4]))
    else:
        raise SystemExit("usage: backup|verify|restore ...")

执行一次备份、校验和恢复:

python3 mini_hardstorage.py backup demo_pgdata demo_repo backup_001
python3 mini_hardstorage.py verify demo_repo backup_001
python3 mini_hardstorage.py restore demo_repo backup_001 restored_pgdata
find demo_repo -maxdepth 2 -type f | sort
find restored_pgdata -type f -print -exec cat {} \;

你会看到两个内容相同的文件只需要存储同一个 chunk 对象。真实的 pg_hardstorage 会处理更复杂的问题,例如 PostgreSQL base backup、WAL 生命周期、元数据一致性、崩溃安全、垃圾回收和恢复流程;但这个小模型能帮助你抓住设计骨架:对象存储内容,manifest 描述对象如何组成文件,恢复流程按 manifest 重建目录。

评审备份系统时要问的几个问题

如果你正在评估 pg_hardstorage 或类似的 PostgreSQL 备份仓库设计,可以把讨论从“能不能备份”推进到这些更具体的问题:

  • WAL 边界:一次 base backup 对应的 WAL 范围如何记录?恢复到一致状态需要哪些 WAL?
  • 完整性验证:校验是只检查文件存在,还是会验证内容哈希和 manifest 引用?
  • 损坏处理:chunk 缺失、manifest 损坏、metadata 不一致时,系统如何报告?能否定位影响哪些备份?
  • 崩溃安全:备份写入中断时,仓库会留下半成品吗?半成品是否会污染后续恢复?
  • 垃圾回收:删除旧备份时,如何确认没有仍被其他备份引用的 chunk 被清理?
  • 恢复演练:是否定期从仓库恢复到隔离环境,并验证 PostgreSQL 能启动、能回放 WAL、能通过业务级检查?

这些问题看起来偏底层,但它们决定了恢复当天你是在“执行流程”,还是在“猜仓库里哪些东西还能用”。

落地建议:把恢复路径纳入日常运维

pg_hardstorage 这类项目提醒我们:备份系统不只是定时任务,而是一条从 PostgreSQL 数据页、WAL、仓库对象、manifest、metadata 到恢复目录的链路。链路上每一段都需要可观察、可校验、可解释。

采用时可以按小步推进:先读懂仓库布局和恢复流程,再做小规模测试;先验证 base backup 与 WAL 的组合恢复,再引入保留策略和垃圾回收;先做人工演练,再把校验和恢复演练纳入 CI、运维巡检或灾备演习。真正可靠的备份策略,不是“备份成功率 100%”,而是“在可预期的时间内,用可解释的数据恢复出可用数据库”。


相关推荐