Postgresql管理系列-第六章Vacuum Processing

vacuum操作是一个维护进程, 便于PostgreSQL的持久运行。它的两个主要任务是删除dead tuples和冻结事务id, 第5.10节中简要的提到了这两个问题。
为了删除dead tuples, vacuum提供了两种模式, 即并发vacuum和full vacuum。并发vacuum(通常简称 “VACUUM”)为表文件的每一页删除dead tuples, 而其他事务可以在此过程中读取表的数据。于此不同的, Full VACUUM不但删除dead tuples,还对整个文件进行碎片整理,full vacuum进行的时候,其他事务无法访问表。

尽管vacuum是必不可少的, 但是改进其功能与其他功能相比是缓慢的。例如, 在8.0 版之前,必须手动执行此过程 (使用psql实用程序或使用cron守护进程),直到2005年才有了autovacuum的功能.
由于vacuum涉及扫描整个表, 这是一个代价很高的的过程。在8.4版(2009年)中,引入了Visibility Map(VM), 以提高删除dead tuples的效率。在9.6版(2016)中, 通过增加的VM改进了冻结处理的效率.

6.1概述了并发VACUUM过程,后面的部分描述了下面的内容。

  • Visibility Map
  • 冻结处理
  • 移除不必要的clog文件
  • Autovacuum守护进程
  • Full VACUUM
6.1. 并发vacuum概述

vacuum为数据库中的所有表或者指定的表执行以下任务

  1. 删除dead tuples
    删除dead tuples并对每个页面的live tuples进行碎片整理
    删除指向dead tuples的index tuples
  2. 冻结老的txid
    在必要的情况下冻结老的txid
    更新冻结txid相关的系统目录(pg_database和pg_class)
    移除没有必要clog
  3. 其他
    更新已处理表的FSM和VM
    更新各自的统计信息(pg_stat_all_tables,etc)

如果读者熟悉以下术语: dead tuples, freezing txid, FSM, and the clog; 如果不知道, 参照第五章. 6.2介绍了VM.

以下伪代码描述vacuum处理的过程

 Pseudocode: Concurrent 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)       Truncate the last page if possible
(9)       Update both the statistics and system catalogs of the target table
           Release ShareUpdateExclusiveLock lock
       END FOR

        /* Post-processing */
(10)  Update statistics and system catalogs
(11)  Remove both unnecessary files and pages of the clog if possible

(1) 获取指定表.
(2) 获取ShareUpdateExclusiveLock锁。 这个锁允许其他事物读.
(3) 扫描所有page得到dead tuples,如果有必要冻结老的tuples.
(4) 删除指向dead tuples的index tuples
(5) 对表中的每一个page执行(6)和(7).
(6) 删除dead tuples,并在page中重新分派live tuples.
(7) 更新目标表相应的 FSM 和 VM截断最后一页.
(8) 如果最后一个page没有tuples,那么久就截断最后一个page
(9) 更新目标表vacuum处理相关的统计数据和系统目录。
(10) 更新vacuum相关的统计数据和系统目录
(11) 删除没有必要的文件和没有必要的clog pages

此伪代码有两个部分: 每个表的循环和Post-processing。内部循环可分为三个块。每个块都有单独的任务. 这三个块和Post-processing在下面概述。

6.1.1. 第一个块

这个块执行冻结处理,并且移除指向dead tuples的index tuples
首先,Postgresql扫描一个目标表,创建一个dead tuples列表,冻结old tuples,这个列表存储在maintenance_work_mem指定的本地内存中,冻结处理过程将在6.3描述
扫描后, PostgreSQL通过涉及到的dead tuples列表来删除index tuples。此过程在内部称为 “清理阶段”。当然, 这一过程代价高昂。在10版本以前, 清理阶段是一直有的。在版本11或更高版本中, 如果目标索引是B-tree, 则清理阶段是否执行由配置参数vacuum_cleanup_index_scale_factor决定。有关详细信息, 请参阅此参数的说明。

当maintenance_work_mem满了,而扫描没有完成完成的时候,Postgresql将继续处理下一个任务,即前面步骤的(4)到(7),然后返回第(3)步执行剩余扫描

