PostgreSQL 的清理過程

本文是《PostgreSQL指南--內(nèi)幕探索》(鈴木啟修著 馮若航 劉陽明 張文升譯)的讀書筆記,僅供自己學(xué)習(xí)使用,請勿轉(zhuǎn)載。這是一本好書,如有需要請直接購買書籍。

清理過程(通常簡稱為VACUUM)是一種維護(hù)過程,有助于 PostgreSQL 的持久運(yùn)行。它的兩個(gè)主要任務(wù)是刪除死元組,以及凍結(jié)事務(wù)標(biāo)識(shí)。

為了移除死元組,清理過程有兩種模式,分別是并發(fā)清理與完整清理。并發(fā)清理過程會(huì)刪除表文件每個(gè)頁面中的死元組,而其他事務(wù)可以在其運(yùn)行時(shí)繼續(xù)讀取該表。相反,完整清理不僅會(huì)移除整個(gè)文件中所有的死元組,還會(huì)對整個(gè)文件中所有的活元組進(jìn)行碎片整理。其他事務(wù)在完整清理運(yùn)行時(shí)無法訪問該表。

盡管清理過程對PostgreSQL至關(guān)重要,但與其他功能相比,它的改進(jìn)相對其他功能而言要慢一些。例如在8.0版本之前,清理過程必須手動(dòng)執(zhí)行(通過psql 實(shí)用程序或使用 cron 守護(hù)進(jìn)程)。直到2005年實(shí)現(xiàn)了autovacuum 守護(hù)進(jìn)程時(shí),這一過程才實(shí)現(xiàn)了自動(dòng)化。

由于清理過程涉及全表掃描,因此該過程代價(jià)高昂。在版本8.4(2009)中引入了可見性映射( Visibility Map,VM)來提高移除死元組的效率。在版本9.6(2016)中增強(qiáng)了 VM,從而改善了凍結(jié)過程的表現(xiàn)。

1. 并發(fā)清理概述

清理過程為指定的表或數(shù)據(jù)庫中的所有表執(zhí)行以下任務(wù)。

1.移除死元組和對活元組進(jìn)行碎片整理。

  • 移除每一頁中的死元組,并對每一頁內(nèi)的活元組進(jìn)行碎片整理。
  • 移除指向死元組的索引元組。

2.凍結(jié)舊的事務(wù)標(biāo)識(shí)。

  • 如有必要,凍結(jié)舊元組的事務(wù)標(biāo)識(shí)。
  • 更新與凍結(jié)事務(wù)標(biāo)識(shí)相關(guān)的系統(tǒng)視圖( pg_database 與 pg_class)。
  • 如果可能,移除不必要的提交日志文件。

3.其他。

  • 更新已處理表的空閑空間映射(FSM)和可見性映射(VM)。
  • 更新一些統(tǒng)計(jì)信息( pg_stat_all_tables 等)。

以下偽代碼描述了清理的過程。

    (1)      FOR each table
    (2)          在目標(biāo)表上獲取 ShareUpdateExclusiveLock 鎖 /* 允許其他事務(wù)對該表進(jìn)行讀取 */

                 /* 第一部分 */
    (3)          掃描所有頁面,定位死元組,如有必要,凍結(jié)過早的元組
    (4)          如果存在,移除指向死元組的索引元組

                 /* 第二部分 */
    (5)          FOR each page of the table
    (6)               移除死元組,重排本頁內(nèi)的活元組
    (7)               逐頁更新目標(biāo)表頁對應(yīng)的 FSM 與 VM
                 END FOR

                   /* 第三部分 */
      (8)          如果最后一個(gè)頁面沒有任何元組,截?cái)嘧詈蟮捻撁?      (9)          更新系統(tǒng)數(shù)據(jù)字典與統(tǒng)計(jì)信息
                   釋放ShareUpdateExclusiveLock鎖
              END FOR

              /* 后續(xù)處理 */
      (10)     更新統(tǒng)計(jì)信息與系統(tǒng)數(shù)據(jù)字典
      (11)     如果可能,移除非必要的文件及CLOG中的文件

該偽碼分為兩大塊:一塊是依次處理表的循環(huán),一塊是后處理邏輯。而循環(huán)塊又分為三個(gè)部分,每一個(gè)部分都有各自的任務(wù)。接下來會(huì)描述這三個(gè)部分及后處理的邏輯。

1.1 第一部分

這一部分執(zhí)行凍結(jié)處理,并刪除指向死元組的索引元組。

