《postgresql指南--内幕探索》第六章 清理过程

清理过程(通常简称为VACUUM)是一种维护过程,有助于 PostgreSQL 的持久运行。它的两个主要任务是删除死元组,以及冻结事务标识。

为了移除死元组,清理过程有两种模式,分别是并发清理与完整清理。并发清理过程会删除表文件每个页面中的死元组,而其他事务可以在其运行时继续读取该表。相反,完整清理不仅会移除整个文件中所有的死元组,还会对整个文件中所有的活元组进行碎片整理。其他事务在完整清理运行时无法访问该表。

尽管清理过程对PostgreSQL至关重要,但与其他功能相比,它的改进相对其他功能而言要慢一些。例如在8.0版本之前,清理过程必须手动执行(通过psql 实用程序或使用 cron 守护进程)。直到2005年实现了autovacuum 守护进程时,这一过程才实现了自动化。

由于清理过程涉及全表扫描,因此该过程代价高昂。在版本8.4(2009)中引入了可见性映射( Visibility Map,VM)来提高移除死元组的效率。在版本9.6(2016)中增强了 VM,从而改善了冻结过程的表现。

并发清理概述

清理过程为指定的表或数据库中的所有表执行以下任务。

1.移除死元组和对活元组进行碎片整理。

  • 移除每一页中的死元组,并对每一页内的活元组进行碎片整理。
  • 移除指向死元组的索引元组。

2.冻结旧的事务标识。

  • 如有必要,冻结旧元组的事务标识。
  • 更新与冻结事务标识相关的系统视图( pg_database 与 pg_class)。
  • 如果可能,移除不必要的提交日志文件。

3.其他。

  • 更新已处理表的空闲空间映射(FSM)和可见性映射(VM)。
  • 更新一些统计信息( pg_stat_all_tables 等)。

以下伪代码描述了清理的过程。

(1)      FOR each table
(2)          在目标表上获取 ShareUpdateExclusiveLock 锁 /* 允许其他事务对该表进行读取 */

             /* 第一部分 */
(3)          扫描所有页面,定位死元组,如有必要,冻结过早的元组
(4)          如果存在,移除指向死元组的索引元组

             /* 第二部分 */
(5)          FOR each page of the table
(6)               移除死元组,重排本页内的活元组
(7)               逐页更新目标表页对应的 FSM 与 VM
             END FOR

               /* 第三部分 */
(8)          如果最后一个页面没有任何元组,截断最后的页面
(9)          更新系统数据字典与统计信息
               释放ShareUpdateExclusiveLock锁
          END FOR

          /* 后续处理 */
(10)     更新统计信息与系统数据字典
(11)     如果可能,移除非必要的文件及CLOG中的文件

该伪码分为两大块:一块是依次处理表的循环,一块是后处理逻辑。而循环块又分为三个部分,每一个部分都有各自的任务。接下来会描述这三个部分及后处理的逻辑。

第一部分

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

首先,PostgreSQL 扫描目标表以构建死元组列表,如果可能的话,还会冻结旧元组。该列表存储在本地内存中的 maintenance_work_mem 里(维护用的工作内存)。冻结过程将在下文中介绍。

扫描完成后,PostgreSQL 根据构建得到的死元组列表来删除索引元组。该过程在内部被称为“清除阶段”。不用说,该过程代价高昂。在 10.0 或更低版本中始终会执行清除阶段。在 11.0 或更高版本中,如果目标索引是B树,是否执行清除阶段由配置参数 vacuum_cleanup_index_scale_factor 决定。详细信息请参考此参数的说明。

当 maintenance_work_mem 已满,且未完成全部扫描时,PostgreSQL继续进行后续任务,即步骤(4)到(7),完成后再重新返回步骤(3)并继续扫描。

第二部分

这一部分会移除死元组,并逐页更新 FSM 和 VM。
在这里插入图片描述
假设该表包含三个页面,首先关注 0 号页面(即第一个页面),该页面包含三条元组, 其中 Tuple_2 是一条死元组,如图中 (1)所示。在这里PostgreSQL 移除了 Tuple_2,并重排剩余元组来整理碎片空间,然后更新该页面的 FSM 和 VM,如图中(2)所示。PostgreSQL 不断重复该过程直至最后一页。注意,非必要的行指针是不会被移除的,它们会在将来被重用。因为如果移除了行指针,就必须同时更新所有相关索引中的索引元组。

第三部分

第三部分会针对每个表,更新与清理过程相关的统计信息和系统视图。此外,如果最后一页中没有元组,则该页会从表文件中被截断。

后续处理