6.1.2. 第二个块

这个块移除dead tuples,并逐页更新FSM和VM,图6.1是一个例子
图 6.1. 移除一个dead tuples
在这里插入图片描述

假定表包含三个page。我们关注0th page (即第一页)。此页有三个tuples。Tuple_2 是一个dead tuples (图 6.1 (1))。在这种情况下, PostgreSQL 将删除 Tuple_2 并重新排序剩余的tuples以修复碎片, 然后更新此页的 FSM 和 VM (图 6.1 (2))。PostgreSQL 将此过程一直持续到最后一页。

请注意, 不必要的行指针不会被删除, 它们将重复使用。因为, 如果删除行指针, 则必须更新关联索引的所有index tuples。

6.1.3. 第三个块

第三个块更新vacuum相关的每个目标表的统计数据和系统目录.
此外, 如果最后一页没有tuples, 它将从表文件中截断。

6.1.4. Post-processing

当vacuum完成后, PostgreSQL将更新与vacuum相关的几个统计数据和系统目录, 并在可能的情况下移除没有必要的clog部分 (第6.4 节)。

Vacuum使用ring buffer, 将在8.5描述; 因此, 处理后的pages不会缓存到shared buffers.

6.2. Visibility Map

vacuum是有代价的; 因此, 8.4版本被引入VM,以减少代价。

VM的基本概念很简单。每个表都有一个单独的可见性映射, 用于保持表文件中每个page的可见性。page的可见性决定了每个页面是否有dead tuples。vacuum可以跳过一个没有dead tuples的页面。

图6.2 说明了VM是如何使用的。假设表由三个page组成, 第0页和第2页都有dead tuples, 第1页没有。这个表的VM包含了哪些页面包含有dead tuples的信息。在这种情况下, vacuum依照VM的这些信息跳过第一页。
图 6.2. VM如何使用
在这里插入图片描述

每个VM由一个或多个8KB页组成, 此文件以 “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显示页面可见性以及有关tuples是否在每个页面中被冻结的信息(6.3.3 节)。

6.3. 冻结处理

冻结处理有两种模式, 根据特定条件, 在任一种模式下执行。为方便起见, 这些模式被称为lazy mode和eager mode

并发vacuum在内部通常被称为 “lazy vacuum”。然而, 本文档中定义的lazy mode是冻结处理执行的模式。
冻结处理通常在延迟模式下运行;但是, 当满足特定条件时, 将运行eager mode。
在lazy mode下, 冻结处理仅扫描目标表VM里包含dead tuples的页面。
相反, eager会扫描所有页面, 而不管页面是否包含dead tuples, 并且它还会更新与冻结处理相关的系统目录, 并在可能的情况下删除不必要的clog部分。
6.3.1部分和6.3.2分别描述了这些模式。6.3.3节描述了在eager下改进冻结过程的情况。

6.3.1 lazy mode

当开始冻结处理时,PostgreSQL会计算freeze limit txid,冻结t_xmin小于freeze limit txid的元组。

freezeLimit xid 计算公式:
freezeLimit_txid=(OldestXmin−vacuum_freeze_min_age)

OldestXmin在当前正在运行的事务中最老的txid。例如,当vacuum的时候,有三个事物在运行,(txids 100, 101, and 102),OldestXmin就是100。如果么有其他的事物,OldestXmin就是当前执行vacuum命令的txid,vacuum_freeze_min_age是一个配置参数 (默认50,000,000).

图6.3 显示了一个具体的示例。在这里, Table_1由三个页面组成, 每个页面有三个tuples。当执行vacuum时, 当前txid 为 50,002,500,并且没有其他事务。在这种情况下, OldestXmin为50,002,500;因此, 冻结限制的txid为2500。冻结处理按如下方式执行。
图 6.3. 在lazy mode冻结tuples.
在这里插入图片描述

0th page:
三个tuples全部冻结,因为所有的t_xid值都小于freezeLimit txid。另外,Tuple_1是dead tuples,在vacuum的时候被移除

