[译] 6. Vacuum 处理
原文地址:https://www.interdb.jp/pg/pgsql06.html
原文作者:Hironobu SUZUKI
Vacuum 是一个有助于 PostgreSQL 的持久化操作的维护进程。它的两个主要任务是删除死元组和冻结事务 id,这两者都在 5.10 节中简要提及。
为了移除死元组(dead tuples),vacuum 进程提供了两种模式:Concurrent VACUUM 和 Full VACUUM。Concurrent VACUUM通常简称为VACUUM,移除表文件每一页的死元组,VACUUM在运行的同时其它事务能够读取表。相比之下,Full VACUUM 删除死元组并堆整个文件中的活动元组进行碎片整理,Full VACUUM运行期间,其它事务无法访问表。
尽管vacuum处理对于PostgreSQL来说是必不可少的,与其他功能相比,改进其功能的速度很慢。例如,在8.0版之前,这个过程必须手动执行(使用psql使用程序或cron守护进程)。在2005年通过autovacuum守护进程实现自动。
由于vacuum处理涉及扫描整张表,因此,它是一个昂贵的过程。在版本 8.4 (2009) 中,引入了可见性映射 (Visibility Map-VM) 以提高删除死元组的效率。在版本 9.6 (2016) 中,通过增强 VM 改进了冻结过程。
第 6.1 节概述了Concurrent VACUUM 过程。然后,后续部分将描述以下内容。
- Visibility Map
- Freeze processing
- Removing unnecessary clog files
- Autovacuum daemon
- Full VACUUM
6.1 Concurrent VACUUM 概要
VACUUM 处理对数据库中的指定表或所有表执行以下任务。
- 移除死元组
- 移除死元组和对每个页面的活动元组进行碎片整理
- 删除指向死元组的索引元组
- 冻结旧的 txid
- 如有必要,冻结元组的旧 txid
- 更新冻结的 txid 相关系统目录(pg_database 和 pg_class)
- 尽可能移除clog不必要部分
- 其它
- 更新已处理表的 FSM 和 VM
- 更新几个统计信息(pg_stat_all_tables 等)
这里假设读者熟悉以下术语:dead tuples, freezing txid, FSM, 和 the clog;否则,请参阅第 5 章。第 6.2 节将会介绍了 VM。
以下伪代码描述了vacuum处理过程
Pseudocode: Concurrent VACUUM
(1) FOR each table (2) Acquire ShareUpdateExclusiveLock lock for the target table /* The first block */ (3) Scan all pages to get all dead tuples, and freeze old tuples if necessary (4) Remove the index tuples that point to the respective dead tuples if exists /* The second block */ (5) FOR each page of the table (6) Remove the dead tuples, and Reallocate the live tuples in the page (7) Update FSM and VM END FOR /* The third block */ (8) Clean up indexes (9) Truncate the last page if possible (10 Update both the statistics and system catalogs of the target table Release ShareUpdateExclusiveLock lock END FOR /* Post-processing */ (11) Update statistics and system catalogs (12) Remove both unnecessary files and pages of the clog if possible
(1) 从指定表中获取每一张表
(2) 获取表的ShareUpdateExclusiveLock锁。该锁允许其他事务读取表
(3) 扫描所有页面以获取所有死元组,必要时冻结旧元组。
(4) 如果存在指向相应死元组的索引元组,则删除
(5) 为表的每一页执行以下任务,步骤 (6) 和 (7)
(6) 移除死元组并重新分配页面中的活元组
(7)更新目标表各自的FSM和VM
(8) 通过index_vacuum_cleanup()函数清理索引
(9) 如果最后一页没有任何元组,则截断最后一页
(10) 更新与目标表的vacuum处理相关的统计信息和系统目录
(11) 更新与vacuum处理相关的统计和系统目录
(12) 如果可能,删除不必要的文件和clog页面
这个伪代码有两个部分:每个表的循环和后处理。内循环可分为三个块。每个块都有单独的任务。
下面概述了这三个块和后处理。
6.1.1 第一块(内循环)
上面伪代码中的(3),(4)
此块执行冻结处理并删除指向死元组的索引元组。
首先,PostgreSQL 扫描一个目标表以构建一个死元组列表,并在可能的情况下冻结旧元组。该列表存储在本地内存中的 maintenance_work_mem 中。冻结处理在第 6.3 节中描述。
扫描后,PostgreSQL 通过引用死元组列表来删除索引元组。此过程在内部称为“清理阶段(cleanup stage)”。不用说,这个过程是昂贵的。在版本 10 或更早版本中,始终执行清理阶段。在 11 或更高版本中,如果目标索引为 B-tree,则是否执行清理阶段由配置参数 Vacuum_cleanup_index_scale_factor 决定。详见该参数的说明。
当maintenance_work_mem已满且扫描未完成时,PostgreSQL进行下一个任务,即步骤(4)到(7);然后返回步骤(3)并进行余数扫描。
6.1.2 第二块内循环
上面伪代码中的(5),(6),(7)
此块删除死元组并逐页更新 FSM 和 VM。图 6.1 显示了一个示例:
Fig. 6.1. Removing a dead tuple.
假设该表包含三页。我们关注第 0 页(即第一页)。这个页面有三个元组。 Tuple_2 是一个死元组(图 6.1(1))。在这种情况下,PostgreSQL 删除 Tuple_2 并重新排序剩余的元组以修复碎片,然后更新此页面的 FSM 和 VM(图 6.1(2))。 PostgreSQL 继续这个过程直到最后一页。
请注意,不必要的行指针不会被删除,它们将来会被重用。因为,如果删除行指针,则必须更新关联索引的所有索引元组。
6.1.3 第3块内循环
上面伪代码中的(8),(9),(10)
第三个块在删除索引后执行清理,并更新与每个目标表的vacuum处理相关的统计信息和系统目录。
此外,如果最后一页没有元组,则将其从表文件中截断。
6.1.4 后期处理
上面伪代码中的(11),(12)
vacuum处理完成后,PostgreSQL 会更新与vacuum处理相关的几个统计信息和系统目录,如果可能,它会删除不必要的clog部分(第 6.4 节)。
6.2 可见性映射(Visibility Map)
vacuum 处理成本高;因此,在8.4版本中引入VM以降低此成本。
VM 的基本概念很简单。每个表都有一个单独的可见性映射,用于保存表文件中每个页面的可见性。页面的可见性决定了每个页面是否有死元组。vacuum处理可以跳过没有死元组的页面。
图 6.2 显示了 VM 的使用方式。假设表由三页组成,第 0 页和第 2 页包含死元组,而第 1 页没有。该表的 VM 保存有关哪些页面包含死元组的信息。在这种情况下,vacuum 处理通过引用 VM 的信息跳过第一页。
Fig. 6.2. How the VM is used.
每一个VM文件包含一个或多个8KB页,并以'vm'为后缀名。例如:一个表文件,其 relfilenode 为 18751,FSM (18751_fsm) 和 VM (18751_vm) 文件如下所示
$ cd $PGDATA
$ ls -la base/16384/18751*
-rw------- 1 postgres postgres 8192 Apr 21 10:21 base/16384/18751
-rw------- 1 postgres postgres 24576 Apr 21 10:18 base/16384/18751_fsm
-rw------- 1 postgres postgres 8192 Apr 21 10:18 base/16384/18751_vm
6.2.1 VM 增强
VM在PG9.6版本得以增强,以提高冻结处理的效率。它显示页面的可见性和每个页面中元组是否被冻结的信息(第6.3.3节)
6.3 冻结处理
冻结处理有2种模式,它根据特定条件以任何一种模式执行。为了方便起见,这些模式称为 lazy mode 和 eager mode。
Concurrent VACUUM 在内部通常称为“lazy vacuum”。但是,本文档中定义的lazy mode 是冻结处理执行的一种模式。
冻结处理通常以lazy mode运行;但是,当满足特定条件时,就会以eager mode运行。
在lazy 模式中,冻结处理使用目标表的相应 VM 信息仅扫描包含死元组的页面。
相比之下,eager 模式会扫描所有页面,无论每个页面是否包含死元组,它还会更新与冻结处理相关的系统目录,并尽可能删除不必要的clog。
6.3.1 和 6.3.2 节分别描述了这两种模式。第 6.3.3 节描述了在 eager 模式下冻结过程的改进。
6.3.1 Lazy Mode
当启动冻结处理时,PostgreSQL 计算 freezeLimit txid 并冻结t_xmin小于 freezeLimit txid 的元组。freezeLimit txid 定义如下:freezeLimit_txid = (OldestXmin - vacuum_freeze_min_age)
其中,OldestXmin 时当前正在运行事务中最旧的 txid 。例如:在运行 vacuum命令时,假设有3个正在运行的事务(txid 100, 101 和 102),则 OldestXmin 为 100。如果不存在其它事务,OldestXmin 就是运行vacuum 命令的txid。此外, vacuum_freeze_min_age 是配置参数(默认值 50,000,000)
图6.3 展现了一个具体的例子。Table_1 包含3页,并且每页有3个元组。在运行vacuum命令时,当前的txid 是 50,002,500 并没有其它事务。在这种情况下, OldestXmin 是 50,002,500 ;因此,freezeLimit txid 是2500(50,002,500-50,000,000=2500)。冻结处理的执行过程如下。
Fig. 6.3. Freezing tuples in lazy mode.
$0^{th}$ 页:三个元组被冻结,因为所有 t_xmin 值都小于 freezeLimit txid。此外,由于Tuple_1是死元组,它会被Vacuum删除。
$1^{st}$ 页:由VM信息,跳过此页
$2^{nd}$ 页:Tuple_7 和 Tuple_8 被冻结; Tuple_7 被删除。
在完成vacuum之前,会更新和vacuum相关的统计信息,如: pg_stat_all_table 系统表中的 n_live_tup, n_dead_tup, last_vacuum, vacuum_count 等字段。
如上例所示,lazy模式可能无法冻结所有元组,因为它可以跳过页面。
6.3.2 eager mode
eager mode 弥补了 lazy mode的缺陷。它扫描所有页面以检查表中的所有元组,更新相关的系统目录,并尽可能删除不必要的文件和clog页面。
当满足以下条件时运行 eager mode.
pg_database.datfrozenxid < (OldestXmin-vacuum_freeze_table_age)
在上述条件中,pg_database.datfrozenxid 代表 pg_database 的列,并保存每个数据库最旧的冻结 txid。细节在后面描述;因此,我们假设所有 pg_database.datfrozenxid 的值为 1821(这是 9.5 版新装数据库集簇的初始值)。 Vacuum_freeze_table_age 是一个配置参数(默认为 150,000,000)。
图6.4 显示了一个具体例子。在Table_1中,Tuple_1 和 Tuple_2 被删除,Tuple_10和Tuple_11被插入到第2页。在运行 vacuum 命令时,其txid 为 150,002,000 并没有其它事务。因此,OldestXmin 是 150,002,000, freezeLimit txid 是 100,002,000(=150,002,000-50,000,000)。在这种情况下,满足上述条件,因为 '1821 <(150002000-150000000=2000)';因此,冻结处理使用eager mode执行,如下所示。
(注意这是 9.5 或更早版本的行为;最新的行为在第 6.3.3 节中描述。)
Fig. 6.4. Freezing old tuples in eager mode (version 9.5 or earlier).
$0^{th}$ 页:Tuple_2 和 Tuple_3 已被检查,即使所有元组都已被冻结
$1^{st}$ 页:此页面中的三个元组已被冻结,因为所有 t_xmin 值都小于 freezeLimit txid。请注意,在lazy模式下会跳过此页面。
$2^{nd}$ 页:Tuple_10 已被冻结。 Tuple_11 没有。
冻结每个表后,再更新目标表的pg_class.relfrozenxid。 pg_class 是一个系统目录,它的每个 pg_class.relfrozenxid 列都保存着对应目标表最新冻结 xid。本例中,Table_1 的 pg_class.relfrozenxid 更新为当前的 freezeLimit txid(即 100,002,000),这意味着 Table_1 中所有 t_xmin 小于 100,002,000 的元组都被冻结。
在完成vacuum过程之前,pg_database.datfrozenxid 会根据需要进行更新。每个 pg_database.datfrozenxid 列保存对应数据库中的最小 pg_class.relfrozenxid。例如,如果只有 Table_1 在 eager 模式下被冻结,则该数据库的 pg_database.datfrozenxid 不会更新,因为其他关系(表)(从当前数据库中可以看到的其他表和系统目录)的 pg_class.relfrozenxid 没有更改(图 6.5(1))。如果当前数据库中的所有关系(表)都在eager模式下被冻结,则更新数据库的 pg_database.datfrozenxid,因为该数据库的所有关系(表)的 pg_class.relfrozenxid 都更新为当前的 freezeLimit txid(图 6.5(2))。
Fig. 6.5. Relationship between pg_database.datfrozenxid and pg_class.relfrozenxid(s).
如何显示 pg_class.relfrozenxid 和 pg_database.datfrozenxid
在下文中,第一个查询显示了“testdb”数据库中所有可见关系的 relfrozenxid,第二个查询显示了“testdb”数据库的 pg_database.datfrozenxld。
testdb=# VACUUM table_1;
VACUUM
testdb=# SELECT n.nspname as "Schema", c.relname as "Name", c.relfrozenxid
FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','')
AND n.nspname <> 'information_schema' AND n.nspname !~ '^pg_toast'
AND pg_catalog.pg_table_is_visible(c.oid)
ORDER BY c.relfrozenxid::text::bigint DESC;
Schema | Name | relfrozenxid
------------+-------------------------+--------------
public | table_1 | 100002000
public | table_2 | 1846
pg_catalog | pg_database | 1827
pg_catalog | pg_user_mapping | 1821
pg_catalog | pg_largeobject | 1821
...
pg_catalog | pg_transform | 1821
(57 rows)
testdb=# SELECT datname, datfrozenxid FROM pg_database WHERE datname = 'testdb';
datname | datfrozenxid
---------+--------------
testdb | 1821
(1 row)
FREEZE 选项
在 eager 模式下,执行带有 FREEZE 选项的 VACUUM 命令会强制冻结指定表中的所有 txid。但是,freezeLimit 设置为 OldestXmin(而不是“OldestXmin - Vacuum_freeze_min_age”)。例如,当 txid 5000 执行 VACUUM FULL 命令且没有其他正在运行的事务时,OldesXmin 设置为 5000,小于 5000 的 txid 将被冻结。
6.3.3 在eager模式中改进冻结处理
在pg9.5及更早版本中的eager 模式并非高效的,因为总是扫描所有页。例如,在第6.3.2节中的例子,即使第0号页中的所有元组已被冻结,依然会扫描它。
为了解决这个问题,9.6 版本对 VM 和冻结过程进行了改进。正如第 6.2.1 节所述,新 VM 具有每个页面中所有元组是否都被冻结的相关信息。当以 eager 模式执行冻结处理时,可以跳过仅包含冻结元组的页面。
图 6.6 显示了一个示例。冻结此表时,由VM信息而跳过第0号页。冻结第1号页后,更新关联的 VM 信息,因为该页的所有元组都已冻结。
Fig. 6.6. Freezing old tuples in eager mode (version 9.6 or later).
6.4 删除不要的clog文件
在第5.4节描述,clog存储事务状态。当更新pg_database.datfrozenxid时,PostgreSQL 会尝试删除不必要的clog文件。请注意,相应的clog(内存)页面也会被删除。
图 6.7 显示了一个示例。如果最小 pg_database.datfrozenxid 包含在 clog 文件“0002”中,则可以删除较旧的文件(“0000”和“0001”),因为存储在这些文件中的所有事务都可以被视为整个数据库集簇中的已冻结 txid。
Fig. 6.7. Removing unnecessary clog files and pages.
pg_database.datfrozenxid 和 clog文件
下面显示了 pg_database.datfrozenxid 和 clog 文件的实际输出:
$ psql testdb -c "SELECT datname, datfrozenxid FROM pg_database"
datname | datfrozenxid
-----------+--------------
template1 | 7308883
template0 | 7556347
postgres | 7339732
testdb | 7506298
(4 rows)
$ ls -la -h data/pg_xact/ # In version 9.6 or earlier, "ls -la -h data/pg_clog/"
total 316K
drwx------ 2 postgres postgres 28 Dec 29 17:15 .
drwx------ 20 postgres postgres 4.0K Dec 29 17:13 ..
-rw------- 1 postgres postgres 256K Dec 29 17:15 0006
-rw------- 1 postgres postgres 56K Dec 29 17:15 0007
6.5 autocacuum守护进程
vacuum处理已通过 autovacuum 守护进程实现自动化;至此,PostgreSQL 的操作变得异常简单。
autovacuum 守护进程定期调用几个 autovacuum_worker 进程。默认情况下,它每 1 分钟唤醒一次(由 autovacuum_naptime 定义),并调用三个 worker(由 autovacuum_max_works 定义)。
由 autovacuum 调用的 autovacuum worker 逐渐对各个表同时执行vacuum处理,使得对数据库运行的影响最小。
如何维护AUTOVACUUM
参阅"Tuning Autovacuum in PostgreSQL and Autovacuum Internals"
6.6 Full Vacuum
尽管Concurrent VACUUM 是必不可少的操作,但这还不够。例如,即使删除了许多死元组,它也无法减小表大小。
Fig. 6.8. An example showing the disadvantages of (concurrent) VACUUM.
图6.8 显示了一个极端的例子。假设一张表由3页组成,并且每页包含6个元组。执行下面的DELETE命令删除元组,并执行vacuum命令删除死元组:
testdb=# DELETE FROM tbl WHERE id % 6 != 0;
testdb=# VACUUM tbl;
死元组已被删除,但是表的大小未减小。这既浪费磁盘空间,又对数据库性能产生负面影响。例如,在上面的示例中,当读取表中的三个元组时,必须从磁盘加载三个页面。
为了应对这种情况,PostgreSQL 提供了 Full VACUUM 模式。图 6.9 显示了该模式的概要。
Fig. 6.9. Outline of Full VACUUM mode.
[1] 创建新表文件: 图6.9(1)
当对一个表执行 VACUUM FULL 命令时,PostgreSQL 首先获取该表的 AccessExclusiveLock 锁,并创建一个大小为 8 KB 的新表文件。 AccessExclusiveLock 锁不允许访问。
[2] 复制活动元组到新表文件:图6.9(2)
PostgreSQL 仅将旧表文件中的活动元组复制到新表中。
[3] 删除旧表文件,重建索引和更新统计信息, FSM,VM 图6.9(3)
复制所有活动元组后,PostgreSQL 删除旧文件,重建所有关联的表索引,更新该表的 FSM 和 VM,并更新关联的统计信息和系统目录。
Full VACUUM 的伪代码
Full VACUUM 的伪代码如下所示:
(1) FOR each table
(2) Acquire AccessExclusiveLock lock for the table
(3) Create a new table file
(4) FOR each live tuple in the old table
(5) Copy the live tuple to the new table file
(6) Freeze the tuple IF necessary
END FOR
(7) Remove the old table file
(8) Rebuild all indexes
(9) Update FSM and VM
(10) Update statistics
Release AccessExclusiveLock lock
END FOR
(11) Remove unnecessary clog files and pages if possible
使用 VACUUM FULL 命令时应考虑两点:
- 当 Full VACUUM 正在执行时,没有人可以访问(读/写)表
- 最多临时使用两倍的表磁盘空间;因此,在处理一个巨大的表时,需要检查剩余的磁盘容量。
应该什么时候做 VACUUM FULL?
不幸的是,目前没有何时应当执行“VACUUM FULL”的最佳实践。然而,扩展 pg_freespacemap 可能会给你很好的建议。
以下查询显示了您想知道的表的平均可用空间比率。
testdb=# CREATE EXTENSION pg_freespacemap;
CREATE EXTENSION
testdb=# SELECT count(*) as "number of pages",
pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size",
round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio"
FROM pg_freespace('accounts');
number of pages | Av. freespace size | Av. freespace ratio
-----------------+--------------------+---------------------
1640 | 99 bytes | 1.21
(1 row)
从上面的结果可以发现可用空间很少。
如果你删除大量的元组并执行 VACUUM 命令,你会发现大量的空页。
testdb=# DELETE FROM accounts WHERE aid %10 != 0 OR aid < 100;
DELETE 90009
testdb=# VACUUM accounts;
VACUUM
testdb=# SELECT count(*) as "number of pages",
pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size",
round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio"
FROM pg_freespace('accounts');
number of pages | Av. freespace size | Av. freespace ratio
-----------------+--------------------+---------------------
1640 | 7124 bytes | 86.97
(1 row)
以下查询检查指定表的每一页的可用空间比率。
testdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio"
FROM pg_freespace('accounts');
blkno | avail | freespace ratio
-------+-------+-----------------
0 | 7904 | 96.00
1 | 7520 | 91.00
2 | 7136 | 87.00
3 | 7136 | 87.00
4 | 7136 | 87.00
5 | 7136 | 87.00
....
执行 VACUUM FULL 后,可以发现表文件已经被压缩。
testdb=# VACUUM FULL accounts;
VACUUM
testdb=# SELECT count(*) as "number of blocks",
pg_size_pretty(cast(avg(avail) as bigint)) as "Av. freespace size",
round(100 * avg(avail)/8192 ,2) as "Av. freespace ratio"
FROM pg_freespace('accounts');
number of pages | Av. freespace size | Av. freespace ratio
-----------------+--------------------+---------------------
164 | 0 bytes | 0.00
(1 row)
附录
并行选项
VACUUM 命令从版本 pg13 开始支持 PARALLEL 选项,如果开启了并行选项并且已创建了多个索引,vacuum在清理索引阶段会使用并行技术。
请注意,此功能仅在 VACUUM 命令中有效,autovacuum 不支持。