RDS for PostgreSQL 表膨胀排查与治理:从死元组统计到 pg_repack 在线回收

举报
yd_229207802 发表于 2026/10/25 09:52:14 2026/10/25
【摘要】 在 RDS for PostgreSQL 上跑业务一段时间后,经常会遇到这样的现象:表里的行数没怎么变,磁盘占用却一路上涨,简单的按主键查询也越来越慢。绝大多数情况下,罪魁祸首是"表膨胀"——MVCC 留下的死元组没有被及时清理。本文按照"确认膨胀、找到原因、调参治理、在线回收"四步,把我在实际项目里的排查过程整理出来,所有 SQL 都可以直接在 psql 或数据管理服务 DAS 中执行。一...

在 RDS for PostgreSQL 上跑业务一段时间后,经常会遇到这样的现象:表里的行数没怎么变,磁盘占用却一路上涨,简单的按主键查询也越来越慢。绝大多数情况下,罪魁祸首是"表膨胀"——MVCC 留下的死元组没有被及时清理。本文按照"确认膨胀、找到原因、调参治理、在线回收"四步,把我在实际项目里的排查过程整理出来,所有 SQL 都可以直接在 psql 或数据管理服务 DAS 中执行。

一、先弄清楚死元组从哪里来

PostgreSQL 的 UPDATE 并不是原地修改,而是写入一个新版本、把旧版本标记为过期;DELETE 也只是打标记。这些过期版本就是死元组(dead tuple),要等 VACUUM 把它们占用的空间标记为可复用。autovacuum 进程会按阈值自动触发清理,触发条件大致是:

死元组数 > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * 表行数

默认 scale_factor 是 0.2,也就是说一张 5000 万行的大表,要累积约 1000 万个死元组才会触发一次清理。对于高频更新的订单表、状态表来说,这个阈值明显偏高。

二、第一步:确认哪些表在膨胀

先从统计视图里找死元组最多的表:

SELECT relname,
       n_live_tup,
       n_dead_tup,
       round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
       last_autovacuum,
       pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

经验上,dead_pct 长期超过 20%,或者 last_autovacuum 已经是几天前,就值得重点关注。如果实例安装了 pgstattuple 插件(RDS for PostgreSQL 支持通过 CREATE EXTENSION 启用),可以得到更精确的数字:

CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple_approx('public.orders');

重点看 dead_tuple_percent 和 approx_free_percent,前者是待清理的死元组,后者是已清理但未归还给操作系统的空闲空间。

三、第二步:找出 autovacuum 清理不动的原因

阈值偏高只是其一,更常见也更隐蔽的是"清理了但删不掉"。VACUUM 只能回收比当前最老事务快照还旧的死元组,所以下面三类情况会直接让清理失效:

1. 长事务或 idle in transaction 连接。应用拿到连接后开启事务却迟迟不提交,会把整个实例的可回收边界钉住:

SELECT pid, usename, state, xact_start,
       now() - xact_start AS xact_age,
       left(query, 60) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 5;

2. 废弃的复制槽。做过逻辑订阅或 DRS 迁移后,残留的复制槽会持有 catalog_xmin:

SELECT slot_name, active, xmin, catalog_xmin,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;

3. 只读实例开启了 hot_standby_feedback 且有慢查询。只读节点上的长查询会把反馈的 xmin 回传给主库,同样阻止回收。

我遇到的那次问题就是第一类:一个定时任务框架在每次任务开始时执行 BEGIN,异常分支没有回滚,连接一直保持 idle in transaction 长达 30 多个小时,期间订单表死元组涨到了 4000 多万。

四、第三步:调整参数,让清理跟得上

先解决根因:修复应用里未提交的事务,并在 RDS 控制台的参数模板中设置兜底超时,避免同类问题再次出现:

idle_in_transaction_session_timeout = 600000   -- 10 分钟,单位毫秒
statement_timeout 按业务在会话级别设置

无用的复制槽确认不再需要后删除:

SELECT pg_drop_replication_slot('old_sub_slot');

然后针对高频更新的大表单独下调阈值,而不是全局修改,以免小表被频繁清理:

ALTER TABLE public.orders SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold = 5000,
  autovacuum_vacuum_cost_limit = 2000
);

如果实例规格较大、表较多,还可以在参数模板里把 autovacuum_max_workers 适当调大(修改后需重启生效,建议放在维护窗口)。

五、第四步:回收已经膨胀的空间

普通 VACUUM 只是把空间标记为可复用,并不会缩小文件。想把磁盘真正还回来,常见做法有两种:

  • VACUUM FULL:会重写整张表,期间持有 ACCESS EXCLUSIVE 锁,读写全部阻塞,只适合小表或停机窗口。
  • pg_repack:通过触发器加影子表在线重建,只在开始和结束时短暂加锁,适合线上大表。RDS for PostgreSQL 的插件列表中可以查看是否支持当前版本。

使用 pg_repack 时需要预留大约与目标表加索引同等大小的剩余磁盘空间,并且表必须有主键或唯一非空索引:

CREATE EXTENSION IF NOT EXISTS pg_repack;
-- 在能连接实例的 ECS 上执行客户端命令
pg_repack -h <实例内网地址> -p 5432 -U root -d appdb --table=public.orders --no-superuser-check

执行前后各跑一次第二步的统计 SQL,我那次 orders 表从 62 GB 降到了 21 GB,按主键查询的平均耗时也从十几毫秒回到了 1 毫秒左右。

六、日常巡检建议

  • 把"死元组比例 Top 10"和"最长事务时长"两条 SQL 做成定时巡检,结果超过阈值就告警。
  • 在云监控中关注实例的磁盘使用率和事务 ID 年龄(age(datfrozenxid)),后者接近 2 亿时要提前安排冻结清理,避免触发防回卷的强制 VACUUM。
  • 应用侧统一使用连接池,并确保每个异常分支都会回滚事务。

表膨胀本身不复杂,难点在于它是慢慢积累的,等到磁盘告警时往往已经很严重。只要把"长事务"和"清理阈值"这两个点盯住,大部分问题都能在早期发现并处理。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。