1st page:
从VM可以看到,这个页面将被跳过

2nd page:
Tuple_7和Tuple_8被冻结; Tuple_7被移除.

在vacuum处理完成之前,将更新与vacuum有关的统计信息, 例如,pg_stat_all_tables表的n_live_tup, n_dead_tup, last_vacuum, vacuum_count列等等
如上所示, lazy mode可能无法完全冻tuples, 因为它可以跳过页面。

6.3.2. Eager Mode

eager模式弥补了lazy mode的缺陷。它扫描所有页面以检查表中的所有tuples, 更新相关的系统目录, 并在可能的情况下删除不必要的文件和clog页。

当满足以下条件的时,eager mode执行
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已插入第2页。执行VACUUM命令时, 当前txid为150,002,000, 并且没有其他事务。因此, OldestXmin为150,002,000, freezeLimit txid为100,002,000。在这种情况下, 上述条件是满意的, 因为 "1821<(150002000−150000000)"因此, 冻结处理在eager mode下执行, 如下所示。

图 6.4. eager mode下冻结老的tuple (9.5或者9.5之前).
在这里插入图片描述
0th page:
尽管所有tuples都被冻结, 但Tuple_2和Tuple_3都已检查。

1st page:
此页中的三个tuples已被冻结, 因为所有t_xmin值都小于冻结限制txid。请注意, 此页将在lazy mode下跳过。

2nd page:
Tuple_10被冻结.Tuple_11没有.

冻结每个表后, 将更新目标表的pg_class.relfrozenxid。pg_class是一个系统目录, 每个pg_class.relfrozenxid列都保存相应表的最新冻结xid。在本例中, Table_1’s pg_class.relfrozenxid更新为freezeLimit txid(即100,002,000), 这意味着在 Table_1中,t_xmin小于100,002,000的所有tuples都被冻结。

在完成vacuum过程之前, 如有必要, 将更新pg_database.datfrozenxid. pg_database.datfrozenxid列都包含相应数据库中的最小pg_class.relfrozenxid。例如, 如果只有Table_1在eager mode下被冻结, 则此数据库的pg_database.datfrozenxid不会更新, 因为其他对象(从当前数据库中可以看到的其他表和系统目录)的pg_class.relfrozenxid 没有更新(图 6.5 (1))。如果当前数据库中的所有relation都以eager mode冻结, 则会更新数据库的pg_database.datfrozenxid, 因为此数据库所有relations的pg_class.relfrozenxid都将更新到当前freezeLimit txid(图 6.5(2)).

图 6.5. pg_database.datfrozenxid和pg_class.relfrozenxid(s)的关系.
在这里插入图片描述
如何查看pg_class.relfrozenxid和pg_database.datfrozenxid
在以下例子中, 第一个查询显示"testdb"数据库中所有可见relations的relfrozenxids, 第二个查询显示"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命令强制冻结指定表中的所有txids。这是在eager mode下完成的;但是,freezeLimit设置为OldestXmin (而不是"OldestXmin - vacuum_freeze_min_age")。例如, 当由txid为5000的时候执行vacuum full命令, 并且没有其他正在运行的事务时, OldesXmin设置为5000,并且冻结小于5000的txid。

6.3.3 在eager mode下改进冻结处理

9.5版或更早版本中的eager mode效率并不高, 因为总是扫描所有页面。例如, 在 "节6.3.2 示例中, 即使第0号页面中的所有tuples都被冻结, 也会扫描第0页。
为了解决此问题, 在9.6 版中改进了VM和冻结过程。如6.2.1 节中所述, 新VM包含有关每个页面中是否冻结所有tuples的信息。在eager mode下执行冻结处理时, 可以跳过仅包含冻结tuples的页面。

图6.6 显示了一个示例。冻结这张表时, 通过VM的信息来跳过第0页。冻结第1页后, 将更新关联的VM信息, 因为此页的所有tuples都已冻结。

图 6.6. eager mode下冻结老的tuple (9.6或者9.6之后).
在这里插入图片描述

6.4. 移除没有必要的clog文件

