1. VACUUM概述
VACUUM是PostgreSQL中的一个辅助进程,它主要负责完成两个主要任务:删除死元组(Dead Tuples)和冻结事务ID(Freezing Tansaction Ids)。本文主要分享VACUUM删除死元组的具体过程以及底层实现;而对于另一个“冻结事务ID”任务则将单独用一篇文章来介绍,因为它需要结合PostgreSQL的多版本并发控制 (Multi-Version Concurrency Control, MVCC)技术来理解。
在PostgreSQL 8.0版本之前,必须手动执行VACUUM进程(使用psql实用程序或libpq等客户端程序中执行),而在2005年xx版本中,VACUUM已经通过守护进程(autovacuum)实现自动化。其好处是它定期运行ANALYZE进程来收集频繁更新表的最新统计信息,这使查询规划器能够优化它的计划。
autovacuum守护进程会在每个autovacuum_naptime(默认1min)时间间隔内启动PostgreSQL的autovacuum工作进程(worker process),并且最多只允许同时运行autovacuum_max_workers(默认是3)个工作进程;然后每个工作进程将逐一检查数据库中的每个表,并在需要时,执行VACUUM或ANALYZE操作。如果需要处理的数据库多于autovacuum_max_workers,则下一个数据库将会在第一个worker结束后立即处理。
这几个选项支持重新配置,在PGDATA路径下的postgresql.conf文件中。如下:
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min # time between autovacuum runs
每当postmaster服务启动之后,系统就会fork()一个autovacuum守护进程, 该守护进程将会周期性地检查指定表(若未指定具体表,则检查系统所有表)中的各块是否存在死元组等相关逻辑处理。和另外几个辅助进程一样,VACUUM进程的运行状态也会受到postmaster服务的监视。postmaster服务会定期去检测VACUUM进程的运行情况,一旦发现该进程不存在,则会立刻重新fork()一个autovacuum辅助进程。ServerLoop()是整个postmaster服务的主要循环函数。
static int ServerLoop(void)
{
//省略若干代码
. . . . .
for (;;)
{
//省略若干代码
. . . . . .
if (!IsBinaryUpgrade && AutoVacPID == 0 &&
(AutoVacuumingActive() || start_autovac_launcher) &&
pmState == PM_RUN)
{
//若autovacuum进程退出, 则重新fork()一个autovacuum辅助进程
AutoVacPID = StartAutoVacLauncher();
if (AutoVacPID != 0)
start_autovac_launcher = false; /* signal processed */
}
//省略若干代码
. . . . . .
}
}
2. VACUUM分类
对于VACUUM清理死元组,它提供了两种模式:VACUUM和 VACUUL FULL。其中VACUUM也默认称为“LAZY VACUUM”,或“Concurrent VACUUM”。这两种模式,都可以实现清理表中死元组的过程,但在底层实现即具体处理细节上有所不同,下面将分别介绍。
2.1 VACUUM
VACUUM的主要工作是回收被标记为“dead”元组占用的存储空间,不过它不会将回收的空间归还给操作系统,而是在同一页中进行碎片整理,因此它们只是可供将来在同一表中插入数据时重用。由于VACUUM操作特定表时,使用的是共享锁,所以同时刻其他读(read)/写(write)操作也可以同时在一个表上进行,而不会被阻塞。
VACUUM删除指定表中死元组的过程主要分为以下3个步骤:
(1) 获取死元组,并删除死元组指向的索引元组
在该过程里,VACUUM会从指定的表中依次获取每个表。在 干货 | PostgreSQL数据表文件底层结构布局分析 一文中描述过,当表文件大于1GB,会重新创建一个表文件,该表文件的命名方式是“表OID + “.” + “从1开始递增的序号”。所以这里强调“从指定表获取每个表”。
之后为表获取ShareUpdateExclusiveLock锁,该锁是一个共享锁,它允许从其它事务中读取数据。对于“VACUUM (non-FULL)、ANALYZE、CREATE INDEX CONCURRENTLY”这几个操作都将使用该锁。锁(ShareUpdateExclusiveLock)在src/include/storage/目录下的lockdefs.h文件中定义。
#define NoLock 0
#define AccessShareLock 1 /* SELECT */
#define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY */
#define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW SHARE */
#define ExclusiveLock 7 /* blocks ROW SHARE/SELECT...FOR UPDATE */
#define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM FULL,
* and unqualified LOCK TABLE */
#define MaxLockMode 8
然后将扫描所有页(page)以获取所有的死元组,并在必要的时候冻结旧元组。如果存在,则删除指向各自死元组的索引元组。
当PostgreSQL在扫描目标表所有页以获取死元组的时候,它会将得到的所有死元组构建成一个列表。该列表存储在maintenance_work_mem缓存中。该缓存默认大小是64MB,最小值不能低于1MB。该缓存大小支持动态修改,具体配置方式,有两种,比如直接修改postgresql.conf配置文件中的该选项值,或是使用ALTER SYSTEM命令,对于该命令的详细使用方式请阅读 一文彻底搞清postgresql.auto.conf 与 postgresql.conf 之间的差异 。
#maintenance_work_mem = 64MB # min 1MB
之后,PostgreSQL将通过引用该缓存(maintenance_work_mem)中的死元组列表来删除索引元组数据。当缓存(maintenance_work_mem)已满,且扫描未完成时,则PostgreSQL会进入第(2)个步骤,执行该步骤的操作;之后返回到步骤(1)中,继续执行扫描剩余页,获取死元组,并将死元组放入缓存(maintenance_work_mem)中。
(2) 删除死元组,更新FSM和VM
删除缓存中的所有死元组,并重新分配(排列)该表页中的剩余活元组,以进行碎片整理。之后,更新目标表的“可见性映射(Visibility Map,VM)”文件和“可用空间映射(Free Space Map,FSM)”文件。
(3) 更新统计信息和系统目录
当VACUUM处理完成后,需要更新目标表(以适应最新的查询优化)以及与VACUUM处理相关的一些统计信息(比如pg_class)和系统目录(如:pg_stat)。
2.2 VACUUM FULL
VACUUM FULL和VACUUM最大的区别在于,VACUUM FULL物理删除了死元组,并且将释放的空间归还给了操作系统。其操作过程也可以划分为3个执行步骤。
(1) 首先会为操作的目标表创建一个互斥锁(AccessExclusiveLock),以阻止外部对该表的任何读/写访问;然后再创建一个与目标表结构完全相同的新表。
#define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM FULL,
* and unqualified LOCK TABLE */
(2) 扫描目标表,并将表中的所有活元组复制到新表中。
(3) 删除目标表,并在新表上创建索引,并更新VM、FSM和统计信息,以及相关系统表、系统目录等。然后释放互斥锁(AccessExclusiveLock)。
为了验证VACUUM FULL会删除旧表,创建新表,并将旧表中的所有活元组复制到新创建的表文件中,我们进行一个小测试。在该命令执行之前,目标表的OID是25402。
现在我们psql的终端中执行VACUUM FULL test; 然后再观察磁盘上面的堆表文件情况。发现旧表文件25402已被删除,而多了一个新表文件33576。
3. 监控VACUUM进程
我们知道,在PostgreSQL中的删除元组、更新元组操作并不会立刻删除磁盘上的对应元组数据,因为这样做的同时提供了理想的ACID语义(当其他事务仍然可以看见更新或删除的行时,会使系统变得更加复杂,并且可能会更慢)。
(1) 对于DELETE,它将现有的对应元组标记为死元组(dead tuple),而不是物理删除这些元组。
(2) 对于UPDATE,它将现有的对应元组标记为死元组(dead tuple),同时插入一个新的元组;即 UPDATE = DELETE + INSERT INTO。
因此,对于表中的每个更新(UPDATE)、删除(DELETE)操作都会产生一个死元组,而这些死元组永远不会被使用(除非存在并行事务)。即使有效元组的数量相同或更少,这些死元组也会导致不必要的额外空间使用,这在PostgreSQL中也称为空间膨胀。因此,我们迫切需要VACUUM进程来清理这些死元组。
所以,监控VACUUM(自动化守护进程autovacuum)的运行状态就显得极其重要。为了确保autovacuum在PostgreSQL中正常运行,我们通常需要监控几个指标,它们分别是:死元组数量、表磁盘使用率、上一次autovacuum时间等。
3.1 死元组数量
对于那些频繁更新、删除的表,跟踪其表中死元组的数量将有助于我们去确定、分析autovacuum守护进程是否能够有效地定期清理死元组功能,以便于重用这些死元组所占用的磁盘空间。
PostgreSQL提供了pg_stat_user_tables视图,该视图提供了库中所有表的详细信息,比如删除的元组数量、死元组数量、或元组数量、最后一次ANALYZE时间、索引扫描次数等等。
比如对于test表,当前总数是750W,并且其中这750W元组都是活元组。同时可看到,有125W的死元组(是因为我前面执行DELETE 150W元组操作)。
现在我们删除test表中1666667条元组,再次查看该视图,发现活元组、死元组数量都有所改变,并且活元组的数量值等同于上一次的750W - 本次删除的1666667;而死元组数量等于之前的1250000 + 本次删除的1666667。
3.2 上一次VACUUM运行时间
如3.1节所述,pg_stat_user_tables视图提供了查询上一次在每个表上成功运行VACUUM和autovacuum的时间信息。比如当前库中共有两个表,分别是test和l,当执行VACUUM(不指明具体表)之后,这两个表都会同时被VACUUM执行清理死元组的逻辑操作。
当然也可以在VACUUM中指明具体的表名,关于VACUUM的可选项参数列表请阅读下面的第4章节。
细心的你一定发现了本文中第1章节里面的这段话:
每个工作进程将逐一检查数据库中的每个表,并在需要时,执行VACUUM或ANALYZE操作。
注意这里特意强调了“并在需要时”。autovacuum默认每隔1min时间会启动autovacuum worker工作进程,按理说pg_stat_user_tables视图中的last_autovacuum字段的值,每隔1min时间也会随着更新。然而实际情况并非如此,因为它还需要满足其他条件,即只有当死元组的总数量超过一定阈值时,autovacuum才会将VACUUM启动运行。
以下公式用于决定是否需要执行VACUUM:
vacuum threshold = vacuum base threshold + vacuum scale factor * number of tuples
默认情况下, 基本阈值(base threshold)是50行,比例因子(scale factor)是0.2(即20%)。这意味着只要死元组超过表总元组数的20%时,就会立即执行VACUUM,但前提是至少有50个元组被标记为“死元组”。
3.2.1 示例演示
为了验证这个结论,我们将test表清空,然后重新导入2000条记录。之后删除500条数据(保证死元组数量超过表的20%),观察pg_stat_user_tables视图中字段last_autovacuum的值。
(1) 插入2000条记录
test=# INSERT INTO test(id, name, age) VALUES (generate_series(1, 2000), 'lixiaogang5', 27);
INSERT 0 2000
Time: 18.751 ms
test=# SELECT COUNT(*) FROM test;
count
-------
2000
(1 row)
Time: 0.722ms
(2) 删除主键id小于等于500的记录。
test=# DELETE FROM test WHERE id <= 500;
DELETE 500
Time: 0.793 ms
test=# SELECT COUNT(*) FROM test;
count
-------
1500
(1 row)
Time: 0.376 ms
现在查看pg_stat_user_tables视图中字段last_vacuum、last_autovacuum的值,发现last_autovacuum字段的值已更新。
注意,默认情况下,目标表中的总记录总数不能低于autovacuum_vacuum_insert_threshold(默认1000)条,不然,即使死元组超过20%也不会执行autovacuum。更多与VACUUM相关的配置参数如下:
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
// Enable autovacuum subprocess? 'on' requires track_counts to also be on.
#autovacuum = on
// -1 disables, 0 logs all actions and their durations, > 0 logs only actions running at least this number of milliseconds.
#log_autovacuum_min_duration = -1
// max number of autovacuum subprocesses(change requires restart)
#autovacuum_max_workers = 3
// time between autovacuum runs
#autovacuum_naptime = 1min
// min number of row updates before vacuum
#autovacuum_vacuum_threshold = 50
// min number of row inserts before vacuum; -1 disables insert vacuums
#autovacuum_vacuum_insert_threshold = 1000
// min number of row updates before analyze
#autovacuum_analyze_threshold = 50
// fraction of table size before vacuum
#autovacuum_vacuum_scale_factor = 0.2
// fraction of inserts over table size before insert vacuum
#autovacuum_vacuum_insert_scale_factor = 0.2
// fraction of table size before analyze
#autovacuum_analyze_scale_factor = 0.1
// maximum XID age before forced vacuum (change requires restart)
#autovacuum_freeze_max_age = 200000000
// maximum multixact age before forced vacuum (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000
// default vacuum cost delay for autovacuum, in milliseconds; -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_delay = 2ms
// default vacuum cost limit for autovacuum, -1 means use vacuum_cost_limit
#autovacuum_vacuum_cost_limit = -1
3.3 目标表磁盘使用空间
跟踪每个目标表磁盘空间的使用情况很重要,因为它能帮助你衡量查询性能随时间的预期变化,以及检测潜在的与VACUUM相关的问题。向大量执行更新、删除操作的目标表中添加许多新的数据记录,却发现该目标表文件使用的磁盘空间额外快速增大,这或许间接地表明autovacuum进程没有在正常运行。因为正常情况下,autovacuum会将表文件块中的死元组标记为“可重用”,因此,若autovacuum运行异常,则新入库的数据会占用新的磁盘空间,而不是重用死元组的磁盘空间。
SELECT
relname AS "table_name",
pg_size_pretty(pg_table_size(C.oid)) AS "table_size"
FROM
pg_class C
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema') AND nspname !~ '^pg_toast' AND relkind IN ('r')
ORDER BY pg_table_size(C.oid)
通过上面的语句查询出来的每个表大小和实际的表文件的磁盘空间占用大小是能够相对应的。
3.3.1 确定autovacuum是否启用
我们可以通过pg_settings视图来查看autovacuum进程是否启用。
test=# SELECT name, setting FROM pg_settings WHERE name='autovacuum';
name | setting
------------+---------
autovacuum | on
(1 row)
test=#
如果autovacuum进程已开启,但是服务器上找不到该进程,则可能是“统计收集器”没有打开。因为autovacuum进程的处理逻辑中,需要去检查目标表中死元组的数量,而这个死元组的数量统计来自于统计收集器。
同样,我们可以通过pg_settings视图来查看统计收集器是否开启。
test=# SELECT name, setting FROM pg_settings WHERE name='track_counts';
name | setting
--------------+---------
track_counts | on
(1 row)
当然,也有可能是autovacuum进程在表的某个点被禁止了,可以通过pg_class系统表来查看autovacuum进程的启用情况。若参数autovacuum_enabled的值为false,则表明禁用了autovacuum进程。
我们可以通过ALTER TABLE . . .语句来重新开启autovacuum进程。
test=# ALTER TABLE test SET (autovacuum_enabled = true);
ALTER TABLE
test=#
3.3.2 通过pg_settings视图查询autovacuum选项参数
通过pg_settings视图,结合模糊查询,可以获取与autovacuum进程相关的选项参数列表。这些选项配置也可以在postgresql.conf配置文件中找到,通过查询pg_settings视图,能够快速帮你确认选项的参数值是多少。
4. VACUUM选项参数
使用psql实用程序登录postmaster服务后,可以使用\d VACUUM命令来获取VACUUM的使用格式以及所有选项参数列表。
test=# \h VACUUM
Command: VACUUM
Description: garbage-collect and optionally analyze a database
Syntax:
VACUUM [ ( option [, ...] ) ] [ table_and_columns [, ...] ]
VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ]
where option can be one of:
FULL [ boolean ]
FREEZE [ boolean ]
VERBOSE [ boolean ]
ANALYZE [ boolean ]
DISABLE_PAGE_SKIPPING [ boolean ]
SKIP_LOCKED [ boolean ]
INDEX_CLEANUP [ boolean ]
TRUNCATE [ boolean ]
PARALLEL integer
and table_and_columns is:
table_name [ ( column_name [, ...] ) ]
5. VACUUM和VACUUM FULL的区别
VACUUM和VACUUM FULL两者的主要区别有如下几点:
(1) VACUUM执行时,不会锁住表;而VACUUM FULL执行期间则会锁住目标表,其他事务无法进行读/写,不允许并行操作。
(2) VACUUM只是将目标表中的死元组的空间转换为可使用状态;而VACUUM FULL则会删除目标表,并释放死元组占用的磁盘空间,将其归还给操作系统。理想情况下,PostgreSQL的应用程序设计方案避免使用VACUUM FULL方式。首先它使用互斥锁,会阻止其他一切尝试操作该目标表的读写行为;其次,由于它本质是上创建一个新的(堆)表文件,然后将所有活元组(live tuples)导入到新表文件中,所以其执行速度比较慢,效率低。当然,优点是执行VACUUM FULL后其表文件只存在活元组,由于没有多余的死元组,因此,将大大提高查询执行的效率。总而言之,若可以,则尽量不去使用VACUUM FULL,除非你十分清楚目标表中大部分的元组都是死元组。