当处理完成后,PostgreSQL 会更新与清理过程相关的几个统计数据,以及相关的系统视图;如果可能的话,它还会移除部分不必要的 CLOG 文件,见下文。

可见性映射

VM (可见性映射)的基本概念很简单。每个表都拥有各自的可见性映射,用于保存表文件中每个页面的可见性。页面的可见性确定了每个页面是否包含死元组。清理过程可以跳过没有死元组的页面。

如下图所示,假设该表包含3个页面,第0页和第2页包含死元组,而第1页不包含死元组。表的可见性映射中保存着那些页面包含死元组的信息。在这种情况下,清理过程可以参考VM中的信息,跳过第1页。
在这里插入图片描述
每个 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
冻结过程

冻结过程有两种模式,依特定条件而择其一执行。为方便起见,我们将这两种模式分别称为惰性模式和迫切模式。

并发清理通常在内部被称为“惰性清理”。但是,本文中定义的惰性模式是冻结过程执行的模式。

冻结过程通常以惰性模式运行,但当满足特定条件时,也会以迫切模式运行。在惰性模式下,冻结过程仅使用目标表对应的VM扫描包含死元组的页面。迫切模式相则反,它会扫描所有的页面,无论其是否包含死元组,都会更新与冻结过程相关的系统视图,并在可能的情况下删除不必要的CLOG文件。

惰性模式

惰性模式当开始冻结处理时, PostgreSQL 计算 freezeLimit_txid ,并冻结 t_xmin 小于 freezeLimit_txid 的元组。freezeLimit_txid定义如下:

freezeLimit_txid = ( OldestXmin - vacuum_freeze_min_age )

OldestXmin 是当前正在运行的事务中最早的事务标识。举个例子,如果在执行VACUUM命令时,还有其他三个事务正在运行,且其txid分别为100、101和102,那么 OldestXmin 就是 100。如果不存在其他事务,OldestXmin 就是执行此 VACUUM 命令的事务标识。这里vacuum_freeze_min_age是一个配置参数(默认值为50 000 000)。

下图给出了一个具体的例子。Table_1 由三个页面组成,每个页面包含三条元组。执行VACUUM命令时,当前txid为50002500且没有其他事务。在这种情况下,OldestXmin就是50002500,因此freezeLimit_txid为2500。冻结过程按照如下步骤执行。
在这里插入图片描述
第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等字段。

如上例所示,因为惰性模式可能会跳过页面,它可能无法冻结所有需要冻结的元组。

迫切模式(9.5或更低版本)

迫切模式弥补了惰性模式的缺陷。它会扫描所有页面,检查表中的所有元组,更新相关的系统视图,并在可能时删除不必要的CLOG文件与页面。当满足以下条件时,会执行迫切模式。

pg_database.datfrozenxid < ( OldestXmin - vacuum_freeze_table_age)

在上面的条件中,pg_database.datfrozenxid 是系统视图 pg_database 中的列,并保存着每个数据库中最老的已冻结的事务标识,细节将在后面描述。这里我们假设所有 pg_database.datfrozenxid 的值都是1821(这是在9.5版本中安装新数据库集群之后的初始值)。vacuum_freeze_table_age 是配置参数(默认为150 000 000)。

下图给出了一个具体的例子。在表 1 中,Tuple_1 和 Tuple_7都已经被删除,Tuple_10和 Tuple_11 则已经插入第 2 页中。执行 VACUUM 命令时的事务标识为 150 002 000,且没有其他事务。因此,OldestXmin = 150 002 000,freezeLimit_txid = OldestXmin - vacuum_freeze_min_age =(150 002 000 - 50 000 000)=100 002 000。在这种情况下满足了上述条件:因为1821 < (150 002 000 - 150 000 000),所以冻结过程会以迫切模式执行,如下所示。注意,这里是 9.5 或更低版本的行为,最新版本的行为将在下文中描述。
在这里插入图片描述
第0页:即使所有元组都被冻结,也会检查 Tuple_2 和 Tuple_3。

第1页:此页面中的三条元组都会被冻结,因为所有元组的 t_xmin 值都小于 freezeLimit_txid。注意,在惰性模式下会跳过此页面。

第2页: 将 Tuple_10冻结,而 Tuple_11 没有冻结。

冻结完一张表的所有元组后,更新系统视图 pg_class 的 relfrozenxid 为 freezeLimit_txid:

冻结一张表后,目标表的 pg_class.relfrozenxid 将被更新。pg_class是一个系统视图,每个pg_class.relfrozenxid 列都保存着相应表的最近冻结的事务标识。本例中表1的 pg_class.relfrozenxid 会被更新为当前的 freezeLimit_txid(即100 002000),这意味着表 1 中 t_xmin 小于100 002 000 的所有元组都已被冻结。