首先,PostgreSQL 掃描目標(biāo)表以構(gòu)建死元組列表,如果可能的話,還會(huì)凍結(jié)舊元組。該列表存儲(chǔ)在本地內(nèi)存中的 maintenance_work_mem 里(維護(hù)用的工作內(nèi)存)。凍結(jié)過程將在第 3 節(jié)中介紹。

掃描完成后,PostgreSQL 根據(jù)構(gòu)建得到的死元組列表來刪除索引元組。該過程在內(nèi)部被稱為“清除階段”。不用說,該過程代價(jià)高昂。在 10.0 或更低版本中始終會(huì)執(zhí)行清除階段。在 11.0 或更高版本中,如果目標(biāo)索引是B樹,是否執(zhí)行清除階段由配置參數(shù) vacuum_cleanup_index_scale_factor 決定。詳細(xì)信息請參考此參數(shù)的說明。

當(dāng) maintenance_work_mem 已滿,且未完成全部掃描時(shí),PostgreSQL繼續(xù)進(jìn)行后續(xù)任務(wù),即步驟(4)到(7),完成后再重新返回步驟(3)并繼續(xù)掃描。

1.2 第二部分

這一部分會(huì)移除死元組,并逐頁更新 FSM 和 VM。

刪除死元組.png

? 圖 1 刪除死元組

假設(shè)該表包含三個(gè)頁面,首先關(guān)注 0 號(hào)頁面(即第一個(gè)頁面),該頁面包含三條元組, 其中 Tuple_2 是一條死元組,如圖 1(1)所示。在這里PostgreSQL 移除了 Tuple_2,并重排剩余元組來整理碎片空間,然后更新該頁面的 FSM 和 VM,如圖 1(2)所示。PostgreSQL 不斷重復(fù)該過程直至最后一頁。注意,非必要的行指針是不會(huì)被移除的,它們會(huì)在將來被重用。因?yàn)槿绻瞥诵兄羔槪捅仨毻瑫r(shí)更新所有相關(guān)索引中的索引元組。

1.3 第三部分

第三部分會(huì)針對每個(gè)表,更新與清理過程相關(guān)的統(tǒng)計(jì)信息和系統(tǒng)視圖。此外,如果最后一頁中沒有元組,則該頁會(huì)從表文件中被截?cái)唷?/p>

1.4 后續(xù)處理

當(dāng)處理完成后,PostgreSQL 會(huì)更新與清理過程相關(guān)的幾個(gè)統(tǒng)計(jì)數(shù)據(jù),以及相關(guān)的系統(tǒng)視圖;如果可能的話,它還會(huì)移除部分不必要的 CLOG 文件,見第 4 節(jié)。

2. 可見性映射

為了能加快VACUUM查找包含無效元組的文件塊的過程,PG 為每個(gè)表文件設(shè)置了一個(gè)附屬文件 ——— 可見性映射表。 可見性映射在 9.6 版中進(jìn)行了加強(qiáng),以提高凍結(jié)處理的效率。新的 VM 除了顯示頁面可見性之外,還包含了頁面中元組是否全部凍結(jié)的信息。 VM 中為表的每一個(gè)文件塊(Page)設(shè)置了一位,用來標(biāo)記該文件塊是否包含無效元組。對于包含無效元組的文件塊,VACUUM有兩種方式處理,即快速清理(Lazy VACUUM)和完全清理(Full VACUUM)。
注意,VM 文件僅在 Lazy VACUUM 操作中被用到,F(xiàn)ull VACUUM 由于要跨塊清理等復(fù)雜操作,需要對整個(gè)表文件進(jìn)行掃描,所以 VM 文件此時(shí)作用不大。

2.1 結(jié)構(gòu)分析

對于每個(gè)表文件,其對應(yīng)的VM文件命名為:“關(guān)系表OID_vm”。對該文件的操作在 visibility_map.c 文件中進(jìn)行了定義。
與其他文件一樣, VM文件也被劃分為若干個(gè)文件塊(簡稱VM塊)。VM塊中除了必要的標(biāo)記信息外,其他的每一位都對應(yīng)于一個(gè)表塊,當(dāng)表塊中所有元組都對當(dāng)前事務(wù)可見時(shí),表塊對應(yīng)的位才被設(shè)置為1。 其文件結(jié)構(gòu)如下所示:

圖3-10.png

當(dāng)標(biāo)志位為1時(shí),VACUUM會(huì)忽略掃描對應(yīng)的表塊,所以能大大提高VACUUM的效率。由于VM文件不跟蹤索引,所以對索引的操作還是需要完全掃描。

