上个月我们经历了一次惊心动魄的磁盘空间报警:生产环境一个核心交易历史表,在执行了一批历史订单状态批量更新任务后,表大小在短短两小时内暴涨了整整 200GB,磁盘可用空间直接掉到 5% 以下。最诡异的是,这个表实际的有效数据行数只有一千多万,按理说最多也就 30GB 左右。当时整个运维群炸开了锅,紧急扩容磁盘后,我们开始排查这个极具杀伤力的 PostgreSQL 表膨胀(Table Bloat)大坑。
在 PostgreSQL 中,MVCC(多版本并发控制)的实现机制和 MySQL / Oracle 的 Undo Log 机制完全不同。PostgreSQL 对数据的每一行更新(UPDATE)或删除(DELETE),并不会直接在原物理位置修改数据,而是新插入一条带行标记的版本(Tuple),并将旧版本标记为“死元组(Dead Tuple)”。
这些死元组必须依赖背景进程 autovacuum 进行清理和物理回收。然而,autovacuum 的正常运转有一个致命前提:它只能清理那些对当前所有活跃事务都不可见的死元组。
如果数据库中存在任何一个长事务未提交(例如:某位开发同学通过客户端连接生产库执行了 BEGIN 查询却没有执行 COMMIT,或者定时分析报表任务跑了几个小时还没结束),PostgreSQL 的全局最小活跃事务 ID(xmin)就会停滞不前。这导致在此长事务之后产生的全部 UPDATE/DELETE 死元组,在 autovacuum 看来都“可能对长事务可见”,因而无法被清理!
结果,在我们的批量更新任务不断修改数据的过程中,数千万死元组堆积在数据文件中,占用了物理存储空间却无法被重用,数据文件如同吹气球般暴涨,最终导致了这次由于长事务未提交引发的 Auto-Vacuum 延迟与表膨胀故障。
排查此类问题时,首先需要揪出是谁在阻塞 vacuum。以下是我们用于分析故障现场的实用 SQL 诊断工具包:
-- 1. 查询当前运行时间最长的活跃事务,找出阻塞 autovacuum 的罪魁祸首SELECT pid, usename, application_name, client_addr, backend_start, xact_start, now() - xact_start AS xact_duration, query, stateFROM pg_stat_activityWHERE state <> 'idle' AND xact_start IS NOT NULLORDER BY xact_duration DESCLIMIT 5;
-- 2. 查询死元组 (Dead Tuples) 堆积最多的前 10 张表,评估清理滞后情况SELECT schemaname, relname, n_dead_tup, n_live_tup, round(100.0 * n_dead_tup / nullif(n_dead_tup + n_live_tup, 0), 2) as dead_tuple_ratio, last_vacuum, last_autovacuumFROM pg_stat_user_tablesWHERE (n_dead_tup + n_live_tup) > 10000ORDER BY n_dead_tup DESCLIMIT 10;揪出元凶并手动 kill 掉卡死事务后,虽然死元组能够被回收,但占用的磁盘文件空间(High Watermark)默认不会退还给操作系统。为了防止这种情况再次发生,我们需要在 postgresql.conf 中优化 autovacuum 参数,让其执行得更积极、更快:
# 开启自动 vacuumautovacuum = on# 记录所有耗时超过 200ms 的 autovacuum 运行细节,便于性能调优log_autovacuum_min_duration = 200# 增加并发 worker 数量,防大表排队(默认 3)autovacuum_max_workers = 5# 降低触发比例阈值,从默认的 20% 降低至 5%,更频繁地触发 vacuum 避免膨胀autovacuum_vacuum_scale_factor = 0.05# 提高单次 vacuum 操作允许消耗的代价上限,极大提升单次清理效率(默认 200)autovacuum_vacuum_cost_limit = 2000对于那些不幸已经膨胀到极致的超大表,如果不能接受 VACUUM FULL 产生的八级重度锁表代价(会导致读写完全阻塞),强烈建议使用 pg_repack 工具。它通过新建临时表并同步增量日志的方式,在不阻塞任何在线读写的前提下完成表空间的在线收缩与重构。
大家在维护千万级以上的 PostgreSQL 数据库时,有没有被各种未提交的长事务坑过?你们的 autovacuum 参数又是怎么调整的?