第5.4 节中clog存放了事物状态。更新pg_database.datfrozenxid时, PostgreSQL会尝试删除不必要的clog文件。请注意, 相应的clog page也将被删除。

图6.7 显示了一个示例。如果pg_database.datfrozenxid的最小值包含在clog file’0002’中, 则可以删除掉更老的文件 (“0000” 和 “0001”), 因为在整个数据库集群中,存储在这些文件中的所有事务作为冻结txids被处理。

图 6.7. 移除没有必要的clog文件和page
在这里插入图片描述

pg_database.datfrozenxid和clog文件

下面展示了实际的pg_database.datfrozenxid和clog文件:

$ psql testdb -c "SELECT datname, datfrozenxid FROM pg_database"
  datname  | datfrozenxid 
-----------+--------------
 template1 |      7308883
 template0 |      7556347
 postgres  |      7339732
 testdb    |      7506298
(4 rows)

$ ls -la -h data/pg_clog/	# In version 10 or later, "ls -la -h data/pg_xact/"
total 316K
drwx------  2 postgres postgres   28 Dec 29 17:15 .
drwx------ 20 postgres postgres 4.0K Dec 29 17:13 ..
-rw-------  1 postgres postgres 256K Dec 29 17:15 0006
-rw-------  1 postgres postgres  56K Dec 29 17:15 0007
6.5. Autovacuum守护进程

vacuum已由守护程序自动执行;因此,PostgreSQL的操作变得非常简单。

autovacuum daemon周期性的调用多个autovacuum_worker进程。默认情况下, 它每1分钟唤醒一次(由autovacuum_naptime定义), 并调用三个work(默认由autovacuum_max_workers指定)。

在数据库影响最小的时候,由autovacuum调用的autovacuum work对每个的表逐步同时进行vacuum处理。

如何维护autovacuum

参考 “Tuning Autovacuum in PostgreSQL and Autovacuum Internals”.

6.6 full vacuum

虽然并发vacuum是必需的, 但这还不够。例如, 即使删除了许多dead tuples, 也无法减小表大小。

图6.8 显示了一个极端的示例。假设一个表由三个页面组成, 并且每个页面包含六个tuples。执行以下DELETE命令以删除元组, 并执行VACUUM命令以删除dead tuples:
在这里插入图片描述

testdb=# DELETE FROM tbl WHERE id % 6 != 0;
testdb=# VACUUM tbl;

dead tuples被移除;但是,表大小不会减小。这既是磁盘空间的浪费, 也对数据库性能产生负面影响。例如, 在上面的示例中, 当读取表中的三个tuples时, 必须从磁盘加载三个页面。

为了处理这种情况, PostgreSQL提供了full vacuum。图6.9描述了相关过程。

图 6.9. full vacuum概述.
在这里插入图片描述

[1] 创建新的表文件: 图 6.9(1)
当vacuum full在一个表上执行的时候,PostgreSQL首先获取表的 “AccessExclusiveLock” 锁,并创建一个大小为8KB新的表文件。“AccessExclusiveLock” 锁不允许访问这个表。

[2] 拷贝live tuples到新的表文件: 图 6.9(2)
PostgreSQL仅仅拷贝老的表文件中的live tuples到新表.

[3] 删除老的表文件,重建索引并且更新统计信息, FSM和VM: 图 6.9(3)
复制所有live tuples后, PostgreSQL 将删除旧文件, 重建所有关联的表索引, 更新此表的FSM和VM, 并更新相关的统计信息和系统目录。

full vacuum伪代码如下:

</>Pseudocode: 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

在使用full vacuum命令时, 应考虑两点.
当Full VACUUM正在处理时, 任何人都无法访问 (读写) 表.
最大会临时占用两个表的空间;因此, 在处理一个巨大的表时,有必要检查剩余的磁盘容量。

什么时候执行full vacuum?

遗憾的是, 没有执行"full vacuum"的最佳实践。但是, pg_freespacmap可能会给你一些建议

以下查询显示了你所想知道的表的平均freespace的比率

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)

结果是, 你发现还有一些可用空间
如果你几乎删除了所有tuples,并执行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)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值