PostgreSQL的MVCC(6)--Vacuum

In-page vacuum工做速度很快,但仅释放了一部分空间。它在一个表页内工做,而且不涉及索引。数据库

常规的vacuum操做是使用VACUUM命令完成的。数组

所以,vaccum是对整个表操做。它不只清除死元组,并且清除全部索引中对死元组的引用。缓存

vacuum与系统中的其余活动同时进行。表和索引能够按常规方式用于读取和更新(可是,不可能同时执行诸如CREATE INDEX,ALTER TABLE等命令)。app

仅是对那些发生过活动的表页执行vacuum,为了检测这些页,使用了*visibility map*(*visibility map*跟踪那些包含很是老的元组的页面,这些页面确定在全部数据快照中均可见)。仅处理那些*visibility map*未跟踪的页面,而且更新*visibility map*自己。dom

*free space map*也会在此过程当中进行更新,以反映页面中的额外可用空间。函数

和往常同样,让咱们建立一个表:post

=> CREATE TABLE vac(
  id serial,
  s char(100)
) WITH (autovacuum_enabled = off);
=> CREATE INDEX vac_s ON vac(s);
=> INSERT INTO vac(s) VALUES ('A');
=> UPDATE vac SET s = 'B';
=> UPDATE vac SET s = 'C';

咱们使用autovacuum_enabled参数来关闭自动清理过程。下次咱们将进行讨论,如今手动控制vacuum对于咱们的实验相当重要。url

该表如今具备三个元组,每一个元组都在索引中都有引用:spa

=> SELECT * FROM heap_page('vac',0);
 ctid  | state  |   xmin   |   xmax   | hhu | hot | t_ctid 
-------+--------+----------+----------+-----+-----+--------
 (0,1) | normal | 4000 (c) | 4001 (c) |     |     | (0,2)
 (0,2) | normal | 4001 (c) | 4002     |     |     | (0,3)
 (0,3) | normal | 4002     | 0 (a)    |     |     | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | ctid  
------------+-------
          1 | (0,1)
          2 | (0,2)
          3 | (0,3)
(3 rows)

在被vacuum以后,死元组被清除掉,只剩下一个活元组。索引中只剩下一个引用:操作系统

=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  |   xmin   | xmax  | hhu | hot | t_ctid 
-------+--------+----------+-------+-----+-----+--------
 (0,1) | unused |          |       |     |     | 
 (0,2) | unused |          |       |     |     | 
 (0,3) | normal | 4002 (c) | 0 (a) |     |     | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | ctid  
------------+-------
          1 | (0,3)
(1 row)

能够看到,前两个指针得到的状态是«unused»,而不是«dead»,它们将在in-page vacuum中得到这种状态。

PostgreSQL如何肯定哪些元组能够视为是dead状态? 在讨论数据快照时,咱们已经谈到了事务范围的概念,可是重申这一重要问题并无什么坏处。

让咱们再次开始上一个实验。

=> TRUNCATE vac;
=> INSERT INTO vac(s) VALUES ('A');
=> UPDATE vac SET s = 'B';

可是在再次更新行以前,让另外一个事务开始(而不是结束)。

在本例中,它将使用Read Committed级别,可是它必须得到一个真实的(而不是虚拟的)事务号。例如,事务能够改变甚至锁定任何表中的某些行,而不是强制性在表vac:

|  => BEGIN;
|  => SELECT s FROM t FOR UPDATE;
|    s  
|  -----
|   FOO
|   BAR
|  (2 rows)
=> UPDATE vac SET s = 'C';

如今表中有三行,索引中有三个引用。vacuum后会发生什么?

=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  |   xmin   |   xmax   | hhu | hot | t_ctid 
-------+--------+----------+----------+-----+-----+--------
 (0,1) | unused |          |          |     |     | 
 (0,2) | normal | 4005 (c) | 4007 (c) |     |     | (0,3)
 (0,3) | normal | 4007 (c) | 0 (a)    |     |     | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | ctid  
