pg_hardstorage 为什么故意不要增量备份链

2026-07-06 41 预计阅读时间: 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.

预计阅读时间:9 分钟

在 PostgreSQL 备份工具里,“全量 + 一串增量”几乎是默认想象:上一次备份是下一次备份的父节点,恢复时沿着链条一路重放。pg_hardstorage 选择了另一条路:它没有增量链,而且这是设计目标,不是缺失功能。核心取舍很直接:用更多源端读取 I/O,换一个不容易在凌晨恢复时突然断掉的仓库格式。

增量链的问题不在日常,而在恢复现场

链式增量格式看起来很省:

full A <- incr B <- incr C <- incr D <- diff E

只要每个节点都在,恢复路径清晰,存储成本也低。但它有一个危险性质:链上任何一个备份坏掉、丢失、被过早清理,都会影响它后面的所有备份。

真实事故通常不是工具当场报错,而是静悄悄埋雷:

  • S3 lifecycle policy 误删了 incr Bincr Cincr Ddiff E 都失去意义。
  • 后端存储出现 bit flip,某个差异备份损坏,只能恢复到它之前。
  • 运维执行 backup expire 时保留策略过激,基础全量被删,整条链悬空。
  • 基础全量的 manifest schema 在版本升级中变化,迁移脚本又有边界 bug。

这些问题单独看都能处理,真正麻烦的是:很多团队直到恢复时才发现链断了。备份系统最怕的不是“平时看起来不够漂亮”,而是“恢复时才暴露依赖关系”。

pg_hardstorage 用内容寻址替代父子引用

pg_hardstorage 的 manifest 不引用“上一个 manifest”,而是引用内容块的 SHA-256。形式大致像这样:

{
  "id": "prod-2026-05-02-0334",
  "chunks": [
    "7e1f2a...ab",
    "9c4d3b...12",
    "2faabc...77"
  ],
  "chunks_added": 1288490188
}

每个 chunk 的文件名就是它的 hash。两个备份共享相同数据时,它们共享的是存储池里的同一个 chunk,而不是通过“父备份指针”继承数据。

这带来一个非常实际的删除模型:删除较老备份时,较新备份需要的 chunk 不会因为父节点被删而消失。只有当垃圾回收确认某个 chunk 没有任何 manifest 引用时,它才应该被清理。

这个思路并不新,restic、kopia、borgbackup 都采用类似的内容寻址与去重模型。但在 PostgreSQL 生态里,链式增量太常见,所以 pg_hardstorage 的选择值得单独说明:它把“备份之间的顺序依赖”降到了 chunk 引用集合,而不是一条易碎的链。

存储省了,源端 I/O 还要付账

链式增量的两个优势通常是:

  • 数据没变时,不重复写相同字节。
  • 数据大部分没变时,不必每次完整读取数据目录。

内容寻址天然解决第一个问题:相同内容的 chunk hash 一样,只写一次。pg_hardstorage manifest 里的 chunks_added 可以直接告诉你这次备份实际向 chunk pool 写入了多少数据。

第二个问题更难。根据来源摘要,pg_hardstorage 当前每次备份都会读取完整 PostgreSQL data dir,然后再和 chunk pool 做去重。对 100 TB 级别、底层存储较慢的数据库来说,这不是免费午餐。项目正在做 snapshot-aware fast path,希望在可用时利用文件系统级 COW snapshot 降低读取成本,但该能力尚未发布。

所以这里的取舍要说清楚:pg_hardstorage 不是魔法般同时消除链依赖和源端扫描成本。它当前更偏向恢复可靠性、仓库可检查性和删除语义清晰度。

来源摘要里给了一组生产级 fleet 的数据:487 GiB 数据规模,按小时备份一周,结果是:

chunks_total:   1,247,883
chunks_unique:    156,423 (12.5%)
dedup_ratio:       87.5%
total_storage:  12.4 TiB (vs raw 89.2 TiB)
chunks_added:   1,200 ± 400 per backup

这说明在该样本里,内容寻址模型的存储效率可以接近链式增量,同时移除了链式依赖。

可以这样实践:用 jq 检查 manifest 的引用和新增量

下面是一个最小可运行示例,用来模拟 pg_hardstorage 这类 manifest 的可检查性。它不调用真实 pg_hardstorage,只演示“manifest 引用 chunk,而不是引用父备份”的仓库思路。

运行前需要本机安装 jq

mkdir -p /tmp/pg-hardstorage-demo/manifests /tmp/pg-hardstorage-demo/chunks
cd /tmp/pg-hardstorage-demo

cat > manifests/backup-001.json <<'JSON'
{
  "id": "backup-001",
  "chunks": ["aaa111", "bbb222", "ccc333"],
  "chunks_added": 300
}
JSON

cat > manifests/backup-002.json <<'JSON'
{
  "id": "backup-002",
  "chunks": ["bbb222", "ccc333", "ddd444"],
  "chunks_added": 100
}
JSON

touch chunks/aaa111 chunks/bbb222 chunks/ccc333 chunks/ddd444

printf "Backup IDs and bytes added:\n"
jq -r '"\(.id) added=\(.chunks_added)"' manifests/*.json

printf "\nChunk reference counts:\n"
jq -r '.chunks[]' manifests/*.json | sort | uniq -c | sort -nr

printf "\nChunks needed by backup-002:\n"
jq -r '.chunks[]' manifests/backup-002.json

你会看到 backup-002 需要 bbb222ccc333ddd444,它并不需要知道 backup-001 是否存在。即使删除 backup-001.json,只要这些 chunk 还在,backup-002 的引用关系依然完整。

可以继续模拟一次安全删除检查:

cd /tmp/pg-hardstorage-demo
rm manifests/backup-001.json

printf "Remaining manifest references:\n"
jq -r '.chunks[]' manifests/*.json | sort | uniq -c

printf "\nUnreferenced chunks that GC may remove:\n"
comm -23 \
  <(find chunks -type f -printf '%f\n' | sort) \
  <(jq -r '.chunks[]' manifests/*.json | sort -u)

输出里的 aaa111 就是没有任何 manifest 引用的 chunk,垃圾回收可以考虑清理它;而 bbb222ccc333 仍被 backup-002 使用,不能因为旧备份被删就删除。

采用前的判断清单

pg_hardstorage 的设计适合重视恢复确定性、希望仓库格式可审计、并且不想让中间某次备份成为单点风险的团队。manifest 能用 jq 这类普通工具查看,也让排障更接近“检查数据结构”,而不是猜一条隐含链是否还完整。

但它也有边界:当前每次备份读取完整 PG data dir,对超大库、慢存储、备份窗口紧张的系统,需要认真压测源端 I/O。等 snapshot-aware fast path 发布后,这个取舍可能会变化,但在那之前,不能只看去重率,还要看备份扫描对生产负载的影响。

一个务实的落地方式是:先在非核心库或只读副本上跑一周,记录 chunks_added、总存储增长、备份耗时、源端 I/O 峰值和恢复演练结果。备份格式的好坏不该只看写入时有多省,更要看删除、升级、损坏和凌晨恢复时是否还能讲得清楚。


相关推荐