最近搞了一个公众号PostgreSQL运维技术,欢迎来踩~

悄悄放一张:

PostgreSQL运维技术 

 

我在之前的一篇介绍MVCC的文章中提到,MVCC有事务ID回卷的问题,它的解决方案是VACUUM。但是没有展开讲。这章的话,会详细地介绍下VACUUM。

VACUUM的两个主要任务是删除死亡元组和冻结事务ID

为了删除死亡元组,VACUUM提供了两种模式,即Concurrent Vacuum和Full Vacuum

Concurrent Vacuum通常简称为VACUUM。它在删除表文件中的死亡元组时,其他事务仍可以在此进程运行时读取表。而Full VACUUM在运行时其他事务不能访问表。

 

本章将主要描述以下内容

VM(可见性映射)

Freeze

删除不必要的clog

AutoVacuum

Full VACUUM

 

 

注:本文是The Internals of PostgreSQL第六章


 

6.1. VACUUM的概述

 

VACUUM对数据库中的表执行以下的任务:

1、删除死亡元组

    删除死亡元组和整理每个页面的活元组。

    删除指向死元组的索引元组。

2、冻结old txids

    如有必要,冻结元组的old txids。

    更新冻结的txid相关的系统目录(pg_database和pg_class)。

    如果可能,清除clog不必要的部分。

3、其他

    更新已处理表的FSM和VM。

    更新一些统计信息(pg_stat_all_tables等)。

 

下面的伪代码描述了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()@indexam.c函数清理索引。

(9)截断最后一页,如果最后一页没有任何元组。

(10)更新目标表中与vacuum处理相关的统计信息和系统目录。

(11)更新与真空处理相关的统计数据和系统目录。

(12)如果可能,删除clog中不必要的文件和页面。

 

这个伪代码有两个部分:每个表的循环和后续处理。内环可分为三个区块。每个块都有单独的任务。

下面将概述这三个模块和后期处理。

 

注意:VACUUM命令从版本13开始就支持PARALLEL选项,如果设置了该选项,并且创建了多个索引,那么它将并行地处理vacuum index和cleanup index 阶段。

注意,此特性仅在VACUUM命令中有效,autovacuum不支持此特性。

 

6.1.1. 第一部分

 

这一部分执行冻结处理并删除指向死元组的索引元组。

 

首先,PostgreSQL扫描一个目标表,建立一个死元组列表,如果可能的话,冻结旧元组。列表存储在本地内存的maintenance_work_mem中。

 

扫描完成后,PostgreSQL通过引用死元组列表来删除索引元组。这个过程在内部被称为“清理阶段”。无须多说,这个过程是昂贵的。在版本10或更早的版本中,清理阶段是一定会被执行的。在版本11或更高版本中,如果目标索引是B-tree,那么是否执行清理阶段由配置参数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. 第三部分

 

第三部分在删除索引后执行清理,并更新与每个目标表vacuum处理相关的统计信息和系统目录。

而且,如果最后一页没有元组,它将从表文件中截断。

 

6.1.4. 后续处理

 

当vacuum处理完成后,PostgreSQL会更新一些与vacuum处理相关的统计信息和系统目录,并尽可能删除不必要的clog(章节6.4)。

 

注:

VACUUM采用第8.5节所述的ring buffer;因此,已处理的页面不会缓存在共享缓冲区(shared buffer)中。

 


 

6.2. VM(可见性映射)

 

VACUUM处理成本很高;因此,在8.4版本中引入VM以减少此成本。

 

VM的基本概念很简单: 每个表都有一个单独的可见性映射,它保存表文件中每个页面的可见性。页面的可见性决定了每个页面是否有死元组。真空处理可以跳过没有死元组的页面。

 

VM的使用情况如图6.2所示。假设该表由三个页面组成,第0页和第2页包含死元组,第1页不包含。该表的VM保存有关哪些页包含死元组的信息。此时,vacuum处理跳过第1页,直接参考VM的信息进行处理。

