本文沿用第 1 篇的订单系统案例,聚焦 MVCC 回收、自动维护与运行资源边界。

MVCC 为什么会留下死元组

PostgreSQL 用 MVCC(多版本并发控制)让事务按各自快照读取一致数据。UPDATE 通常创建新元组版本,旧版本继续留在堆表中;DELETE 也只是让目标版本对后续事务不可见。只要仍有旧快照可能读取这些版本,数据库就不能回收它们。等可见性边界推进后,它们才成为 Vacuum 能处理的死元组。

普通 Vacuum 会清理不再需要的版本、让页内空间可被后续写入复用,并维护可见性映射。它通常不会重写整张表,也不保证文件立即缩小。写入速度长期高于回收速度时,无效版本、索引项和过期统计信息会一起增加读取与规划成本。

MVCC 保留版本,Vacuum 回收可复用空间长事务会延长死元组寿命;普通 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 篇继续收敛。

参考资料