合 《PostgreSQL技术内幕——原理探索》第六章 清理过程(VACUUM)
Tags: PGPostgreSQL翻译《PostgreSQL技术内幕——原理探索》vacuum
- 6.1 并发清理概述
- 伪码:并发清理
- 6.1.1 第一部分
- 6.1.2 第二部分
- 6.1.3 第三部分
- 6.1.4 后续处理
- 6.2 可见性映射
- 6.2.1 可见性映射的改进
- 6.3 冻结过程
- 6.3.1 惰性模式
- 6.3.2 迫切模式
- 如何显示pg_class.relfrozenxid与pg_database.datfrozenxid
- FREEZE选项
- 6.3.3 改进迫切模式中的冻结过程
- 6.4 移除不必要的提交日志文件
- pg_database.datfrozenxid与clog文件
- 6.5 自动清理守护进程
- 关于如何维护AUTOVACUUM
- 6.6 完整清理(FULL VACUUM)
- 伪代码:完整清理
- 什么时候该使用VACUUM FULL?
清理(VACUUM)是一种维护过程,有助于PostgreSQL的持久运行。它的两个主要任务是删除死元组,以及冻结事务标识,两者都在第5.10节中简要提到过。
为了移除死元组,清理过程有两种模式:并发清理(Concurrent Vacuum) 与完整清理(Full Vacuum) 。并发清理(通常简称为VACUUM
)会删除表文件每个页面中的死元组,而其他事务可以在其运行时继续读取该表。相反,完整清理不仅会移除整个文件中所有的死元组,还会对整个文件中所有的活元组进行碎片整理。而其他事务在完整清理运行时无法访问该表。
尽管清理过程对PostgreSQL至关重要,但与其他功能相比,它的改进相对其他功能而言要慢一些。例如在8.0版本之前,清理过程必须手动执行(通过psql
实用程序或使用cron
守护进程)。直到2005年实现了autovacuum
守护进程时,这一过程才实现了自动化。
由于清理过程涉及到全表扫描,因此该过程代价高昂。在版本8.4(2009)中引入了可见性映射(Visibility Map, VM)来提高移除死元组的效率。在版本9.6(2016)中增强了VM,从而改善了冻结过程的表现。
6.1节概述了并发清理的过程,而后续部分的内容如下所示:
- 可见性映射
- 冻结(Freeze)过程
- 移除不必要的clog文件
- 自动清理(AutoVacuum)守护进程
- 完整清理
6.1 并发清理概述
清理过程为指定的表,或数据库中的所有表执行以下任务。
移除死元组
- 移除每一页中的死元组,并对每一页内的活元组进行碎片整理。
- 移除指向死元组的索引元组。
冻结旧的事务标识(
1txid)
- 如有必要,冻结旧元组的事务标识(txid)。
- 更新与冻结事务标识相关的系统视图(
pg_database
与pg_class
)。 - 如果可能,移除非必需的提交日志(clog)。
其他
- 更新已处理表的空闲空间映射(FSM)和可见性映射(VM)。
- 更新一些统计信息(
pg_stat_all_tables
等)。
这里假设读者已经熟悉以下术语:死元组,冻结事务标识,FSM,clog;如果读者不熟悉这些术语的含义,请参阅第5章。VM将在第6.2节中介绍。
以下伪代码描述了清理的过程。
伪码:并发清理
12345678910111213141516171819202122 (1) FOR each table(2) 在目标表上获取 ShareUpdateExclusiveLock 锁/* 第一部分 */(3) 扫描所有页面,定位死元组;如有必要,冻结过老的元组。(4) 如果存在,移除指向死元组的索引元组。/* 第二部分 */(5) FOR each page of the table(6) 移除死元组,重排本页内的活元组。(7) 更新 FSM 与 VMEND FOR/* 第三部分 */(8) 如果可能,截断最后的页面。(9) 更新系统数据字典与统计信息释放ShareUpdateExclusiveLock锁END FOR/* 后续处理 */(10) 更新统计信息与系统数据字典(11) 如果可能,移除没有必要的文件,以及clog中的文件。
- 从指定的表集中依次处理每一张表。
- 获取表上的
ShareUpdateExclusiveLock
锁, 此锁允许其他事务对该表进行读取。- 扫描表中所有的页面,以获取所有的死元组,并在必要时冻结旧元组。
- 删除指向相应死元组的索引元组(如果存在)。
- 对表的每个页面执行步骤(6)和(7)中的操作
- 移除死元组,并重新分配页面中的活元组。
- 更新目标表对应的FSM与VM。
- 如果最后一个页面没有任何元组,则截断最后一页。
- 更新与目标表清理过程相关的统计数据和系统视图。
- 更新与清理过程相关的统计数据和系统视图。
- 如果可能,移除clog中非必需的文件与页面。
该伪码分为两大块:一块是依次处理表的循环,一块是后处理逻辑。而循环块又能分为三个部分,每一个部分都有各自的任务。接下来会描述这三个部分,以及后处理的逻辑。
6.1.1 第一部分
这一部分执行冻结处理,并删除指向死元组的索引元组。
首先,PostgreSQL扫描目标表以构建死元组列表,如果可能的话,还会冻结旧元组。该列表存储在本地内存中的maintenance_work_mem
里(维护用的工作内存)。冻结处理将在第6.3节中介绍。
扫描完成后,PostgreSQL根据构建得到的死元组列表来删除索引元组。该过程在内部被称为“清除阶段(cleanup stage)”。不用说,该过程代价高昂。在10或更早版本中始终会执行清除阶段。在11或更高版本中,如果目标索引是B树,是否执行清除阶段由配置参数vacuum_cleanup_index_scale_factor
决定。详细信息请参考此参数的说明。
当maintenance_work_mem
已满,且未完成全部扫描时,PostgreSQL继续进行后续任务,即步骤4到7;完成后再重新返回步骤3并继续扫描。
6.1.2 第二部分
这一部分会移除死元组,并逐页更新FSM和VM。图6.1展示了一个例子:
图6.1 删除死元组
假设该表包含三个页面,这里先关注0号页面(即第一个页面)。该页面包含三条元组, 其中Tuple_2
是一条死元组,如图6.1(1)所示。在这里PostgreSQL移除了Tuple_2
,并重排剩余元组来整理碎片空间,然后更新该页面的FSM和VM,如图6.1(2)所示。 PostgreSQL不断重复该过程直至最后一页。
请注意,非必需的行指针是不会被移除的,它们会在将来被重用。因为如果移除了行指针,就必须同时更新所有相关索引中的索引元组。
6.1.3 第三部分
第三部分会针对每个表,更新与清理过程相关的统计信息和系统视图。
此外,如果最后一页中没有元组,则该页会从表文件中被截断。
6.1.4 后续处理
当处理完成后,PostgreSQL会更新与清理过程相关的几个统计数据,以及相关的系统视图;如果可能的话,它还会移除部分非必需的clog(第6.4节)。
清理过程使用8.5节中将描述的环形缓冲区(ring buffer)。因此处理过的页面不会缓存在共享缓冲区中。
6.2 可见性映射
清理过程的代价高昂,因此PostgreSQL在8.4版中引入了VM,用于减小清理的开销。
VM的基本概念很简单。 每个表都拥有各自的可见性映射,用于保存表文件中每个页面的可见性。 页面的可见性确定了每个页面是否包含死元组。清理过程可以跳过没有死元组的页面。
图6.2展示了VM的使用方式。 假设该表包含三个页面,第0页和第2页包含死元组,而第1页不包含死元组。 表的可见性映射中保存着哪些页面包含死元组的信息。 在这种情况下,清理过程可以参考VM中的信息,跳过第一个页面。
图6.2 VM的使用方式
每个VM由一个或多个8 KB页面组成,文件以后缀_vm
存储。 例如,一个表文件的relfilenode
是18751,其FSM(18751_fsm
)和VM(18751_vm
)文件如下所示。
1 2 3 4 5 | $ 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 可见性映射的改进
可见性映射在9.6版中进行了加强,以提高冻结处理的效率。新的VM除了显示页面可见性之外,还包含了页面中元组是否全部冻结的信息,参见第6.3.3节。
6.3 冻结过程
冻结过程有两种模式,依特定条件而择其一执行。为方便起见,将这两种模式分别称为惰性模式(lazy mode)和迫切模式(eager mode)。
并发清理(Concurrent VACUUM)通常在内部被称为“惰性清理(lazy vacuum)”。但是,本文中定义的惰性模式是冻结过程(Freeze Processing)执行的模式。
冻结过程通常以惰性模式运行;但当满足特定条件时,也会以迫切模式运行。在惰性模式下,冻结处理仅使用目标表对应的VM扫描包含死元组的页面。迫切模式相则反,它会扫描所有的页面,无论其是否包含死元组,它还会更新与冻结处理相关的系统视图,并在可能的情况下删除不必要的clog。
第6.3.1和6.3.2节分别描述了这两种模式;第6.3.3节描述了改进后的迫切模式冻结过程。
6.3.1 惰性模式
当开始冻结处理时,PostgreSQL计算freezeLimit_txid
,并冻结t_xmin
小于freezeLimit_txid
的元组。
freezeLimit_txid
定义如下: $$ \begin{align} \verb|freezeLimit_txid| = (\verb|OldestXmin| - \verb|vacuum_freeze_min_age|) \end{align} $$
而OldestXmin
是当前正在运行的事务中最早的事务标识(txid)。 举个例子,如果在执行VACUUM
命令时,还有其他三个事务正在运行,且其txid
分别为100,101,102
,那么这里OldestXmin
就是100。如果不存在其他事务,OldestXmin
就是执行此VACUUM
命令的事务标识。 这里vacuum_freeze_min_age
是一个配置参数(默认值为50,000,000
)。
图6.3给出了一个具体的例子。这里Table_1
由三个页面组成,每个页面包含三条元组。 执行VACUUM
命令时,当前txid
为50,002,500
且没有其他事务。在这种情况下,OldestXmin
就是50,002,500
;因此freezeLimit_txid
为2500
。冻结过程按照如下步骤执行。
图6.3 冻结元组——惰性模式
第0页:
三条元组被冻结,因为所有元组的
t_xmin
值都小于freezeLimit_txid
。此外,因为Tuple_1
是一条死元组,因而在该清理过程中被移除。第1页:
通过引用可见性映射(从VM中发现该页面所有元组都可见),清理过程跳过了对该页面的清理。
第2页:
Tuple_7
和Tuple_8
被冻结,且Tuple_7
被移除。
在完成清理过程之前,与清理相关的统计数据会被更新,例如pg_stat_all_tables
视图中的n_live_tup
,n_dead_tup
,last_vacuum
,vacuum_count
等字段。
如上例所示,因为惰性模式可能会跳过页面,它可能无法冻结所有需要冻结的元组。
6.3.2 迫切模式
迫切模式弥补了惰性模式的缺陷。它会扫描所有页面,检查表中的所有元组,更新相关的系统视图,并在可能时删除非必需的clog文件与页面。
当满足以下条件时,会执行迫切模式。 $$ \begin{align} \verb|pg_database.datfrozenxid| < (\verb|OldestXmin| - \verb|vacuum_freeze_table_age|) \end{align} $$ 在上面的条件中,pg_database.datfrozenxid
是系统视图pg_database
中的列,并保存着每个数据库中最老的已冻结的事务标识。细节将在后面描述;因此这里我们假设所有pg_database.datfrozenxid
的值都是1821
(这是在9.5版本中安装新数据库集群之后的初始值)。 vacuum_freeze_table_age
是配置参数(默认为150,000,000
)。
图6.4给出了一个具体的例子。在表1中,Tuple_1
和Tuple_7
都已经被删除。Tuple_10
和Tuple_11
则已经插入第2页中。执行VACUUM
命令时的事务标识为150,002,000
,且没有其他事务。因此,OldestXmin=150,002,000
,freezeLimit_txid=100,002,000
。在这种情况下满足了上述条件:因为1821 < (150002000 - 150000000)
,因而冻结过程会以迫切模式执行,如下所示。