图. 6.2. VM是怎么工作的?

 

每个VM由一个或多个8 KB的页面组成,该文件以“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的增强版

 

在9.6版本中增强了VM,以提高冻结处理的效率。新的VM显示了页面的可见性以及元组是否在每个页面中被冻结的信息(章节6.3.3)。

 


 

6.3. Freeze处理

 

冻结处理有两种模式,根据特定条件在任意一种模式下执行。为了方便起见,这些模式被称为Lazy(惰性)模式和eager(急切)模式。

注:concurrent Vacuum通常被称为“lazy Vacuum”。然而,本文档中定义的lazy模式是冻结处理执行的一种模式。

冻结处理通常在lazy模式下运行;但是,在满足特定条件时运行eager模式。

在lazy模式下,冻结处理只扫描使用目标表的VM各自包含死元组的页面。

相反, eager模式扫描所有页面,而不管每个页面是否包含死元组,它还更新与冻结处理相关的系统目录,并在可能的情况下删除clog中的不必要部分。

 

第6.3.1节和第6.3.2节分别描述了这两个模式。以eager模式改进冻结流程,请参见6.3.3。

 

6.3.1. Lazy 模式

 

当启动freeze处理时,PostgreSQL会计算一个freezeLimit txid值,冻结t_xmin小于这个值的元组。

freezeLimit txid的定义如下:​​​​​​​

freezeLimit_txid=(OldestXmin−vacuum_freeze_min_age)

 

OldestXmin是当前运行的事务中最古老的txid。举个例子,如果在执行VACUUM命令时运行三个事务(txids 100、101和102),那么OldestXmin是100。

而vacuum_freeze_min_age是一个配置参数(默认为50,000,000)。

图6.3显示了一个特定的示例。这里,Table_1由三个页面组成,每个页面有三个元组。当执行VACUUM命令时,当前txid为50,002,500,并且没有其他事务。在这种情况下,OldestXmin是50002500,因此,freezeLimit txid为2500。冻结处理如下所示。

 

图. 6.3. Lazy Mode

图片

 

第0页: 冻结三个元组是因为所有t_xmin值都小于freezeLimit txid。另外,在这个VACUUM过程中,Tuple_1会因为都是死元组而被移除。

第1页: 此页因为VM而略过。

第2页: Tuple_7和Tuple_8冻结;Tuple_7被移除。

在完成VACUUM处理之前,与VACUUM相关的统计信息会被更新,例如:pg_stat_all_tables' n_live_tup, n_dead_tup, last_vacuum, vacuum_count等。

 

6.3.2. Eager 模式

 

eager模式弥补了lazy模式的缺陷。它扫描所有页面,以检查表中的所有元组,更新相关的系统目录,并删除不必要的文件和页面阻塞,如果可能的话。

当满足以下条件时,将执行eager模式:​​​​​​​

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_7都被移除。Tuple_10和Tuple_11已经插入到第二页。当执行VACUUM命令时,当前txid为150,002,000,并且不存在其他事务。因此,OldestXmin为150,002,000,而freezeLimit txid为100,002,000。在这种情况下,上述条件得到满足,因为:

1821<(150002000−150000000)

因此,冻结处理以eager模式执行,如下所示。

(请注意,这是9.5或更早版本的行为;最新的行为将在章节6.3.3中描述。)

图6.4. 以eager模式冻结旧元组(9.5版或更早版本)。

图片

 

第0页:

即使所有元组已经冻结,Tuple_2和Tuple_3仍会被检查,

第1页:

因为所有的t_xmin值都小于freezeLimit txid,所以该页中的三个元组已经被冻结。注意,该页面在惰性模式下被跳过。

第2页:

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.datafronzenxid也可能会被更新。每个pg_database.datfrozenxid列保存着相应数据库里最小的pg_class.relfrozenxid。

例如,如果只有Table_1在eager模式下被冻结,那么pg_database.datfrozenxid不会被更新。因为这个数据库中其他的表的pg_class.relfrozenxid还没有被更新。如果数据库中的所有表都以eager 模式冻结,那么pg_database.datfrozenxid会被更新。

 