如果当前数据库中的所有关系都以迫切模式冻结,则更新此数据库的pg_database. datfrozenxid :

在完成清理过程之前,必要时会更新 pg_database.datfrozenxid。每个 pg_database. datfrozenxid 列都包含相应数据库中的最小 pg_class.relfrozenxid。如果在迫切模式下仅仅对表 1 做冻结处理,则不会更新该数据库的 pg_database. datfrozenxid,因为其他关系的 pg_class.relfrozenxid(当前数据库可见的其他表和系统视图)还没有发生变化,如下图中(1)所示。如果当前数据库中的所有关系都以迫切模式冻结,则数据库的 pg_database. datfrozenxid 就会被更新,因为此数据库的所有关系的 pg_class.relfrozenxid 都被更新为当前的 freezeLimit_txid,如下图中(2)所示。

在这里插入图片描述
如何查询 pg_class.relfrozenxid 与 pg_database.datfrozenxid?

如下,分别查询test_db数据库中所有可见关系的relfrozenxid,以及testdb数据库的relfrozenxid。

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选项:

带有 FREEZE 选项的 VACUUM 命令会强制冻结指定表中的所有事务标识。虽然这是在迫切模式下执行的,但是这里 freezeLimit会被设置为 OldestXmin 而不是OldestXmin -vacuum_freeze_min_age。例如,当txid=5000的事务执行 VACUUM FULL 命令,且没有其他正在运行的事务时,OldesXmin 会被设置为 5000,而t_xmin 小于 5000的元组将会被冻结。

改进迫切模式中的冻结过程(9.6版本及更高版本)

9.5或更低版本中的迫切模式效率不高,因为它始终会扫描所有页面。比如在第 3.2 节的例子中,尽管第0页中所有元组都被冻结,但还是会被扫描。

为了解决这一问题,9.6版本改进了可见性映射VM与冻结过程。新VM包含着每个页面中所有元组是否都已被冻结的信息。在迫切模式下进行冻结处理时,可以跳过仅包含冻结元组的页面。

下图中给出了一个例子。根据VM中的信息,冻结此表时会跳过第0页。在更新完1号页面后,相关的VM信息会被更新,因为该页中所有的元组都已经被冻结了。
在这里插入图片描述

移除不必要的 CLOG 文件

CLOG 中存储着事务的状态。当更新 pg_database.datfrozenxid 时, PostgreSQL 会尝试删除不必要的CLOG 文件。注意,相应的 CLOG 页面也会被删除。下图给出了一个例子。如果 CLOG 文件 0002 中包含最小的pg_database.datfrozenxid,则可以删除旧文件(0000 和0001),因为存储在这些文件中的所有事务在整个数据库集簇中已经被视为冻结了。
在这里插入图片描述

自动清理守护进程

自动清理守护进程已经将清理过程自动化,因此 PostgreSQL 运维起来非常简单。自动清理守护程序周期性地唤起几个 autovacuum_worker 进程,默认情况下每分钟唤醒一次(由参数 autovacuum_naptime 定义),每次唤起三个工作进程(由 autovacuum_max_works 定义)。

自动清理守护进程唤起的 autovacuum 工作进程会依次对各个表执行并发清理,从而将对数据库活动的影响降至最低。

完整清理(FULL VACUUM)

虽然并发清理对于运维至关重要,但光有它还不够。比如,即使删除了很多死元组,也无法压缩表大小的情况。
如下图一个极端的例子。假设一个表有3个页面组成,每个页面包含6条元组。delete删除每个页面的5条元组,并执行vacuum移除死元组。
在这里插入图片描述
死元组虽然都被移除了,但表的尺寸没有减小。这种情况既浪费了磁盘空间,又会对数据库性能产生负面影响。例如在上面的例子中,当读取表中的3条元组时,必须从磁盘加载3个页面。

为了解决这种问题,PostgreSQL提供了Full VACUUM模式。下图显示了此模式的概述。
在这里插入图片描述
1.创建新表文件:
当对表执行VACUUM FULL命令时,PostgreSQL首先获取该表的AccessExclusiveLock锁,然后创建一个大小为8 KB的新表文件。AccessExclusiveLock锁不允许访问。

2.将活动元组复制到新表中:
PostgreSQL仅将旧表文件中的活动元组复制到新表中。

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

完整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 possib

使用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)

翻译整理自:
http://www.interdb.jp/pg/pgsql06.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值