4. 表空间与文件系统容量映射
在 PostgreSQL 中,数据库对象最终都会落到操作系统的某个目录下,而表空间正是连接“逻辑对象”和“物理文件系统”的桥梁。容量治理如果只看 pg_database_size() 或 pg_total_relation_size(),往往无法回答“磁盘为什么满了”“哪个挂载点最先告急”“扩容该加在哪块盘”这类问题。要回答这些问题,必须把表空间布局、数据目录结构、文件系统使用率和具体数据库对象四者关联起来。
一、表空间与数据目录的物理布局
PostgreSQL 初始化后,数据目录(PGDATA,由 initdb 创建)内部有清晰的物理结构。核心目录包括:base/ 存放默认表空间 pg_default 中各个数据库的文件,子目录名即数据库 OID;global/ 存放共享系统目录;pg_tblspc/ 存放自定义表空间的符号链接,链接名是表空间 OID,指向真正的文件系统路径;pg_wal/ 存放 WAL 日志;pg_xact/、pg_multixact/ 等存放事务相关文件。
自定义表空间创建时,PostgreSQL 会在指定路径下建立 PG_版本号_目录版本号/ 子目录,例如 PG_16_202307071/,然后在 pg_tblspc/ 下创建同名 OID 的软链接指向它。因此,一个表空间在文件系统上对应一个真实目录,在 PGDATA 中对应一个软链接,在系统目录 pg_tablespace 中对应一行记录。三者通过 OID 一一对应,这是做容量映射的关键。
二、用 SQL 建立对象与表空间的关联
要定位“哪个表空间、哪个对象占了多少空间”,可以借助系统目录视图。下面这条查询把数据库、表空间、对象名、对象类型和大小关联起来,按表空间分组排序:
SELECT
t.spcname AS tablespace,
n.nspname AS schema,
c.relname AS object_name,
CASE c.relkind
WHEN 'r' THEN 'table'
WHEN 'i' THEN 'index'
WHEN 't' THEN 'toast'
WHEN 'm' THEN 'matview'
ELSE c.relkind::text
END AS obj_type,
pg_size_pretty(pg_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.relkind IN ('r','i','t','m')
ORDER BY pg_relation_size(c.oid) DESC
LIMIT 20;
注意 pg_class.reltablespace = 0 表示对象位于数据库默认表空间(通常是 pg_default),因此上面的 JOIN 会漏掉这类对象。更完整的写法应使用 COALESCE(NULLIF(c.reltablespace,0), d.dattablespace) 回退到数据库默认表空间。此外,pg_relation_size() 只统计主文件,索引、TOAST 需要分别统计,或用 pg_total_relation_size() 汇总。
三、把表空间映射到文件系统使用率
表空间本身不携带容量信息,容量来自它所在的挂载点。因此需要先查出每个表空间的物理路径,再与 df 输出对照。查询表空间路径:
SELECT spcname, oid,
pg_tablespace_location(oid) AS location
FROM pg_tablespace;
对于 pg_default 和 pg_global,pg_tablespace_location() 返回空字符串,它们实际位于 PGDATA 下的 base/ 和 global/。自定义表空间则返回真实路径。拿到路径后,在操作系统侧执行:
df -h /data/pgdata /data/ts_hot /data/ts_archive /data/wal
把 df 的 Use% 与表空间对应起来,就能得到“表空间—挂载点—使用率”的映射表。例如 ts_archive 位于 /data/ts_archive,该挂载点已用 91%,而 ts_hot 仅 62%,说明归档表空间是当前容量风险点。
四、关联分析:从使用率反查对象
当某个挂载点使用率偏高时,需要进一步定位是哪些对象贡献了增长。可以按表空间汇总对象大小:
SELECT t.spcname,
pg_size_pretty(SUM(pg_total_relation_size(c.oid))) AS total_size,
COUNT(*) AS obj_count
FROM pg_class c
JOIN pg_tablespace t
ON t.oid = COALESCE(NULLIF(c.reltablespace,0),
(SELECT dattablespace FROM pg_database
WHERE datname = current_database()))
WHERE c.relkind IN ('r','i','m')
GROUP BY t.spcname
ORDER BY SUM(pg_total_relation_size(c.oid)) DESC;
再结合 pg_stat_user_tables 的 n_dead_tup、n_live_tup 以及 pg_statio_user_tables 的读取情况,可以判断增长是来自业务写入、索引膨胀还是死元组堆积。例如某表 pg_total_relation_size 为 120GB,其中索引占 45GB,而 n_dead_tup 高达 3000 万,说明该表既需要 VACUUM 回收,也可能需要评估索引是否冗余。
五、容量映射的实践要点
- OID 是纽带:数据库 OID、表空间 OID、relfilenode 共同决定文件路径,排查时先取 OID 再拼路径,避免凭目录名猜测。
- 软链接要跟到底:
pg_tblspc/下是软链接,用ls -l或readlink -f解析真实路径,否则du统计会失真。 - 区分逻辑大小与物理占用:
pg_relation_size是逻辑大小,文件系统占用还受块大小、稀疏文件、WAL 和临时文件影响,两者不必相等。 - 预留安全水位:单挂载点使用率建议不超过 80%,WAL 所在盘不超过 70%,为
VACUUM FULL、pg_basebackup等操作留出临时空间。 - 表空间不是备份:表空间只改变存储位置,不提供冗余。容量治理仍需配合备份、归档和监控。
通过“对象 → 表空间 → 软链接 → 挂载点 → 使用率”这条完整链路,容量问题就能从模糊的“磁盘快满了”落到具体的表空间、具体的对象和具体的增长原因上,为后续的扩容、迁移和清理提供明确依据。