3. 凍結(jié)過程

凍結(jié)過程有兩種模式,依特定條件而擇其一執(zhí)行。為方便起見,我們將這兩種模式分別稱為惰性模式和迫切模式。

并發(fā)清理通常在內(nèi)部被稱為“惰性清理”。但是,本文中定義的惰性模式是凍結(jié)過程執(zhí)行的模式。

凍結(jié)過程通常以惰性模式運(yùn)行,但當(dāng)滿足特定條件時(shí),也會(huì)以迫切模式運(yùn)行。在惰性模式下,凍結(jié)過程僅使用目標(biāo)表對應(yīng)的VM掃描包含死元組的頁面。迫切模式相則反,它會(huì)掃描所有的頁面,無論其是否包含死元組,都會(huì)更新與凍結(jié)過程相關(guān)的系統(tǒng)視圖,并在可能的情況下刪除不必要的CLOG文件。

3.1 惰性模式

惰性模式當(dāng)開始凍結(jié)處理時(shí), PostgreSQL 計(jì)算 freezeLimit_txid ,并凍結(jié) t_xmin 小于 freezeLimit_txid 的元組。freezeLimit_txid定義如下:

freezeLimit_txid = ( OldestXmin - vacuum_freeze_min_age )

OldestXmin 是當(dāng)前正在運(yùn)行的事務(wù)中最早的事務(wù)標(biāo)識(shí)。舉個(gè)例子,如果在執(zhí)行VACUUM命令時(shí),還有其他三個(gè)事務(wù)正在運(yùn)行,且其txid分別為100、101和102,那么 OldestXmin 就是 100。如果不存在其他事務(wù),OldestXmin 就是執(zhí)行此 VACUUM 命令的事務(wù)標(biāo)識(shí)。這里vacuum_freeze_min_age是一個(gè)配置參數(shù)(默認(rèn)值為50 000 000)。

圖 2 給出了一個(gè)具體的例子。Table_1 由三個(gè)頁面組成,每個(gè)頁面包含三條元組。執(zhí)行VACUUM命令時(shí),當(dāng)前txid為50 002 500且沒有其他事務(wù)。在這種情況下,OldestXmin就是50 002 500,因此freezeLimit_txid為2500。凍結(jié)過程按照如下步驟執(zhí)行。

凍結(jié)元組——惰性模式.png

? 圖2 凍結(jié)元組 -- 惰性模式

第0頁:

三條元組被凍結(jié),因?yàn)樗性M的 t_xmin 值都小于 freezeLimit_txid。此外,因?yàn)門uple_1是一條死元組,所以在該清理過程中被移除。

第1頁:

通過引用可見性映射(從VM中發(fā)現(xiàn)該頁面所有元組都可見),清理過程跳過了對該頁面的清理。

第2頁:

Tuple_7和Tuple_8被凍結(jié),且Tuple_7被移除。

在完成清理過程之前,與清理相關(guān)的統(tǒng)計(jì)數(shù)據(jù)會(huì)被更新,例如 pg_stat_all_tables視圖中的n_live_tup、n_dead_tup、last_vacuum、vacuum_count等字段。

如上例所示,因?yàn)槎栊阅J娇赡軙?huì)跳過頁面,它可能無法凍結(jié)所有需要凍結(jié)的元組。

這里補(bǔ)充一個(gè)長事務(wù)的例子:

數(shù)據(jù)庫運(yùn)行一個(gè)長事務(wù),很久沒有提交導(dǎo)致current_oldest_xmin一直不會(huì)超過vacuum_freeze_min_age,vacuum不會(huì)凍結(jié)任何元組。這樣最低的xmin就和當(dāng)前最新的xmin的距離越來越遠(yuǎn),差值慢慢接近20億,這時(shí)候數(shù)據(jù)庫為保證數(shù)據(jù)不丟失,會(huì)有告警甚至宕機(jī)。

告警

WARNING:  database "mydb" must be vacuumed within 177009986 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in "mydb".

宕機(jī)

ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
HINT:  Stop the postmaster and vacuum that database in single-user mode.

3.2 迫切模式(9.5或更低版本)

迫切模式彌補(bǔ)了惰性模式的缺陷。它會(huì)掃描所有頁面,檢查表中的所有元組,更新相關(guān)的系統(tǒng)視圖,并在可能時(shí)刪除不必要的CLOG文件與頁面。當(dāng)滿足以下條件時(shí),會(huì)執(zhí)行迫切模式。

