PostgreSQL 的“每个关系一个文件”:为什么勒索恢复会变得棘手

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

预计阅读时间:11 分钟

一次勒索软件恢复案例把一个平时很少被业务开发者关注的 PostgreSQL 存储细节推到台前:PostgreSQL 通常把每张表、每个索引等 relation 存成独立文件。这个设计让定位、截断、清理、备份某些对象更直观,但当系统目录被破坏、部分文件被加密或删除时,恢复人员可能会发现:数据文件还在,数据库却“不认识”它们了。

这篇文章从工程视角拆开这个问题:为什么 PostgreSQL 的单文件 relation 存储会让 catalog 恢复痛苦,它和 MySQL、Oracle 的暴露面有什么不同,以及你可以怎样提前做恢复演练。

问题不只是“文件丢了”,而是“目录失忆了”

PostgreSQL 的表、索引、TOAST 表等对象在磁盘上对应 relation 文件。文件名通常和 relfilenode 有关,而对象含义则记录在系统目录中,例如:

  • pg_class:relation 的元信息,包含表、索引、TOAST 表等;
  • pg_database:数据库到目录 OID 的映射;
  • pg_tablespace:表空间映射;
  • 依赖关系、命名空间、类型信息等还散落在其他系统 catalog 中。

也就是说,磁盘上看到一个文件,并不等于你知道它属于哪张业务表、哪一个索引、哪一个数据库 schema。正常情况下,PostgreSQL 通过 catalog 把逻辑对象和物理文件连起来。一旦 catalog 文件被勒索软件加密、覆盖,或者恢复时不一致,恢复就会从“拷贝文件”变成“考古”。

一个典型痛点是:业务表的数据文件可能仍然存在,但缺少可信的 catalog 映射后,无法安全地把它们重新挂回原数据库。你可能能猜出某些大文件是核心表,却很难保证字段布局、TOAST 关系、索引状态、可见性信息都一致。

和 MySQL、Oracle 的暴露面差异

这里不能简单说谁“更安全”。不同数据库的文件布局、数据字典设计和恢复工具链不同,风险形态也不同。

PostgreSQL 的特点是大量 relation 文件直接散落在数据库目录、表空间目录下。优势是对象边界清楚,很多运维操作和诊断可以落到单个 relation 文件;代价是 catalog 一旦不可用,文件的业务语义恢复难度显著增加。

MySQL,尤其是 InnoDB,常见形态包括共享表空间、独立表空间 .ibd 文件、redo/undo、数据字典等组合。它也有“文件还在但字典不认”的问题,例如独立表空间导入需要元数据匹配。但由于不同版本和配置差异很大,恢复路径通常围绕 InnoDB 字典、表结构、表空间导入工具链展开。

Oracle 更强调数据文件、控制文件、redo、归档日志和数据字典之间的整体恢复链路。企业现场常借助 RMAN、归档日志和控制文件备份完成时间点恢复。它并非没有 catalog/data dictionary 风险,但工具链通常围绕数据库整体一致性来设计。

勒索恢复时,真正的分界不是“单文件还是多文件”这么粗,而是:

  • 是否有离线、不可变、可验证的备份;
  • catalog 或 data dictionary 是否能和数据文件保持一致;
  • 是否有 WAL/redo/归档日志支撑时间点恢复;
  • 团队是否演练过“只剩部分文件”的坏场景。

可以这样实践:看懂 PostgreSQL relation 文件映射

下面这个小实验可以在本地测试库运行,用来理解 PostgreSQL 如何把表映射到物理文件。不要在生产库直接执行实验命令;如果要观察生产库,只运行只读查询。

准备一个临时数据库:

