冻结过程有两种模式,依特定条件而择其一执行。为方便起见,将这两种模式分别称为惰性模式(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
的元组。
而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文件与页面。 当满足以下条件时,会执行迫切模式。
在上面的条件中,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)
,因而冻结过程会以迫切模式执行,如下所示。
(注意,这里是版本9.5或更早版本的行为;最新版本的行为将在第6.3.3节中描述。)
图6.4 冻结旧元组——迫切模式(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
列都保存着相应表的最近冻结的事务标识。本例中表1的pg_class.relfrozenxid
会被更新为当前的freezeLimit_txid
(即100,002,000
),这意味着表1中t_xmin
小于100,002,000
的所有元组都已被冻结。
在完成清理过程之前,必要时会更新pg_database.datfrozenxid
。每个pg_database.datfrozenxid
列都包含相应数据库中的最小pg_class.relfrozenxid
。例如,如果在迫切模式下仅仅对表1做冻结处理,则不会更新该数据库的pg_database.datfrozenxid
,因为其他关系的pg_class.relfrozenxid
(当前数据库可见的其他表和系统视图)还没有发生变化,如图6.5(1)所示。如果当前数据库中的所有关系都以迫切模式冻结,则数据库的pg_database.datfrozenxid
就会被更新,因为此数据库的所有关系的pg_class.relfrozenxid
都被更新为当前的freezeLimit txid
,如图6.5(2)所示。
图6.5 pg_database.datfrozenxid
与pg_class.relfrozenxid
之间的关系
如何显示
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
选项带有
FREEZE
选项的VACUUM
命令会强制冻结指定表中的所有事务标识。虽然这是在迫切模式下执行的,但这里freezeLimit
会被设置为OldestXmin
(而不是OldestXmin - vacuum_freeze_min_age
)。 例如当txid=5000
的事务执行VACUUM FULL
命令,且没有其他正在运行的事务时,OldesXmin
会被设置为5000,而t_xmin
小于5000的元组将会被冻结。
6.3.3 改进迫切模式中的冻结过程
9.5或更早版本中的迫切模式效率不高,因为它始终会扫描所有页面。 比如在第6.3.2节的例子中,尽管第0页中所有元组都被冻结,但也会被扫描。
为了解决这一问题,9.6版本改进了可见性映射VM与冻结过程。如 第6.2.1节 所述,新VM包含着每个页面中所有元组是否都已被冻结的信息。在迫切模式下进行冻结处理时,可以跳过仅包含冻结元组的页面。
图6.6给出了一个例子。 根据VM中的信息,冻结此表时会跳过第0页。在更新完1号页面后,相关的VM信息会被更新,因为该页中所有的元组都已经被冻结了。
图6.6 冻结旧元组——迫切模式(9.6或更高版本)