------------+-------
          1 | (0,2)
          2 | (0,3)
(2 rows)

表中还剩下两个元组:VACUUM决定(0,2)元组还不能被清理。缘由确定是在数据库的事务范围内,在此示例中,这是由未完成的事务肯定的:

|  => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
|   backend_xmin 
|  --------------
|           4006
|  (1 row)

咱们可让vacuum报告正在发生的事情:

=> VACUUM VERBOSE vac;
INFO:  vacuuming "public.vac"
INFO:  index "vac_s" now contains 2 row versions in 2 pages
DETAIL:  0 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO:  "vac": found 0 removable, 2 nonremovable row versions in 1 out of 1 pages
DETAIL:  1 dead row versions cannot be removed yet, oldest xmin: 4006
There were 1 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM

·2个不可删除的行版本-在表中找到两个没法删除的元组。
·1个死行版本尚没法删除-其中一个已死。
·最旧的xmin显示当前范围。

让咱们重申一下结论:若是数据库的事务持续时间很长(未完成或执行的时间很长),则不管vacuum发生的频率如何,均可能致使表膨胀。 所以,**OLTP型和OLAP型工做负载很难在一个PostgreSQL数据库中共存**:运行数小时的报表不会让更新后的表被适当清理。建立用于报表目的单独副本多是解决此问题的方法。

在完成未事务后,范围移动,状况获得了解决:

|  => COMMIT;
=> VACUUM VERBOSE vac;
INFO:  vacuuming "public.vac"
INFO:  scanned index "vac_s" to remove 1 row versions
DETAIL:  CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
INFO:  "vac": removed 1 row versions in 1 pages
DETAIL:  CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
INFO:  index "vac_s" now contains 1 row versions in 2 pages
DETAIL:  1 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO:  "vac": found 1 removable, 1 nonremovable row versions in 1 out of 1 pages
DETAIL:  0 dead row versions cannot be removed yet, oldest xmin: 4008
There were 1 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM

如今,页面中只剩下行最新的、实时的版本:

=> SELECT * FROM heap_page('vac',0);
 ctid  | state  |   xmin   | xmax  | hhu | hot | t_ctid 
-------+--------+----------+-------+-----+-----+--------
 (0,1) | unused |          |       |     |     | 
 (0,2) | unused |          |       |     |     | 
 (0,3) | normal | 4007 (c) | 0 (a) |     |     | (0,3)
(3 rows)

索引也只有一行:

=> SELECT * FROM index_page('vac_s',1);
 itemoffset | ctid  
------------+-------
          1 | (0,3)
(1 row)

内部细节

vacuum必须同时处理表和索引,以避免锁定其余进程。怎么作呢?

全部步骤都从scanning heap阶段开始(将visible map考虑在内)。在读取的页面中,检测到死元组,将其tid写到专用数组中。数组存储在vacuum进程的本地内存中,在该进程中为其分配了maintenance_work_mem字节的内存。此参数的默认值为64MB,不是按需分配。可是,若是表不大,则会分配较少的内存。

而后,咱们要么到达表的末尾,要么为数组分配的内存结束了。在任何一种状况下,vacuuming indexes阶段都会开始。为此,将对表上建立的每一个索引进行彻底扫描,以查找引用记住的元组的行。找到的行将从索引页清除。

在这里,咱们面临如下问题:索引已经没有对死元组的引用,而表中仍然有它们。可是这并没啥问题:执行查询时,咱们要么不命中死元组(具备索引访问权限),要么在可见性检查中不命中它们(扫描表时)。

此后,vacuuming heap阶段开始。再次扫描该表以读取适当的页面,将已记住的元组清除它们并释放指针。由于再也不有索引引用,因此咱们能够这样作。

若是在第一个周期中未彻底读取该表,则会清空数组,并从到达的位置重复全部操做。

综上所述:

