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/

相關文章
相關標籤/搜索