Freeze命令的可选项

带有冻结选项的VACUUM命令将强制冻结指定表中的所有txids。这是在eager模式下执行的;但是,freezeLimit被设置为OldestXmin(而不是' OldestXmin - vacuum_freeze_min_age ')。例如,当txid 5000执行VACUUM FULL命令时,没有其他正在运行的事务,OldesXmin将设置为5000,小于5000的txid将被冻结。

 

6.3.3. 改进eager模式下的Freeze处理

 

9.5版本或更早版本中的eager模式效率不高,因为它总是扫描所有页面。例如,在第6.3.2节的示例中,即使第0页中的所有元组都被冻结,仍然会扫描第0页。

为了解决这个问题,9.6版本改进了VM和冻结进程。如6.2.1节所述,VM有关于是否所有元组在每个页面中都被冻结的信息。当以eager模式执行冻结处理时,可以跳过只包含冻结元组的页面。

图6.6显示了一个示例。冻结该表时,根据VM的具体信息跳过第0页。冻结第一页后,由于该页的所有元组都已冻结,关联的VM信息会更新。

图6.6. 在eager模式下冻结旧元组(9.6或更高版本)。

 


 

6.4. 删除不必要的Clog文件

 

在5.4节中描述的clog存储事务状态。当pg_database.datfrozenxid更新后,PostgreSQL尝试删除不必要的clog文件。请注意,相应的clog页面也被删除。

图6.7显示了一个示例。如果最小的pg_database.datfrozenxid包含在阻塞文件“0002”中,可以删除较旧的文件(“0000”和“0001”),因为存储在这些文件中的所有事务都可以作为整个数据库集群中冻结的txid处理。

图. 6.7. 删除不必要的clog文件

图片

 


 

6.5. Autovacuum

 

VACUUM已经通过autovacuum守护进程实现了自动化;因此,PostgreSQL的操作变得非常简单。

autovacuum守护进程定期调用几个autovacuum_worker进程。默认情况下,它每1分钟唤醒一次(由autovacuum_naptime定义),并调用三个worker(由autovacuum_max_works定义)。

由autovacuum调用的autovacuum工作器对各个表并行地逐步执行VACUUM处理,而对数据库活动的影响最小。

 


 

6.6. Full VACUUM

 

虽然并行VACUUM对操作是必要的,但它是不够的。例如,即使删除了许多无用的元组,它也不能减少表的大小。

图6.8展示了一个极端的例子。假设一个表包含3个页面,每个页面包含6个元组。使用DELETE命令删除元组,使用VACUUM命令删除死元组:

图. 6.8. 一个展示(并行)VACUUM缺点的例子。

图片

 

死去的元组被移除;然而,表的大小并没有减少。这不仅浪费磁盘空间,而且对数据库性能有负面影响。例如,在上面的示例中,当读取表中的三个元组时,必须从磁盘加载三个页面。

为了处理这种情况,PostgreSQL提供了FULL VACUUM模式。图6.9显示了这种模式的轮廓。

 

图. 6.9. Full VACUUM 模式的轮廓.

图片

 

(1)创建新表文件:图6.9(1)

当对表执行VACUUM FULL命令时,PostgreSQL首先获取表的AccessExclusiveLock锁,并创建一个大小为8 KB的新表文件。AccessExclusiveLock锁不允许访问。

(2)复制活元组到新表:图6.9(2)

PostgreSQL只将旧表文件中的活元组复制到新表中。

(3)删除旧文件,重建索引,更新statistics, FSM, VM:图6.9(3)

复制完所有的活元组后,PostgreSQL删除旧文件,重建所有相关的表索引,更新该表的FSM和VM,更新相关的统计信息和系统目录。

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处理时,没有人可以访问(读/写)表(阻塞读写)。

  • 临时使用的磁盘空间最多为表的两倍;因此,在处理大表时,有必要检查剩余磁盘容量。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