·表始终被扫描两次。 ·若是清理删除了太多的元组,以至它们都没法容纳在大小为maintenance_work_mem的内存中,则全部索引将根据须要进行屡次扫描。

对于大表,这可能须要不少时间,并会增长至关大的系统负载。 固然,查询不会被锁定,可是有额外的输入/输出。

为了加快处理速度,能够更频繁地调用VACUUM(这样就不会每次清理掉太多的元组),或者分配更多的内存。

从版本11开始,PostgreSQL能够跳过索引扫描,除非迫切须要,不然不建议。

监控

咱们如何肯定VACUUM没法在一个周期内完成其工做?

咱们已经看到了第一种方法:使用VERBOSE选项调用VACUUM命令。 在这种状况下,有关过程阶段的信息将输出到控制台。

其次,从9.6版开始,可使用pg_stat_progress_vacuum视图,该视图还提供了全部必要的信息。

(第三种方法也是可用的:将信息输出到消息日志,但这仅适用于autovacuum,这将在下次讨论。)

让咱们在表中插入不少行,以使vacuum过程持续很长时间,并更新全部这些行,以使VACUUM能够完成工做。

=> TRUNCATE vac;
=> INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);
=> UPDATE vac SET s  = 'B';

让咱们减小分配给标识符数组的内存大小:

=> ALTER SYSTEM SET maintenance_work_mem = '1MB';
=> SELECT pg_reload_conf();

让咱们开始VACUUM,当它工做时,让咱们访问pg_stat_progress_vacuum视图几回:

=> VACUUM VERBOSE vac;

  

|  => SELECT * FROM pg_stat_progress_vacuum \gx
|  -[ RECORD 1 ]------+------------------
|  pid                | 6715
|  datid              | 41493
|  datname            | test
|  relid              | 57383
|  phase              | vacuuming indexes
|  heap_blks_total    | 16667
|  heap_blks_scanned  | 2908
|  heap_blks_vacuumed | 0
|  index_vacuum_count | 0
|  max_dead_tuples    | 174762
|  num_dead_tuples    | 174480

  

|  => SELECT * FROM pg_stat_progress_vacuum \gx
|  -[ RECORD 1 ]------+------------------
|  pid                | 6715
|  datid              | 41493
|  datname            | test
|  relid              | 57383
|  phase              | vacuuming indexes
|  heap_blks_total    | 16667
|  heap_blks_scanned  | 5816
|  heap_blks_vacuumed | 2907
|  index_vacuum_count | 1
|  max_dead_tuples    | 174762
|  num_dead_tuples    | 174480

  

咱们能够看到:

​ ·当前阶段的名称-咱们讨论了三个主要阶段,但整体上有更多阶段。
​ ·表页的总数(heap_blks_total)。
​ ·扫描页数(heap_blks_scanned)。
​ ·已清除的页面数(heap_blks_vacuumed)。
​ ·index vacuum cycles数(index_vacuum_count)。



整体进度由heap_blks_vacuumed与heap_blks_total之比肯定,但咱们应考虑到此值因为扫描索引而以较大的增量而不是平滑的方式变化。 可是,更应该注意的是vacuum cycles的次数:数字大于1表示分配的内存不足以在一个循环中完成vacuum。

VACUUM VERBOSE命令的输出:

INFO:  vacuuming "public.vac"
INFO:  scanned index "vac_s" to remove 174480 row versions
DETAIL:  CPU: user: 0.50 s, system: 0.07 s, elapsed: 1.36 s
INFO:  "vac": removed 174480 row versions in 2908 pages
DETAIL:  CPU: user: 0.02 s, system: 0.02 s, elapsed: 0.13 s
INFO:  scanned index "vac_s" to remove 174480 row versions
DETAIL:  CPU: user: 0.26 s, system: 0.07 s, elapsed: 0.81 s
INFO:  "vac": removed 174480 row versions in 2908 pages
DETAIL:  CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.10 s
INFO:  scanned index "vac_s" to remove 151040 row versions
DETAIL:  CPU: user: 0.13 s, system: 0.04 s, elapsed: 0.47 s
INFO:  "vac": removed 151040 row versions in 2518 pages
DETAIL:  CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.08 s
INFO:  index "vac_s" now contains 500000 row versions in 17821 pages
DETAIL:  500000 index row versions were removed.
8778 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO:  "vac": found 500000 removable, 500000 nonremovable row versions in 16667 out of 16667 pages
DETAIL:  0 dead row versions cannot be removed yet, oldest xmin: 4011
There were 0 unused item pointers.
0 pages are entirely empty.
CPU: user: 1.10 s, system: 0.37 s, elapsed: 3.71 s.
VACUUM

  

