PostgreSQL 性能优化(七):MVCC、Vacuum 与资源边界
本文沿用第 1 篇的订单系统案例,聚焦 MVCC 回收、自动维护与运行资源边界。
MVCC 为什么会留下死元组
PostgreSQL 用 MVCC(多版本并发控制)让事务按各自快照读取一致数据。UPDATE 通常创建新元组版本,旧版本继续留在堆表中;DELETE 也只是让目标版本对后续事务不可见。只要仍有旧快照可能读取这些版本,数据库就不能回收它们。等可见性边界推进后,它们才成为 Vacuum 能处理的死元组。
普通 Vacuum 会清理不再需要的版本、让页内空间可被后续写入复用,并维护可见性映射。它通常不会重写整张表,也不保证文件立即缩小。写入速度长期高于回收速度时,无效版本、索引项和过期统计信息会一起增加读取与规划成本。
观察 Vacuum 与 Analyze 状态
先从表级统计找出死元组增长快、长时间未维护或修改后尚未分析的对象:
SELECT
relname,
n_live_tup,
n_dead_tup,
n_mod_since_analyze,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
这些值是统计估算,适合看趋势,不等于精确膨胀率。还要结合表大小、更新速率、事务年龄、autovacuum 日志和当前维护进度判断。ANALYZE 更新规划器统计信息,但不回收死元组;需要临时补做维护时可以执行:
VACUUM (ANALYZE, VERBOSE) orders;
频繁手工执行只能缓解现象。若同一张表持续落后,应查清自动维护为何触发太晚、运行太慢,或为何被旧快照限制。
按热点表调整 autovacuum
autovacuum 的触发阈值由固定数量与表规模比例共同决定。超大表即使只修改一小部分,绝对死元组数也可能很高;小而高频更新的表则可能很快积累过期统计。优先针对热点表设置参数,避免一次全局调整改变所有表的 I/O 行为:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01
);
示例值只说明配置方式,不是通用推荐值。生产值应由表行数、每秒更新量、死元组增长速度、单次 Vacuum 耗时和可用 I/O 预算反推。调低阈值后还要确认 worker 能及时取得资源,且更频繁的维护没有挤压前台延迟。
长事务会阻止旧版本回收
事务是否写入并不决定它是否阻塞回收;一个长期不结束的只读事务也可能持有旧快照。连接池中的 idle in transaction 尤其危险:业务请求已经停住,事务边界却没有释放。
SELECT
pid,
usename,
application_name,
state,
xact_start,
now() - xact_start AS transaction_age,
age(backend_xmin) AS xmin_age,
wait_event_type,
wait_event,
left(query, 160) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
治理顺序是确认会话所有者,缩短事务范围,保证异常路径提交或回滚,并避免在事务中等待外部网络调用。可以按业务时限设置事务与空闲事务超时,但直接终止后端会中断正在执行的工作,必须先评估影响。
表和索引膨胀的处理边界
普通 Vacuum 主要复用已有空间;只有文件末尾出现可截断空页时才可能归还部分空间。VACUUM FULL 会重写表、需要额外磁盘空间并取得 ACCESS EXCLUSIVE 锁,因此不应作为周期性保养。它适合已经确认存在大量不可复用空洞、能够安排阻塞窗口,并且回收磁盘空间的收益足够明确的场景。
索引同样会因删除、随机插入和索引列更新承受膨胀与写放大。索引很大并不自动意味着需要重建:先比较索引体积、扫描次数、写入代价和真实访问计划。证据充分时可评估 REINDEX CONCURRENTLY,但并发方式仍会消耗 CPU、I/O 和临时磁盘,并在部分阶段获取短暂锁。所有重写操作都应预留空间、执行窗口与回滚方案。
连接与内存参数的边界
资源配置的核心不是让单条查询取得尽可能多的内存,而是在目标并发下保持数据库可预测。每个客户端连接对应一个后端进程;应用实例数乘以池上限,才是数据库可能面对的连接规模。连接池应设置硬上限并处理排队与超时,防止流量峰值直接变成数据库内部的进程、锁和查询并发峰值。
PgBouncer 等外部池可以减少短连接成本,但事务池模式会改变会话语义。依赖临时表、会话级 SET、会话锁或跨事务状态的应用,迁移前必须逐项验证。max_connections 是保护边界,不是吞吐目标;提高它可能让更多昂贵查询同时争用同一份 CPU、内存和存储。
shared_buffers、work_mem 与 effective_cache_size
下面把连接控制和主要内存参数放在同一张风险表中。判断“是否直接分配”时要区分启动时共享内存、按执行操作实际使用的上限,以及仅供规划器估算的提示。
| 设置 | 作用 | 是否直接分配 | 放大风险 |
|---|---|---|---|
| 连接池 | 在应用或代理侧限制并复用数据库连接 | 否;它控制进入数据库的并发 | 多实例池上限相加,可能突破数据库容量 |
max_connections | 限制可建立的后端连接数 | 不是为每个槽位预分配全部查询内存;提高上限会增加共享资源与后端预算 | 更多进程、锁竞争和同时执行的内存操作 |
shared_buffers | 提供 PostgreSQL 共享缓冲区 | 是;服务器启动时分配共享内存 | 过大会挤压操作系统文件缓存,并增加脏页与检查点压力 |
work_mem | 限制单个排序、哈希等执行操作写临时文件前可用的基础内存 | 按执行操作需要使用,不是每连接只分配一次 | 多计划节点、并行 worker 与并发查询相乘 |
effective_cache_size | 告诉规划器单次查询预计可利用多少缓存 | 否;不预留 PostgreSQL 或操作系统缓存 | 估算失真会让规划器偏向不合适的扫描路径 |
maintenance_work_mem | 为 Vacuum、建索引等维护操作提供内存上限 | 按维护操作使用;autovacuum 还受 autovacuum_work_mem 约束 | 人工任务、autovacuum worker 和并行维护叠加 |
因此不能只用 work_mem × max_connections 估算峰值,也不能复制固定的内存百分比。应从执行计划中的排序与哈希节点、临时块、并行度、维护并发和操作系统余量建立预算,只对受控批处理使用会话级或事务级放大。
PostgreSQL 18 异步 I/O 的作用
PostgreSQL 18 引入异步 I/O 子系统,使后端能够排队多个读取请求。首批受益操作包括顺序扫描、Bitmap Heap Scan 和 Vacuum;可通过 io_method 选择同步、I/O worker 或受支持平台上的 io_uring,并用 pg_aios 观察正在使用的 I/O 句柄。实际收益取决于操作系统、存储设备、缓存状态、I/O 并发设置和工作负载,不能仅凭升级版本推断。
边界必须说清:异步 I/O 改变的是部分读取等待方式和可并行发出的请求数量,不会减少查询必须扫描的行数或数据块数量。缺少过滤条件、分区裁剪或合适索引的查询,仍会读取大量数据并争用缓存与带宽。验证时继续以 EXPLAIN (ANALYZE, BUFFERS) 的扫描行数、块读取、执行时间和等待事件为准,再比较启用方式前后的同口径结果。
运行维护检查清单
- 按表记录死元组、修改量、最近 Vacuum/Analyze 时间与增长趋势。
- 监控最老事务、
idle in transaction、事务 ID 年龄和异常等待事件。 - 为热点表单独计算 autovacuum 阈值,并验证 worker、I/O 与完成时长。
- 在重写表或索引前确认膨胀证据、额外磁盘、锁窗口和回滚步骤。
- 汇总所有应用实例的连接池上限,给前台查询和维护任务分别预算内存。
- 参数或 PostgreSQL 18 I/O 设置变更前保存基线,变更后使用相同负载复测。
维护不是一次性的“清理”,而是让版本回收速度、统计信息新鲜度和资源峰值长期跟得上业务写入。具体上线证据、观察窗口与回滚阈值在第 8 篇继续收敛。