createdb relfile_demo
psql relfile_demo <<'SQL'
CREATE TABLE orders_demo (
  id bigserial PRIMARY KEY,
  customer_id bigint NOT NULL,
  amount numeric(12,2) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO orders_demo (customer_id, amount)
SELECT g, (random() * 1000)::numeric(12,2)
FROM generate_series(1, 10000) AS g;

CREATE INDEX idx_orders_demo_customer_id ON orders_demo(customer_id);
SQL

查看逻辑对象到物理文件的映射:

psql relfile_demo <<'SQL'
SELECT
  c.oid,
  n.nspname AS schema_name,
  c.relname,
  c.relkind,
  c.relfilenode,
  pg_relation_filepath(c.oid) AS file_path,
  pg_size_pretty(pg_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname IN ('orders_demo', 'orders_demo_pkey', 'idx_orders_demo_customer_id')
ORDER BY c.relname;
SQL

你会看到类似 base/数据库OID/relfilenode 的路径。继续在 shell 里对照实际文件:

DATA_DIR=$(psql -At relfile_demo -c "SHOW data_directory;")
psql -At relfile_demo -c "SELECT pg_relation_filepath('orders_demo'::regclass);" | while read -r path; do
  echo "Data directory: $DATA_DIR"
  echo "Relation file:  $DATA_DIR/$path"
  ls -lh "$DATA_DIR/$path"*
done

注意几个细节:

  • 大 relation 会分段,可能出现 .1.2 这样的文件;
  • 可见性映射、空闲空间映射可能以 _vm_fsm 结尾;
  • TOAST 表会有自己的 relation 文件;
  • VACUUM FULLCLUSTER、某些重写操作可能改变 relfilenode

这就是勒索恢复时痛苦的来源之一:你看到的不只是一个表文件,而是一组需要 catalog 正确解释的物理碎片。

恢复策略:不要把希望押在“手工拼文件”上

如果 PostgreSQL 集群被勒索软件破坏,优先级通常应该是保护现场,而不是立刻启动数据库反复尝试。

可以采用这样的处置顺序:

# 1. 立刻停止实例,避免进一步写入扩大损坏面
sudo systemctl stop postgresql

# 2. 对数据目录做只读快照或块级拷贝;下面只是示意,设备名需要按现场修改
sudo rsync -aHAX --numeric-ids /var/lib/postgresql/ /mnt/forensic-copy/postgresql/

# 3. 记录关键环境信息
postgres --version || true
pg_controldata /var/lib/postgresql/*/main 2>/tmp/pg_controldata.txt || true

# 4. 在隔离环境中分析副本,不要直接操作原始数据目录

更重要的是事前准备。对 PostgreSQL 来说,推荐至少具备下面几类能力:

  • 物理基础备份:例如 pg_basebackup 或成熟备份工具;
  • WAL 归档:支撑时间点恢复,避免只能恢复到上一次全量备份;
  • 离线或不可变备份:备份服务器不要和数据库服务器共享同一套可写凭据;
  • 定期恢复演练:备份能恢复,才叫备份;
  • catalog 一致性意识:不要只备份表文件,也不要以为拷贝 base/ 目录就等于可恢复。

一个最小化的物理备份示例:

# 在备份机或隔离目录执行;按你的环境修改主机、用户和目录
export PGPASSWORD='change-me'
pg_basebackup \
  -h 10.0.0.12 \
  -U repl \
  -D /backup/postgres/base_$(date +%F) \
  -Fp \
  -Xs \
  -P \
  -R

同时在 PostgreSQL 配置中启用 WAL 归档,示例配置如下,路径和命令需要按环境调整:

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backup/postgres/wal/%f && cp %p /backup/postgres/wal/%f'

这段配置的重点不是照抄路径,而是确保 WAL 被持续、安全地复制到数据库主机之外的位置。更成熟的环境应使用专门备份工具、对象存储不可变策略、权限隔离和告警。

落地检查清单

如果你维护 PostgreSQL,建议把下面几项加入灾备检查:

  • 能否在不接触生产库的情况下,从最近一次备份恢复出完整实例?
  • 是否能恢复到某个指定时间点,而不只是恢复到昨晚?
  • 备份账号是否无法删除历史备份?
  • WAL 归档失败是否会触发告警?
  • 是否记录了 PostgreSQL 大版本、扩展版本、表空间路径、关键配置?
  • 是否演练过 catalog 损坏、表空间缺失、部分 relation 文件损坏这些非理想场景?

PostgreSQL 的单 relation 文件设计本身不是缺陷,它是一个明确的工程取舍。但勒索软件恢复会放大这个取舍的阴影:文件还在不等于数据库可恢复,catalog 一致性和 WAL 链路同样关键。真正可靠的方案不是事后手工拼接文件,而是事前构建可验证、可隔离、可演练的恢复体系。


相关推荐