咱们能够看到在索引上完成了三个循环,而且在每一个循环中,vacuum了指向死元组的174480个指针。为何是这个数字? 一个tid占用6个字节,而1024 * 1024/6 = 174762,这是咱们在pg_stat_progress_vacuum.max_dead_tuples中看到的数字。 实际上,可能会使用更少:这确保了在读取下一页时,全部指向无效元组的指针确定会容纳在内存中。

分析

analysis,或者换句话说,为查询计划器收集统计信息,在形式上与vacuum彻底无关。但咱们不只可使用analyze命令进行分析,还可使用VACUUM ANALYZE将vacuum与analyze结合起来进行分析。

但正如咱们稍后将看到的,autovacuum 和automatic analysis是在一个过程当中完成的,并以相似的方式进行控制。

VACUUM FULL

vacuum比in-page vacuum释放更多的空间,但仍不能彻底解决问题。

若是因为某种缘由,表或索引的大小增长了不少,VACUUM将释放现有页面内的空间:页内将出现«holes»,以后能够用于插入新的元组。可是页数不会改变,所以,从操做系统的角度来看,文件将占用与清理以前彻底相同的空间。这是很差的,由于:

·对表(或索引)的彻底扫描速度变慢。 ·可能须要更大的缓冲区高速缓存(由于页面存储在其中,有用信息的密度下降了)。 ·在索引树中,索引深度加深,这将减慢索引访问。 ·这些文件在磁盘和备份副本中会占用额外的空间。

(惟一的例外是位于文件末尾的彻底清除的页面。这些页面已从文件中裁剪并返回给操做系统)

若是文件中有用信息的比例低于某个合理的限制,则管理员能够对表进行VACUUM FULL。在这种状况下,该表及其全部索引都是从头开始重建的,而且数据以最紧凑的方式打包(固然,考虑了fillfactor参数)。在重建过程当中,PostgreSQL首先重建表,而后重建每一个索引。对于每一个对象,将建立新文件,并在重建结束时删除旧文件。咱们应该考虑到在此过程当中将须要额外的磁盘空间。

为了说明这一点,让咱们再次在表中插入必定数量的行:

=> TRUNCATE vac;
=> INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);

咱们如何估计数据的密度呢? 为此,使用专门的扩展很方便:

=> CREATE EXTENSION pgstattuple;
=> SELECT * FROM pgstattuple('vac') \gx
-[ RECORD 1 ]------+---------
table_len          | 68272128
tuple_count        | 500000
tuple_len          | 64500000
tuple_percent      | 94.47
dead_tuple_count   | 0
dead_tuple_len     | 0
dead_tuple_percent | 0
free_space         | 38776
free_percent       | 0.06

该函数读取整个表并显示统计信息:哪些数据占据了文件中的空间。 如今,咱们感兴趣的主要信息是tuple_percent字段:有用数据的百分比。 因为页面内不可避免的信息开销,它小于100,但仍然很高。

对于索引,将输出不一样的信息,可是avg_leaf_density字段具备相同的含义:有用信息的百分比(在叶子页面中)。