pg_database.datfrozenxid < ( OldestXmin - vacuum_freeze_table_age)

在上面的條件中,pg_database.datfrozenxid 是系統(tǒng)視圖 pg_database 中的列,并保存著每個(gè)數(shù)據(jù)庫中最老的已凍結(jié)的事務(wù)標(biāo)識(shí),細(xì)節(jié)將在后面描述。這里我們假設(shè)所有 pg_database.datfrozenxid 的值都是1821(這是在9.5版本中安裝新數(shù)據(jù)庫集群之后的初始值)。vacuum_freeze_table_age 是配置參數(shù)(默認(rèn)為150 000 000)。

圖 3 給出了一個(gè)具體的例子。在表 1 中,Tuple_1 和 Tuple_7都已經(jīng)被刪除,Tuple_10和 Tuple_11 則已經(jīng)插入第 2 頁中。執(zhí)行 VACUUM 命令時(shí)的事務(wù)標(biāo)識(shí)為 150 002 000,且沒有其他事務(wù)。因此,OldestXmin = 150 002 000,freezeLimit_txid = OldestXmin - vacuum_freeze_min_age =(150 002 000 - 50 000 000)=100 002 000。在這種情況下滿足了上述條件:因?yàn)?821 < (150 002 000 - 150 000 000),所以凍結(jié)過程會(huì)以迫切模式執(zhí)行,如下所示。注意,這里是 9.5 或更低版本的行為,最新版本的行為將在第 3.3 節(jié)中描述。

凍結(jié)舊元組——迫切模式(9.5或更低版本).png

? 圖3 凍結(jié)舊元組——迫切模式(9.5或更低版本)

第0頁:即使所有元組都被凍結(jié),也會(huì)檢查 Tuple_2 和 Tuple_3。

第1頁:此頁面中的三條元組都會(huì)被凍結(jié),因?yàn)樗性M的 t_xmin 值都小于 freezeLimit_txid。注意,在惰性模式下會(huì)跳過此頁面。

第2頁: 將 Tuple_10凍結(jié),而 Tuple_11 沒有凍結(jié)。

凍結(jié)完一張表的所有元組后,更新系統(tǒng)視圖 pg_class 的 relfrozenxid 為 freezeLimit_txid:

凍結(jié)一張表后,目標(biāo)表的 pg_class.relfrozenxid 將被更新。pg_class是一個(gè)系統(tǒng)視圖,每個(gè)pg_class.relfrozenxid 列都保存著相應(yīng)表的最近凍結(jié)的事務(wù)標(biāo)識(shí)。本例中表1的 pg_class.relfrozenxid 會(huì)被更新為當(dāng)前的 freezeLimit_txid(即100 002000),這意味著表 1 中 t_xmin 小于100 002 000 的所有元組都已被凍結(jié)。

如果當(dāng)前數(shù)據(jù)庫中的所有關(guān)系都以迫切模式凍結(jié),則更新此數(shù)據(jù)庫的pg_database. datfrozenxid

在完成清理過程之前,必要時(shí)會(huì)更新 pg_database.datfrozenxid。每個(gè) pg_database. datfrozenxid 列都包含相應(yīng)數(shù)據(jù)庫中的最小 pg_class.relfrozenxid。如果在迫切模式下僅僅對表 1 做凍結(jié)處理,則不會(huì)更新該數(shù)據(jù)庫的 pg_database. datfrozenxid,因?yàn)槠渌P(guān)系的 pg_class.relfrozenxid(當(dāng)前數(shù)據(jù)庫可見的其他表和系統(tǒng)視圖)還沒有發(fā)生變化,如圖4(1)所示。如果當(dāng)前數(shù)據(jù)庫中的所有關(guān)系都以迫切模式凍結(jié),則數(shù)據(jù)庫的 pg_database. datfrozenxid 就會(huì)被更新,因?yàn)榇藬?shù)據(jù)庫的所有關(guān)系的 pg_class.relfrozenxid 都被更新為當(dāng)前的 freezeLimit_txid,如圖4(2)所示。

pg_database.datfrozenxid與pg_class.relfrozenxid之間的關(guān)系.png

? 圖4 pg_database.datfrozenxid 與 pg_class.relfrozenxid 之間的關(guān)系

如何查詢 pg_class.relfrozenxidpg_database.datfrozenxid?

    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選項(xiàng):

