24.1. 日常清理 #
PostgreSQL 数据库需要一种称为清理的周期性维护。对于许多安装,让第 24.1.6 节所述的自动清理守护进程执行清理就足够了。为了在你的场景中获得最佳效果,你可能需要调整其中描述的自动清理参数。有些数据库管理员希望用手工管理的
VACUUM 命令来补充甚至取代该守护进程的工作,这类命令通常由
cron 或 Task Scheduler
脚本按计划执行。要正确设置手工管理的清理,理解下面几小节讨论的问题至关重要。依赖自动清理的管理员也不妨略读这一材料,以帮助理解和调整自动清理。
24.1.1. 清理基础 #
PostgreSQL 的
VACUUM 命令之所以必须定期处理每个表,有以下几个原因:
- 回收或再利用被更新或删除的行所占用的磁盘空间。
- 更新 PostgreSQL 查询规划器使用的数据统计信息。
- 更新可见性映射,以加快仅索引扫描。
- 防止由于事务 ID 回卷或多事务 ID 回卷而丢失非常旧的数据。
如以下小节所述,这些原因分别决定了需要以不同的频率和范围执行
VACUUM。
VACUUM 有两种变体:标准的 VACUUM 和
VACUUM FULL。VACUUM FULL 可以回收更多磁盘空间,但执行速度要慢得多。此外,标准形式的 VACUUM 可以与生产数据库操作并行运行。(SELECT、INSERT、UPDATE 以及
DELETE 等命令会继续正常工作,不过在表被清理时,你将不能使用
ALTER TABLE 等命令修改该表的定义。)VACUUM FULL 需要对正在处理的表持有
ACCESS EXCLUSIVE 锁,因此不能与对该表的其他使用并行进行。一般来说,管理员应尽量使用标准 VACUUM 并避免
VACUUM FULL。
VACUUM 会产生大量 I/O 流量,这可能导致其他活动会话性能变差。可以调整一些配置参数来降低后台清理对性能的影响,参见第 19.10.2 节。
24.1.2. 回收磁盘空间 #
在 PostgreSQL 中,对某一行执行
UPDATE 或 DELETE 时,不会立即移除该行的旧版本。这种方法对于获得多版本并发控制(MVCC,见第 13 章)的好处是必需的:当行版本仍可能对其他事务可见时,就不能删除它。但最终,过时或已删除的行版本将不再是任何事务关心的对象。必须回收它占用的空间,以供新行重用,从而避免磁盘空间需求无限增长。这是通过运行 VACUUM
完成的。
标准形式的 VACUUM 会移除表和索引中的死行版本,并将空间标记为可供将来重用。不过,它不会把空间归还给操作系统,除非出现一种特殊情况:表尾的一个或多个页面完全空闲,并且能够轻松获得一个排他表锁。相比之下,VACUUM FULL、CLUSTER 和 REPACK 等表重写命令会主动压实表,它会写出一个不含旧行版本所留死空间的全新表文件版本。这会将表的大小降到最小,但可能耗时很长。在操作完成之前,它还需要额外的磁盘空间来存放表的新副本。
例行清理的通常目标,是足够频繁地执行标准 VACUUM,从而避免需要 VACUUM FULL。自动清理守护进程就是按照这种方式工作的,事实上它永远不会发出 VACUUM FULL。这种方法的思路不是让表始终保持在最小尺寸,而是让磁盘空间的使用维持在稳态:每个表占用的空间相当于其最小尺寸,再加上两次清理之间又被用掉的空间。虽然 VACUUM FULL、CLUSTER 和 REPACK 等通过重写表进行压实的操作可以把表重新缩小到最小尺寸,并把磁盘空间归还给操作系统,但如果该表之后还会再次增长,这样做意义并不大。因此,对于维护被频繁更新的表,与其不常执行这类操作,不如适度频繁地执行标准 VACUUM。
有些管理员喜欢自己安排清理,例如在夜间负载较低时完成全部工作。按固定时间表执行清理的难点在于,如果某个表的更新活动出现意外高峰,它可能膨胀到可能需要 VACUUM FULL 或 REPACK 等通过重写表进行压实的操作才能回收空间的程度。使用自动清理守护进程可以缓解这个问题,因为守护进程会根据更新活动动态安排清理。除非工作负载极其可预测,否则完全禁用守护进程是不明智的。一种可能的折中办法是设置守护进程参数,使其只对异常繁重的更新活动作出反应,从而防止情况失控,而在负载正常时,则期望定期调度的
VACUUM 完成大部分工作。
对于不使用自动清理的人,一个典型的做法是在低使用时段每天安排一次面向整个数据库的
VACUUM,并在必要时更频繁地清理那些更新特别频繁的表。(某些更新率极高的安装,甚至会每隔几分钟就清理一次最繁忙的表。)如果你在一个集簇中有多个数据库,不要忘记对每一个数据库都执行
VACUUM;程序 vacuumdb 可能会有帮助。
提示
当一个表由于大规模更新或删除活动而包含大量死行版本时,普通的 VACUUM
可能并不能令人满意。如果你有这样一个表,并且需要回收它所占用的多余磁盘空间,就需要使用 VACUUM FULL、CLUSTER、REPACK,又或者使用 ALTER TABLE
的某一种重写表变体。这些命令会重写整个表的新副本,并为其构建新的索引。所有这些选项都需要 ACCESS EXCLUSIVE 锁,但 REPACK 可以带 CONCURRENTLY 运行,此时仅在交换表和索引文件期间持有 ACCESS EXCLUSIVE 锁。还要注意,它们会临时额外占用大约等于该表大小的磁盘空间,因为在新表和新索引完成之前,旧的表和索引副本都不能被释放。
提示
如果你有一个表,其全部内容会被定期删除,考虑使用
TRUNCATE,而不是先 DELETE 再 VACUUM。TRUNCATE 会立即移除表的全部内容,而不需要后续再执行
VACUUM 或 VACUUM FULL 来回收此时未使用的磁盘空间。缺点是它会破坏严格的 MVCC 语义。
24.1.3. 更新规划器统计信息 #
PostgreSQL 查询规划器依赖于有关表内容的统计信息,以便为查询生成良好的计划。这些统计信息由
ANALYZE 命令收集,它既可以单独调用,也可以作为 VACUUM 的一个可选步骤执行。拥有足够准确的统计信息很重要,否则糟糕的计划选择可能会降低数据库性能。
如果启用了自动清理守护进程,它会在表内容发生足够大变化时自动发出
ANALYZE 命令。不过,管理员也可能更愿意依靠手工调度的
ANALYZE 操作,尤其是在已知某个表上的更新活动不会影响“重要”列统计信息的情况下。守护进程严格依据插入或更新的行数来安排
ANALYZE;它并不知道这些变化是否会导致有意义的统计变化。
分区和继承子表中发生变化的元组,不会触发对父表的分析。如果父表为空或很少变化,它可能永远不会被自动清理处理,因此也就不会收集整个继承树的统计信息。为了保持统计信息最新,必须手工在父表上运行 ANALYZE。
就像为了回收空间而进行的清理一样,频繁更新统计信息对于更新频繁的表比对于很少更新的表更有用。但即便是更新频繁的表,如果数据的统计分布变化不大,也未必需要更新统计信息。一个简单的经验法则是考虑表中各列的最小值和最大值变化了多少。例如,一个包含行更新时间的 timestamp 列,会随着行的插入和更新而持续增大其最大值;这样的列可能比例如存放网站页面 URL 的列更需要频繁更新统计信息。URL 列收到更改的频率可能一样高,但其值的统计分布变化大概相对缓慢。
可以对特定表,甚至仅对表中的特定列运行 ANALYZE,因此如果你的应用需要,确实可以比其他统计更频繁地更新某些统计信息。然而在实践中,通常最好直接分析整个数据库,因为这是一项很快的操作。ANALYZE 使用对表行的统计随机抽样,而不是读取每一行。
提示
虽然按列微调 ANALYZE 的频率未必很有成效,但按列调整
ANALYZE 收集的统计信息详细程度可能是值得的。在 WHERE 子句中被频繁使用且数据分布高度不规则的列,可能需要比其他列更细粒度的数据直方图。参见 ALTER TABLE
SET STATISTICS,或者使用配置参数 default_statistics_target 更改数据库范围的默认值。
此外,默认情况下,可用于函数选择率的信息是有限的。不过,如果你创建了一个统计对象,或者创建了使用函数调用的表达式索引,就会收集到关于该函数的有用统计信息,这可以显著改进使用该表达式索引的查询计划。
提示
自动清理守护进程不会为外部表发出 ANALYZE 命令,因为它无法判断多高的频率才有用。如果你的查询需要依赖外部表的统计信息来正确规划,一个好办法是按照合适的时间表,在这些表上手工运行 ANALYZE 命令。
提示
自动清理守护进程不会为分区表发出 ANALYZE 命令。只有在父表本身发生变化时,继承父表才会被分析,子表上的变化不会触发父表的自动分析。如果你的查询需要依赖父表统计信息来正确规划,就必须周期性地对这些表手工运行
ANALYZE,以保持统计信息最新。
24.1.4. 更新可见性映射 #
清理会为每个表维护一个可见性映射,用来跟踪哪些页面只包含已知对所有活动事务都可见的元组(并且在该页面再次被修改之前,对所有未来事务也都可见)。这样做有两个目的。第一,下一次清理时可以跳过这样的页面,因为其中没有需要清理的内容。
第二,它使 PostgreSQL 能够在不访问底层表的情况下,仅使用索引回答某些查询。由于 PostgreSQL 的索引不包含元组可见性信息,普通索引扫描会为每个匹配的索引条目取回堆元组,以检查当前事务是否应该看到它。相反,仅索引扫描会先检查可见性映射。如果已知该页面上的所有元组都可见,就可以跳过对堆的访问。这在大型数据集中尤其有用,因为可见性映射能够避免磁盘访问。可见性映射比堆小得多,因此即便堆非常大,也很容易缓存。
24.1.5. 防止事务 ID 回卷失败 #
PostgreSQL 的 MVCC 事务语义依赖于能够比较事务 ID(XID)数字:如果某个行版本的插入 XID 大于当前事务的 XID,那么这个行版本就处于“未来”,当前事务不应看到它。然而,由于事务 ID 的大小有限(32 位),长期运行的集簇(超过 40 亿个事务)会遭遇事务 ID 回卷:XID 计数器回到零,于是原本属于过去的事务会突然看起来像在未来,这意味着它们的结果会变得不可见。简而言之,就是灾难性的数据丢失。(实际上数据仍然在那里,但如果你无法访问,这也没什么安慰。)为了避免这种情况,必须至少每 20 亿个事务对每个数据库中的每个表执行一次清理。
周期性清理之所以能解决这个问题,是因为 VACUUM 会将行标记为冻结,表示这些行由一个很久以前就已提交的事务插入,因此该插入事务的效果肯定对所有当前和未来事务都可见。普通 XID 使用模
232 算术进行比较。这意味着,对任意普通 XID,都有 20 亿个 XID 比它“更老”,另有 20 亿个比它“更新”;换句话说,普通 XID 空间是一个没有端点的环。因此,一旦某个行版本带着某个普通 XID 创建出来,那么无论当前谈论的是哪个普通 XID,在接下来的 20 亿个事务中,这个行版本都会看起来处于“过去”。如果该行版本在超过 20 亿个事务之后仍然存在,它就会突然看起来处于未来。为防止这种情况,PostgreSQL 保留了一个特殊 XID,FrozenTransactionId,它不遵循普通 XID 的比较规则,并且始终被视为比每一个普通 XID 都更老。被冻结的行版本会被当作其插入 XID 是
FrozenTransactionId,因此无论是否发生回卷问题,它们对于所有普通事务都会显得处于“过去”,从而这类行版本在被删除之前一直有效,不管要持续多久。
注意
在 9.4 之前的 PostgreSQL 版本中,冻结是通过实际把一行的插入 XID
替换为 FrozenTransactionId 实现的,这一点可以在该行的
xmin 系统列中看到。较新的版本只是设置一个标志位,同时保留行原始的 xmin 以供可能的取证用途。不过,在从 9.4 之前版本通过 pg_upgrade 升级得到的数据库中,仍然可能发现 xmin 等于
FrozenTransactionId(2)的行。
此外,系统目录中可能包含 xmin 等于
BootstrapTransactionId(1)的行,这表示它们是在
initdb 的第一阶段插入的。和
FrozenTransactionId 一样,这个特殊 XID 也被视为比所有普通 XID 都更老。
vacuum_freeze_min_age 控制某个 XID 值需要达到多老,其对应的行才会被冻结。如果那些本来会被冻结的行很快又会被修改,增大这个设置可以避免不必要的工作;而减小这个设置,则会增加在表必须再次被清理之前可以流逝的事务数量。
VACUUM 使用可见性映射来确定表中的哪些页面必须扫描。通常,即使某些页面仍然含有带旧 XID 值的行版本,只要这些页面没有任何死行版本,它也会跳过。因此,普通的 VACUUM
不一定会冻结表中的每一个旧行版本。在这种情况下,VACUUM 最终需要执行一次激进清理,它会冻结所有符合条件且尚未冻结的 XID 和 MXID 值,包括那些来自全部可见(all-visible)但未全部冻结(all-frozen)页面的值。
如果一个表正在积累全部可见但尚未全部冻结的页面,普通清理可能会选择扫描这些本可跳过的页面,以尝试冻结它们。这样可以减少下一次激进清理必须扫描的页面数。这类页面称为急切扫描页。通过增大 vacuum_max_eager_freeze_failure_rate,可以调整急切扫描,使其尝试冻结更多全部可见页面。即使急切扫描已将全部可见但未全部冻结页面的数量降到最低,大多数表仍然需要周期性地进行激进清理。不过,任何成功被急切冻结的页面在激进清理时都可以跳过,因此急切冻结可以尽量减小激进清理的开销。
vacuum_freeze_table_age 控制何时对表执行激进清理。如果距离上一次这类扫描以来经过的事务数大于
vacuum_freeze_table_age 减去
vacuum_freeze_min_age,就会扫描所有全部可见但未全部冻结的页面。把 vacuum_freeze_table_age 设为 0 会强制
VACUUM 始终采用其激进策略。
一个表可以不清理的最长时间,是 20 亿个事务减去上次激进清理时的 vacuum_freeze_min_age 值。超过这个期限仍不清理,就可能丢失数据。为确保不会发生这种情况,对于可能包含未冻结行、且其 XID 年龄大于配置参数 autovacuum_freeze_max_age 所指定年龄的任何表,都会调用自动清理。(即使禁用了自动清理,也会如此。)
这意味着,如果一个表本来不会因为其他原因被清理,大约每经过
autovacuum_freeze_max_age 减去
vacuum_freeze_min_age 个事务,就会在该表上触发一次自动清理。对于那些为了回收空间而经常清理的表,这一点并不重要。然而,对于静态表(包括只接收插入、但没有更新或删除的表),并不需要为了回收空间而清理,因此设法尽可能拉大这些非常大的静态表上强制自动清理之间的间隔,可能会很有用。显然,这可以通过增大 autovacuum_freeze_max_age 或减小
vacuum_freeze_min_age 来做到。
vacuum_freeze_table_age 的实际最大值是 0.95 *
autovacuum_freeze_max_age;高于该值的设置会被截断到该最大值。设置一个高于 autovacuum_freeze_max_age 的值没有意义,因为无论如何,防回卷自动清理都会在那时被触发,而 0.95 的乘数是为了在那之前留出一些余地以手工运行一次
VACUUM。经验上,vacuum_freeze_table_age
应设置为略低于 autovacuum_freeze_max_age 的值,并留出足够空档,使一次常规调度的 VACUUM 或由正常删除、更新活动触发的自动清理能在这个窗口内运行。若设置得过于接近,即使该表最近已经为了回收空间而清理过,也可能仍会触发防回卷自动清理;而较低的值则会导致更频繁的激进清理。
增大 autovacuum_freeze_max_age(以及随之增大的 vacuum_freeze_table_age)唯一的缺点是,数据库集簇的 pg_xact 和 pg_commit_ts
子目录会占用更多空间,因为它必须存储一直回溯到
autovacuum_freeze_max_age 视界的所有事务的提交状态,以及(如果启用了 track_commit_timestamp)时间戳。提交状态每个事务使用两个位,因此如果 autovacuum_freeze_max_age
被设置为允许的最大值 20 亿,预计 pg_xact 会增长到大约 0.5GB,而 pg_commit_ts 会增长到大约 20GB。如果这和你的数据库总大小相比不值一提,就推荐把 autovacuum_freeze_max_age 设为允许的最大值。否则,应根据你愿意为 pg_xact 和 pg_commit_ts
存储分配多少空间来设置它。(默认值 2 亿个事务,对应大约 50MB 的
pg_xact 存储和大约 2GB 的 pg_commit_ts 存储。)
减小 vacuum_freeze_min_age 的一个缺点是,它可能导致
VACUUM 做无用功:如果某个行版本随后不久就被修改(因而获得新的 XID),冻结它就是浪费时间。因此,该设置应足够大,使得行在不太可能再次发生变化之前不会被冻结。
为了跟踪数据库中最老未冻结 XID 的年龄,VACUUM 会在系统表
pg_class 和 pg_database
中存储 XID 统计信息。具体来说,某个表在 pg_class 中对应行的
relfrozenxid 列,记录了最近一次成功推进
relfrozenxid 的 VACUUM 结束时仍然存在的最老未冻结 XID(通常就是最近一次激进 VACUUM)。类似地,某个数据库在
pg_database 中对应行的 datfrozenxid 列,是出现在该数据库中的未冻结 XID 的下界,它就是该数据库内各表
relfrozenxid 值的最小值。查看这些信息的一种方便方式是执行如下查询:
SELECT c.oid::regclass AS table_name,
greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS age
FROM pg_class c
LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE c.relkind IN ('r', 'm');
SELECT datname, age(datfrozenxid) FROM pg_database;
age 列度量的是从该截止 XID 到当前事务 XID 之间的事务数。
提示
当为 VACUUM 命令指定 VERBOSE 参数时,VACUUM 会打印关于该表的各种统计信息。其中包括
relfrozenxid 和
relminmxid 是如何推进的,以及新冻结页面的数量。当自动清理日志(由 log_autovacuum_min_duration 控制)报告一次由自动清理执行的
VACUUM 操作时,服务器日志中也会出现同样的细节。
虽然 VACUUM 主要扫描自上次清理以来被修改过的页面,但它也可能为了冻结它们而急切扫描某些全部可见但未全部冻结的页面;不过,只有当表中每一个可能包含未冻结 XID 的页面都被扫描时,relfrozenxid 才会被推进。当
relfrozenxid 的年龄超过 vacuum_freeze_table_age 个事务、使用了 VACUUM 的
FREEZE 选项,或者所有尚未全部冻结的页面碰巧都需要清理以移除死行版本时,就会发生这种情况。当 VACUUM 扫描了表中每个尚未全部冻结的页面时,它应把 age(relfrozenxid) 设为略高于所用
vacuum_freeze_min_age 设置的值(更高的部分等于自 VACUUM 开始以来已启动的事务数)。VACUUM 会将 relfrozenxid 设置为表中仍然存在的最老 XID,因此最终值有可能比严格所需的新得多。如果直到达到
autovacuum_freeze_max_age 之前,该表都没有执行一次能推进
relfrozenxid 的 VACUUM,那么很快就会被强制执行一次自动清理。
如果由于某种原因,自动清理没能从某个表中清除旧 XID,当数据库中最老的 XID 距离回卷点只剩一亿个事务时,系统将开始发出如下警告消息:
WARNING: database "mydb" must be vacuumed within 99985967 transactions DETAIL: Approximately 4.66% of transaction ID space remains before wraparound. HINT: To avoid transaction ID assignment failures, execute a database-wide VACUUM in that database.
(如提示所建议,手工执行 VACUUM 应能解决问题;但要注意,该 VACUUM 应由超级用户执行,否则它将无法处理系统目录,从而无法推进数据库的 datfrozenxid。)如果忽略这些警告,一旦距离回卷只剩不到三百万个事务,系统就会拒绝分配新的 XID:
ERROR: database is not accepting commands that assign new transaction IDs to avoid wraparound data loss in database "mydb" HINT: Execute a database-wide VACUUM in that database.
在这种状态下,已经在进行中的事务可以继续,但只能启动只读事务。修改数据库记录或截断关系的操作都会失败。VACUUM 命令仍然可以正常执行。请注意,与早期版本中有时给出的建议相反,为了恢复正常运行,不需要也不希望停止 postmaster 或进入单用户模式。相反,请按以下步骤操作:
- 处理旧的预备事务。可以在
pg_prepared_xacts中查找age(transactionid)很大的行。这些事务应当被提交或回滚。 - 结束长时间运行的打开事务。可以在
pg_stat_activity中查找age(backend_xid)或age(backend_xmin)很大的行。这些事务应当被提交或回滚,或者使用pg_terminate_backend终止对应会话。 - 删除旧的复制槽。使用
pg_replication_slots查找age(xmin)或age(catalog_xmin)很大的槽。在许多情况下,这些槽是为了复制到已经不存在,或已经长时间停机的服务器而创建的。如果你删除的是仍然存在且仍可能尝试连接该槽的服务器所对应的槽,该副本可能需要重建。 - 在目标数据库中执行
VACUUM。面向整个数据库的VACUUM最简单;为了减少所需时间,也可以只在relfrozenxid最老的那些表上手工发出VACUUM命令。在这种场景下不要使用VACUUM FULL,因为它需要 XID,因此会失败;但在超级用户模式下,它反而会消耗一个 XID,从而增加事务 ID 回卷的风险。也不要使用VACUUM FREEZE,因为它做的工作会超过恢复正常运行所需的最低限度。 - 一旦恢复正常运行,确保在目标数据库中正确配置自动清理,以避免将来再出现问题。
注意
在早期版本中,有时确实需要停止 postmaster,并在单用户模式下对数据库执行
VACUUM。在典型场景中,现在已经不再需要这样做,而且应尽量避免,因为这意味着要让系统停机。它也更危险,因为它会禁用为防止数据丢失而设计的事务 ID 回卷保护机制。在这种场景下使用单用户模式的唯一理由,是你想通过 TRUNCATE 或
DROP 不需要的表来避免必须对它们执行 VACUUM。预留的三百万事务安全余量就是让管理员有机会这样做。有关使用单用户模式的详情,请参见 postgres 参考页。
24.1.5.1. 多事务与回卷 #
多事务(multitransaction,MultiXact)ID(MXID)用于支持多个事务对行加锁。由于元组头中用于存储锁信息的空间有限,当有多个事务同时锁定一行时,这些信息会被编码为一个“多事务 ID”,简称 multixact ID。某个特定多事务 ID 包含哪些事务 ID 的信息,会单独存放在 pg_multixact 子目录中,而元组头的
xmax 字段中只出现多事务 ID。和事务 ID 一样,多事务 ID 也是以 32 位计数器及其对应存储实现的,因此同样需要仔细处理老化管理、存储清理和回卷问题。还有一个单独的存储区域保存每个多事务的成员列表,它也使用 32 位计数器,因此同样必须被管理。表 9.85 中介绍的系统函数 pg_get_multixact_members()
可用于检查与某个多事务 ID 关联的事务 ID。
每当 VACUUM 扫描表的任何部分时,它都会把遇到的、年龄超过 vacuum_multixact_freeze_min_age 的任何多事务 ID 替换为另一个值,该值可能是零值、单个事务 ID,或者更新的多事务 ID。对于每个表,pg_class.relminmxid
保存该表任何元组中仍可能出现的最老多事务 ID。如果这个值的年龄超过 vacuum_multixact_freeze_table_age,就会强制执行一次激进清理。正如上一节所述,激进清理意味着只有那些已知为全部冻结的页面才会被跳过。可以对 pg_class.relminmxid
使用 mxid_age() 来查看其年龄。
无论出于何种原因而发生,激进的 VACUUM 都保证能够推进表的 relminmxid。最终,随着所有数据库中的所有表都被扫描,并推进其最老的多事务值,较老多事务的磁盘存储就可以被移除。
作为一项安全措施,对于多事务年龄大于 autovacuum_multixact_freeze_max_age 的任何表,都会进行一次激进清理扫描。此外,如果创建的多事务成员条目数量超过大约 20 亿个(在 pg_multixact/members 目录中占用大约 10GB),就会更频繁地对所有表进行激进清理扫描,从多事务年龄最老的表开始。即使自动清理名义上被禁用,这两类激进扫描也都会发生。当条目数量达到大约 40 亿个(在 pg_multixact/members 目录中占用大约 20GB)时,会触发更激进的清理扫描来回收成员存储空间。
表 9.85 中介绍的
pg_get_multixact_stats() 函数提供了一种实时监控多事务分配和使用模式的方法,例如:
=# SELECT *, pg_size_pretty(members_size) members_size_pretty
FROM pg_catalog.pg_get_multixact_stats();
num_mxids | num_members | members_size | oldest_multixact | members_size_pretty
-----------+-------------+--------------+------------------+---------------------
311740299 | 2785241176 | 13926205880 | 2 | 13 GB
(1 row)
该输出显示一个多事务活动明显的系统:大约 3.12 亿个多事务 ID,以及大约 28 亿个成员条目,占用 13 GB 存储空间。如果 num_mxids 激增,可能表示多个会话正在运行带有外键检查的
UPDATE 语句、并发的 SELECT FOR SHARE
操作,或者由于频繁使用保存点导致锁争用。如果 oldest_multixact 值保持不变而 num_members
持续增长,则可能表示长时间运行的事务正在阻止清理,或者自动清理没有跟上工作负载。
与 XID 的情况类似,如果自动清理没能从某个表中清除旧 MXID,当数据库中最老的 MXID 距离回卷点还有一亿个事务时,系统就会开始发出警告。并且,与 XID 的情况一样,如果忽略这些警告,那么当距离回卷点剩余少于三百万个事务时,系统就会拒绝生成新的 MXID。
MXID 用尽时,恢复正常运行的方法与 XID 用尽时大致相同。按照上一节中的相同步骤进行,但有以下差异:
- 如果没有可能出现在多事务中,那么正在运行的事务和预备事务都可以忽略。
- 与事务 ID 回卷不同,复制槽不会直接阻止多事务清理。因此,删除陈旧的复制槽通常与解决多事务 ID 回卷问题无关。
- MXID 信息不会直接出现在诸如
pg_stat_activity这样的系统视图中;不过,查找旧 XID 仍然是判断哪些事务导致 MXID 回卷问题的好办法。 - XID 用尽会阻止所有写事务,而 MXID 用尽只会阻止其中一部分,即那些涉及需要 MXID 的行锁的写事务。
24.1.6. 自动清理守护进程 #
PostgreSQL 具有一个可选但强烈推荐的特性,称为自动清理,其目的是自动执行
VACUUM 和 ANALYZE 命令。启用后,自动清理会检查那些已经积累了大量插入、更新或删除元组的表。这些检查依赖于统计收集功能;因此,除非 track_counts 被设置为 true,否则无法使用自动清理。在默认配置下,自动清理是启用的,并且相关配置参数也设置得比较合适。
所谓“自动清理守护进程”实际上由多个进程组成。其中有一个持久运行的守护进程,称为自动清理启动器,负责为所有数据库启动自动清理工作进程。启动器会把工作分散在时间上,力图在每个数据库中每隔 autovacuum_naptime 秒启动一个工作进程。(因此,如果安装中有 N 个数据库,就会每隔
autovacuum_naptime/N 秒启动一个新的工作进程。)同时最多允许运行 autovacuum_max_workers 个工作进程。如果需要处理的数据库数量多于 autovacuum_max_workers,那么一旦第一个工作进程结束,就会处理下一个数据库。每个工作进程会检查其数据库中的每个表,并根据需要执行 VACUUM 和/或 ANALYZE。可以设置 log_autovacuum_min_duration 和 log_autoanalyze_min_duration 来监控自动清理工作进程的活动。
如果多个大型表在很短时间内都变得需要清理,那么所有自动清理工作进程都可能会长期忙于清理这些表。这会导致其他表和数据库在某个工作进程空闲下来之前都得不到清理。单个数据库中的工作进程数量没有上限,但工作进程会尽量避免重复其他工作进程已经完成的工作。注意,正在运行的工作进程数量不计入 max_connections 或 superuser_reserved_connections 的限制。
对于 relfrozenxid 值的年龄超过 autovacuum_freeze_max_age 个事务的表,总是会执行清理(这也适用于那些通过存储参数修改了冻结最大年龄的表,见下文)。否则,如果自上次 VACUUM 以来已经失效的元组数超过了“清理阈值”,就会对该表执行清理。清理阈值定义如下:
vacuum threshold = Minimum(vacuum max threshold, vacuum base threshold + vacuum scale factor * number of tuples)
其中,vacuum max threshold 是 autovacuum_vacuum_max_threshold,vacuum base threshold 是 autovacuum_vacuum_threshold,vacuum scale factor 是 autovacuum_vacuum_scale_factor,而元组数是
pg_class.reltuples。
如果自上次清理以来插入的元组数超过了定义的插入阈值,也会对该表执行清理,该阈值定义如下:
vacuum insert threshold = vacuum base insert threshold + vacuum insert scale factor * number of tuples * percent of table not frozen
其中,vacuum insert base threshold 是 autovacuum_vacuum_insert_threshold,vacuum insert scale factor 是 autovacuum_vacuum_insert_scale_factor,元组数是
pg_class.reltuples,而表中未冻结部分的百分比是
1 - pg_class.relallfrozen / pg_class.relpages。这样的清理可能使表的一部分被标记为全部可见,并且也允许元组被冻结,从而减少后续清理所需的工作。对于只接收
INSERT 操作、但没有或几乎没有 UPDATE /
DELETE 操作的表,降低该表的 autovacuum_freeze_min_age 可能是有益的,因为这会让元组在更早的清理中被冻结。过时元组数和插入元组数取自累积统计系统;这是一个由每次 UPDATE、DELETE 和
INSERT 操作更新的最终一致计数。如果该表的
relfrozenxid 值的年龄超过
vacuum_freeze_table_age 个事务,就会执行一次激进清理,以冻结旧元组并推进 relfrozenxid。
对于分析操作,也使用了一个类似的条件:其阈值定义如下:
analyze threshold = analyze base threshold + analyze scale factor * number of tuples
该阈值会与自上次 ANALYZE 以来插入、更新或删除的元组总数进行比较。
分区表不直接存储元组,因此不会被自动清理处理。(自动清理会像处理其他表一样处理表分区。)不幸的是,这意味着自动清理不会对分区表执行 ANALYZE,而这可能导致引用分区表统计信息的查询生成次优计划。你可以在分区表首次填充时手工运行
ANALYZE 来绕过这个问题,并在其各分区中的数据分布发生显著变化时再次运行。
自动清理无法访问临时表。因此,应通过会话 SQL 命令执行适当的清理和分析操作。
默认阈值和尺度因子取自 postgresql.conf,但也可以按表覆盖它们(以及许多其他自动清理控制参数);详情见 Storage Parameters。如果某个设置已经通过表的存储参数修改,那么处理该表时就使用那个值;否则使用全局设置。关于全局设置的更多细节,见第 19.10.1 节。
当多个工作进程同时运行时,自动清理的代价延迟参数(见第 19.10.2 节)会在所有运行中的工作进程之间“平衡”,这样无论实际运行了多少个工作进程,系统承受的总 I/O 影响都相同。不过,任何正在处理那些为每表存储参数
autovacuum_vacuum_cost_delay 或
autovacuum_vacuum_cost_limit 显式设置过值的表的工作进程,都不会被纳入这个均衡算法。为 Parallel Vacuum 启动的并行工作进程使用与领导者工作进程相同的代价延迟参数。如果领导者工作进程修改了这些参数,它会把新的参数值传播给所有并行工作进程。
自动清理工作进程通常不会阻塞其他命令。如果某个进程试图获取与自动清理持有的
SHARE UPDATE EXCLUSIVE 锁冲突的锁,那么该加锁操作会中断自动清理。关于冲突锁模式,见表 13.2。但是,如果自动清理正在运行,其目的是防止事务 ID 回卷(也就是说,在 pg_stat_activity
视图中,自动清理查询名以 (to prevent wraparound) 结尾,或者 pg_stat_progress_vacuum 视图中的
started_by 列显示 autovacuum_wraparound 值),则自动清理不会被自动中断。
警告
定期运行需要获取与 SHARE UPDATE EXCLUSIVE 锁冲突的锁的命令(例如 ANALYZE),实际上可能会使自动清理永远无法完成。
24.1.6.1. 自动清理优先级 #
自动清理分两步决定处理对象:首先选择一个数据库,然后选择该数据库中的表。自动清理启动器进程会优先处理有事务 ID 或多事务 ID 回卷风险的数据库,否则就选择最久未被处理的数据库。作为例外,它会跳过自上次统计信息重置以来没有连接或没有活动的数据库,除非它们有回卷风险。
在数据库内部,自动清理工作进程会构建一个需要清理或分析的表列表,并使用一个评分系统对其排序。它通过取若干分量评分中的最大值来为每个表打分,这些分量反映了对清理或分析重要的各种标准。各分量如下:
事务 ID 分量衡量表的
pg_class.relfrozenxid字段按事务数计算的年龄,并与 autovacuum_freeze_max_age 比较。此外,一旦年龄超过 vacuum_failsafe_age,该分量会大幅增加。该分量的最终值可通过 autovacuum_freeze_score_weight 调整。注意,提高此参数的值也会降低该分量开始激进缩放的年龄,即当其值大于1.0时,缩放年龄会除以其值。多事务 ID 分量衡量表的
pg_class.relminmxid字段按多事务数计算的年龄,并与 autovacuum_multixact_freeze_max_age 比较。此外,一旦年龄超过 vacuum_multixact_failsafe_age,或者已创建的多事务成员条目数量超过大约 20 亿条(见第 24.1.5.1 节),该分量会大幅增加。该分量的最终值可通过 autovacuum_multixact_freeze_score_weight 调整。注意,提高此参数的值也会降低该分量开始激进缩放的年龄,即当其值大于1.0时,缩放年龄会除以其值。清理分量衡量已更新或已删除元组的数量,并与使用 autovacuum_vacuum_threshold、autovacuum_vacuum_scale_factor 和 autovacuum_vacuum_max_threshold 计算出的阈值比较。该分量的最终值可通过 autovacuum_vacuum_score_weight 调整。
由插入触发的清理分量衡量已插入元组的数量,并与使用 autovacuum_vacuum_insert_threshold 和 autovacuum_vacuum_insert_scale_factor 计算出的阈值比较。该分量的最终值可通过 autovacuum_vacuum_insert_score_weight 调整。
分析分量衡量已插入、已更新或已删除元组的数量,并与使用 autovacuum_analyze_threshold 和 autovacuum_analyze_scale_factor 计算出的阈值比较。该分量的最终值可通过 autovacuum_analyze_score_weight 调整。
要恢复到 PostgreSQL 19 之前使用的优先级策略(也就是表在 pg_class 系统目录中的排列顺序),请将前述所有“权重”参数都设为 0.0。否则,这些“权重”参数会与各自的分量评分相乘。例如,将 autovacuum_analyze_score_weight 提高到 2.0
会使分析分量评分翻倍。
pg_stat_autovacuum_scores
视图显示当前数据库中所有表的当前分数。
24.1.7. 并行清理 #
VACUUM 可以使用后台工作进程并行执行索引清理(index vacuuming)和索引收尾清理(index cleanup)阶段(各个清理阶段的细节请参见表 27.52)。并行度由该关系上支持并行清理的索引数量决定。对于手工 VACUUM,如果指定了 PARALLEL 选项,则受该选项限制;此外还会进一步受 max_parallel_maintenance_workers 限制。对于自动清理,这受表的 autovacuum_parallel_workers 限制(如果指定),并且还会进一步受 autovacuum_max_parallel_workers 参数限制。请注意,无法保证执行时一定会使用计算出的并行工作进程数量。有可能实际运行的工作进程少于指定数量,甚至完全没有工作进程。
当且仅当索引大小大于 min_parallel_index_scan_size 时,它才能参与并行清理。每个索引只能使用一个工作进程。因此,只有当表中至少有
2 个索引时,才会启动并行工作进程。清理的工作进程会在每个阶段开始前启动,并在阶段结束时退出。这些行为在未来版本中可能会改变。