=> SELECT * FROM pgstatindex('vac_s') \gx
-[ RECORD 1 ]------+---------
version            | 3
tree_level         | 3
index_size         | 72802304
root_block_no      | 2722
internal_pages     | 241
leaf_pages         | 8645
empty_pages        | 0
deleted_pages      | 0
avg_leaf_density   | 83.77
leaf_fragmentation | 64.25

再看看表和索引大小:

=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
  pg_size_pretty(pg_indexes_size('vac')) index_size;
 table_size | index_size 
------------+------------
 65 MB      | 69 MB
(1 row)

如今咱们删除90%的行。咱们随机选择要删除的行,这样至少有一行极可能会保留在每一个页面中:

=> DELETE FROM vac WHERE random() < 0.9;
DELETE 450189

vacuum后表的大小是多少?

=> VACUUM vac;
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
  pg_size_pretty(pg_indexes_size('vac')) index_size;
 table_size | index_size 
------------+------------
 65 MB      | 69 MB
(1 row)

咱们能够看到,大小没有改变:vacuum没有办法缩小文件的大小。尽管信息密度降低了大约10倍:

=> SELECT vac.tuple_percent, vac_s.avg_leaf_density
FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
 tuple_percent | avg_leaf_density 
---------------+------------------
          9.41 |             9.73
(1 row)

如今让咱们看看vacuum full后咱们获得了什么。如今表和索引使用如下文件:

=> SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
 pg_relation_filepath | pg_relation_filepath 
----------------------+----------------------
 base/41493/57392     | base/41493/57393
(1 row)

  

=> VACUUM FULL vac;
=> SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
 pg_relation_filepath | pg_relation_filepath 
----------------------+----------------------
 base/41493/57404     | base/41493/57407
(1 row)

这些文件如今被替换为新文件。表和索引的大小明显减少,信息密度相应增大:

=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
  pg_size_pretty(pg_indexes_size('vac')) index_size;
 table_size | index_size 
------------+------------
 6648 kB    | 6480 kB
(1 row)

  

=> SELECT vac.tuple_percent, vac_s.avg_leaf_density
FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
 tuple_percent | avg_leaf_density 
---------------+------------------
         94.39 |            91.08
(1 row)

  

请注意,索引中的信息密度甚至大于原始信息。从可用数据重建索引(B树)比将数据逐行插入现有索引中更为有利。

咱们使用的pgstattuple扩展功能读取了整个表。可是,若是表很大,这将很不方便,所以扩展具备pgstattuple_approx函数,该函数会跳过可见性图中标记的页面并显示近似数字。

另外一种方法,但准确性更低,是使用系统目录粗略估计数据大小与文件大小的比率。你能够在Wiki中找到此类查询的示例。

VACUUM FULL不能用于常规用途,由于它在整个过程当中都禁止使用表进行任何工做(包括查询)。显然,对于负载很高的系统,这彷佛是不可接受的。锁将单独讨论,如今咱们仅说起pg_repack扩展,该扩展在工做结束时仅将表锁定一小段时间。

相似的命令

有一些命令也能够彻底重建表和索引,所以相似于VACUUM FULL。它们所有彻底阻止了该表的任何工做,它们都删除了旧数据文件并建立了新文件。

CLUSTER命令与VACUUM FULL彻底类似,但它实际上还会根据可用索引对元组进行排序。这使计划器在某些状况下能够更有效地使用索引访问。可是咱们应该记住,不能clustering是不被维护的:元组的物理顺序将随着表的后续更改而中断。

REINDEX命令在表上重建一个单独的索引。VACUUM FULL和CLUSTER实际上使用此命令来重建索引。

TRUNCATE命令的逻辑相似于DELETE的逻辑—它删除全部表行。可是,正如已经提到的,DELETE只将元组标记为已删除,这须要进一步清理。而TRUNCATE只是建立一个新的干净文件。一般,这会更快地工做,可是咱们应该注意,TRUNCATE会阻塞对表的任何工做,直到事务结束。

 

原文地址:

https://habr.com/en/company/postgrespro/blog/484106/

相关文章
相关标签/搜索