帶有 FREEZE 選項(xiàng)的 VACUUM 命令會(huì)強(qiáng)制凍結(jié)指定表中的所有事務(wù)標(biāo)識(shí)。雖然這是在迫切模式下執(zhí)行的,但是這里 freezeLimit 會(huì)被設(shè)置為 OldestXmin 而不是OldestXmin -vacuum_freeze_min_age。例如,當(dāng)txid=5000的事務(wù)執(zhí)行 VACUUM FULL 命令,且沒有其他正在運(yùn)行的事務(wù)時(shí),OldesXmin 會(huì)被設(shè)置為 5000,而t_xmin 小于 5000的元組將會(huì)被凍結(jié)。

3.3 改進(jìn)迫切模式中的凍結(jié)過程(9.6版本及更高版本)

9.5或更低版本中的迫切模式效率不高,因?yàn)樗冀K會(huì)掃描所有頁面。比如在第 3.2 節(jié)的例子中,盡管第0頁中所有元組都被凍結(jié),但還是會(huì)被掃描。

為了解決這一問題,9.6版本改進(jìn)了可見性映射VM與凍結(jié)過程。新VM包含著每個(gè)頁面中所有元組是否都已被凍結(jié)的信息。在迫切模式下進(jìn)行凍結(jié)處理時(shí),可以跳過僅包含凍結(jié)元組的頁面。

圖 5 給出了一個(gè)例子。根據(jù)VM中的信息,凍結(jié)此表時(shí)會(huì)跳過第0頁。在更新完1號(hào)頁面后,相關(guān)的VM信息會(huì)被更新,因?yàn)樵擁撝兴械脑M都已經(jīng)被凍結(jié)了。

凍結(jié)舊元組——迫切模式(9.6或更高版本).png

? 圖 5 凍結(jié)舊元組——迫切模式(9.6或更高版本)

4. 移除不必要的 CLOG 文件

CLOG 中存儲(chǔ)著事務(wù)的狀態(tài)。當(dāng)更新 pg_database.datfrozenxid 時(shí), PostgreSQL 會(huì)嘗試刪除不必要的CLOG 文件。注意,相應(yīng)的 CLOG 頁面也會(huì)被刪除。圖 6 給出了一個(gè)例子。如果 CLOG 文件 0002 中包含最小的pg_database.datfrozenxid,則可以刪除舊文件(0000 和0001),因?yàn)榇鎯?chǔ)在這些文件中的所有事務(wù)在整個(gè)數(shù)據(jù)庫集簇中已經(jīng)被視為凍結(jié)了。

刪除不必要的CLOG文件和頁面.png

? 圖 6 刪除不必要的CLOG文件和頁面

5. 自動(dòng)清理守護(hù)進(jìn)程

自動(dòng)清理守護(hù)進(jìn)程已經(jīng)將清理過程自動(dòng)化,因此 PostgreSQL 運(yùn)維起來非常簡單。自動(dòng)清理守護(hù)程序周期性地喚起幾個(gè) autovacuum_worker 進(jìn)程,默認(rèn)情況下每分鐘喚醒一次(由參數(shù) autovacuum_naptime 定義),每次喚起三個(gè)工作進(jìn)程(由 autovacuum_max_works 定義)。

自動(dòng)清理守護(hù)進(jìn)程喚起的 autovacuum 工作進(jìn)程會(huì)依次對各個(gè)表執(zhí)行并發(fā)清理,從而將對數(shù)據(jù)庫活動(dòng)的影響降至最低。

6. 完整清理(FULL VACUUM)

死元組雖然都被移除了,但表的尺寸沒有減小。這種情況既浪費(fèi)了磁盤空間,又會(huì)對數(shù)據(jù)庫性能產(chǎn)生負(fù)面影響.

完整清理模式概述.png

? 圖 7 完整清理模式概述

完整清理的偽代碼如下所示

    (1)      FOR each table
    (2)          獲取表上的AccessExclusiveLock鎖
    (3)          創(chuàng)建一個(gè)新的表文件
    (4)          FOR 每條活元組in原表
    (5)               將活元組復(fù)制到新表中
    (6)               如果有必要,凍結(jié)該元組
                END FOR
    (7)          移除舊的表文件
    (8)          重建所有索引
    (9)          更新FSM與VM
    (10)         更新統(tǒng)計(jì)信息
                釋放AccessExclusiveLock鎖
            END FOR
    (11)     移除不必要的CLOG文件

1.當(dāng)執(zhí)行完整清理時(shí),沒有人可以訪問(讀/寫)表。

2.最多會(huì)臨時(shí)使用兩倍于表的磁盤空間;因此在處理大表時(shí),有必要檢查剩余磁盤容量。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容