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, 則清理階段是否執行由配置參數acuum_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計算凍結限制的txid和凍結的tuples(t_xid<凍結限制的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_works指定)。

在數據庫影響最小的時候,由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_freespacap可能會給你很好的建議

以下查詢顯示了你所想知道的表的平均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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章