本文是《PostgreSQL指南--內幕探索》(鈴木啟修著 馮若航 劉陽明 張文升譯)的讀書筆記审洞,僅供自己學習使用,請勿轉載痕支。這是一本好書颁虐,如有需要請直接購買書籍。
清理過程(通常簡稱為VACUUM)是一種維護過程卧须,有助于 PostgreSQL 的持久運行另绩。它的兩個主要任務是刪除死元組,以及凍結事務標識花嘶。
為了移除死元組板熊,清理過程有兩種模式,分別是并發(fā)清理與完整清理察绷。并發(fā)清理過程會刪除表文件每個頁面中的死元組干签,而其他事務可以在其運行時繼續(xù)讀取該表。相反拆撼,完整清理不僅會移除整個文件中所有的死元組容劳,還會對整個文件中所有的活元組進行碎片整理。其他事務在完整清理運行時無法訪問該表闸度。
盡管清理過程對PostgreSQL至關重要竭贩,但與其他功能相比,它的改進相對其他功能而言要慢一些莺禁。例如在8.0版本之前留量,清理過程必須手動執(zhí)行(通過psql 實用程序或使用 cron 守護進程)。直到2005年實現(xiàn)了autovacuum 守護進程時哟冬,這一過程才實現(xiàn)了自動化楼熄。
由于清理過程涉及全表掃描,因此該過程代價高昂浩峡。在版本8.4(2009)中引入了可見性映射( Visibility Map翰灾,VM)來提高移除死元組的效率平斩。在版本9.6(2016)中增強了 VM绘面,從而改善了凍結過程的表現(xiàn)飒货。
1. 并發(fā)清理概述
清理過程為指定的表或數(shù)據(jù)庫中的所有表執(zhí)行以下任務。
1.移除死元組和對活元組進行碎片整理晃虫。
- 移除每一頁中的死元組,并對每一頁內的活元組進行碎片整理荆责。
- 移除指向死元組的索引元組濒持。
2.凍結舊的事務標識。
- 如有必要柑营,凍結舊元組的事務標識酒奶。
- 更新與凍結事務標識相關的系統(tǒng)視圖( pg_database 與 pg_class)瘸彤。
- 如果可能质况,移除不必要的提交日志文件。
3.其他。
- 更新已處理表的空閑空間映射(FSM)和可見性映射(VM)誊涯。
- 更新一些統(tǒng)計信息( pg_stat_all_tables 等)暴构。
以下偽代碼描述了清理的過程误阻。
(1) FOR each table
(2) 在目標表上獲取 ShareUpdateExclusiveLock 鎖 /* 允許其他事務對該表進行讀取 */
/* 第一部分 */
(3) 掃描所有頁面,定位死元組晶丘,如有必要黍氮,凍結過早的元組
(4) 如果存在,移除指向死元組的索引元組
/* 第二部分 */
(5) FOR each page of the table
(6) 移除死元組浅浮,重排本頁內的活元組
(7) 逐頁更新目標表頁對應的 FSM 與 VM
END FOR
/* 第三部分 */
(8) 如果最后一個頁面沒有任何元組沫浆,截斷最后的頁面
(9) 更新系統(tǒng)數(shù)據(jù)字典與統(tǒng)計信息
釋放ShareUpdateExclusiveLock鎖
END FOR
/* 后續(xù)處理 */
(10) 更新統(tǒng)計信息與系統(tǒng)數(shù)據(jù)字典
(11) 如果可能,移除非必要的文件及CLOG中的文件
該偽碼分為兩大塊:一塊是依次處理表的循環(huán)滚秩,一塊是后處理邏輯专执。而循環(huán)塊又分為三個部分,每一個部分都有各自的任務郁油。接下來會描述這三個部分及后處理的邏輯本股。
1.1 第一部分
這一部分執(zhí)行凍結處理攀痊,并刪除指向死元組的索引元組。
首先拄显,PostgreSQL 掃描目標表以構建死元組列表苟径,如果可能的話,還會凍結舊元組躬审。該列表存儲在本地內存中的 maintenance_work_mem 里(維護用的工作內存)棘街。凍結過程將在第 3 節(jié)中介紹。
掃描完成后承边,PostgreSQL 根據(jù)構建得到的死元組列表來刪除索引元組遭殉。該過程在內部被稱為“清除階段”。不用說博助,該過程代價高昂险污。在 10.0 或更低版本中始終會執(zhí)行清除階段。在 11.0 或更高版本中翔始,如果目標索引是B樹罗心,是否執(zhí)行清除階段由配置參數(shù) vacuum_cleanup_index_scale_factor 決定。詳細信息請參考此參數(shù)的說明城瞎。
當 maintenance_work_mem 已滿渤闷,且未完成全部掃描時,PostgreSQL繼續(xù)進行后續(xù)任務脖镀,即步驟(4)到(7)飒箭,完成后再重新返回步驟(3)并繼續(xù)掃描。
1.2 第二部分
這一部分會移除死元組蜒灰,并逐頁更新 FSM 和 VM弦蹂。
? 圖 1 刪除死元組
假設該表包含三個頁面,首先關注 0 號頁面(即第一個頁面)强窖,該頁面包含三條元組凸椿, 其中 Tuple_2 是一條死元組,如圖 1(1)所示翅溺。在這里PostgreSQL 移除了 Tuple_2脑漫,并重排剩余元組來整理碎片空間,然后更新該頁面的 FSM 和 VM咙崎,如圖 1(2)所示优幸。PostgreSQL 不斷重復該過程直至最后一頁。注意褪猛,非必要的行指針是不會被移除的网杆,它們會在將來被重用。因為如果移除了行指針,就必須同時更新所有相關索引中的索引元組碳却。
1.3 第三部分
第三部分會針對每個表队秩,更新與清理過程相關的統(tǒng)計信息和系統(tǒng)視圖。此外追城,如果最后一頁中沒有元組刹碾,則該頁會從表文件中被截斷燥撞。
1.4 后續(xù)處理
當處理完成后座柱,PostgreSQL 會更新與清理過程相關的幾個統(tǒng)計數(shù)據(jù),以及相關的系統(tǒng)視圖物舒;如果可能的話色洞,它還會移除部分不必要的 CLOG 文件,見第 4 節(jié)冠胯。
2. 可見性映射
為了能加快VACUUM查找包含無效元組的文件塊的過程火诸,PG 為每個表文件設置了一個附屬文件 ——— 可見性映射表。 可見性映射在 9.6 版中進行了加強荠察,以提高凍結處理的效率置蜀。新的 VM 除了顯示頁面可見性之外,還包含了頁面中元組是否全部凍結的信息悉盆。 VM 中為表的每一個文件塊(Page)設置了一位盯荤,用來標記該文件塊是否包含無效元組。對于包含無效元組的文件塊焕盟,VACUUM有兩種方式處理秋秤,即快速清理(Lazy VACUUM)和完全清理(Full VACUUM)。
注意脚翘,VM 文件僅在 Lazy VACUUM 操作中被用到灼卢,F(xiàn)ull VACUUM 由于要跨塊清理等復雜操作,需要對整個表文件進行掃描来农,所以 VM 文件此時作用不大鞋真。
2.1 結構分析
對于每個表文件,其對應的VM文件命名為:“關系表OID_vm”沃于。對該文件的操作在 visibility_map.c 文件中進行了定義涩咖。
與其他文件一樣, VM文件也被劃分為若干個文件塊(簡稱VM塊)揽涮。VM塊中除了必要的標記信息外抠藕,其他的每一位都對應于一個表塊,當表塊中所有元組都對當前事務可見時蒋困,表塊對應的位才被設置為1盾似。 其文件結構如下所示:
當標志位為1時,VACUUM會忽略掃描對應的表塊,所以能大大提高VACUUM的效率零院。由于VM文件不跟蹤索引溉跃,所以對索引的操作還是需要完全掃描。
3. 凍結過程
凍結過程有兩種模式告抄,依特定條件而擇其一執(zhí)行撰茎。為方便起見,我們將這兩種模式分別稱為惰性模式和迫切模式打洼。
并發(fā)清理通常在內部被稱為“惰性清理”龄糊。但是,本文中定義的惰性模式是凍結過程執(zhí)行的模式募疮。
凍結過程通常以惰性模式運行炫惩,但當滿足特定條件時,也會以迫切模式運行阿浓。在惰性模式下他嚷,凍結過程僅使用目標表對應的VM掃描包含死元組的頁面。迫切模式相則反芭毙,它會掃描所有的頁面筋蓖,無論其是否包含死元組,都會更新與凍結過程相關的系統(tǒng)視圖退敦,并在可能的情況下刪除不必要的CLOG文件粘咖。
3.1 惰性模式
惰性模式當開始凍結處理時, PostgreSQL 計算 freezeLimit_txid 苛聘,并凍結 t_xmin 小于 freezeLimit_txid 的元組涂炎。freezeLimit_txid定義如下:
freezeLimit_txid = ( OldestXmin - vacuum_freeze_min_age )
OldestXmin 是當前正在運行的事務中最早的事務標識。舉個例子设哗,如果在執(zhí)行VACUUM命令時唱捣,還有其他三個事務正在運行,且其txid分別為100网梢、101和102震缭,那么 OldestXmin 就是 100。如果不存在其他事務战虏,OldestXmin 就是執(zhí)行此 VACUUM 命令的事務標識拣宰。這里vacuum_freeze_min_age是一個配置參數(shù)(默認值為50 000 000)。
圖 2 給出了一個具體的例子烦感。Table_1 由三個頁面組成巡社,每個頁面包含三條元組。執(zhí)行VACUUM命令時手趣,當前txid為50 002 500且沒有其他事務晌该。在這種情況下,OldestXmin就是50 002 500,因此freezeLimit_txid為2500朝群。凍結過程按照如下步驟執(zhí)行燕耿。
? 圖2 凍結元組 -- 惰性模式
第0頁:
三條元組被凍結,因為所有元組的 t_xmin 值都小于 freezeLimit_txid姜胖。此外誉帅,因為Tuple_1是一條死元組,所以在該清理過程中被移除右莱。
第1頁:
通過引用可見性映射(從VM中發(fā)現(xiàn)該頁面所有元組都可見)蚜锨,清理過程跳過了對該頁面的清理。
第2頁:
Tuple_7和Tuple_8被凍結隧出,且Tuple_7被移除踏志。
在完成清理過程之前阀捅,與清理相關的統(tǒng)計數(shù)據(jù)會被更新胀瞪,例如 pg_stat_all_tables視圖中的n_live_tup、n_dead_tup饲鄙、last_vacuum凄诞、vacuum_count等字段。
如上例所示忍级,因為惰性模式可能會跳過頁面帆谍,它可能無法凍結所有需要凍結的元組。
這里補充一個長事務的例子:
數(shù)據(jù)庫運行一個長事務轴咱,很久沒有提交導致current_oldest_xmin一直不會超過vacuum_freeze_min_age汛蝙,vacuum不會凍結任何元組。這樣最低的xmin就和當前最新的xmin的距離越來越遠朴肺,差值慢慢接近20億窖剑,這時候數(shù)據(jù)庫為保證數(shù)據(jù)不丟失,會有告警甚至宕機戈稿。
告警
WARNING: database "mydb" must be vacuumed within 177009986 transactions
HINT: To avoid a database shutdown, execute a database-wide VACUUM in "mydb".
宕機
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或更低版本)
迫切模式彌補了惰性模式的缺陷西土。它會掃描所有頁面,檢查表中的所有元組鞍盗,更新相關的系統(tǒng)視圖需了,并在可能時刪除不必要的CLOG文件與頁面。當滿足以下條件時般甲,會執(zhí)行迫切模式肋乍。
pg_database.datfrozenxid < ( OldestXmin - vacuum_freeze_table_age)
在上面的條件中,pg_database.datfrozenxid 是系統(tǒng)視圖 pg_database 中的列敷存,并保存著每個數(shù)據(jù)庫中最老的已凍結的事務標識墓造,細節(jié)將在后面描述。這里我們假設所有 pg_database.datfrozenxid 的值都是1821(這是在9.5版本中安裝新數(shù)據(jù)庫集群之后的初始值)。vacuum_freeze_table_age 是配置參數(shù)(默認為150 000 000)滔岳。
圖 3 給出了一個具體的例子杠娱。在表 1 中,Tuple_1 和 Tuple_7都已經(jīng)被刪除谱煤,Tuple_10和 Tuple_11 則已經(jīng)插入第 2 頁中摊求。執(zhí)行 VACUUM 命令時的事務標識為 150 002 000,且沒有其他事務刘离。因此室叉,OldestXmin = 150 002 000,freezeLimit_txid = OldestXmin - vacuum_freeze_min_age =(150 002 000 - 50 000 000)=100 002 000硫惕。在這種情況下滿足了上述條件:因為1821 < (150 002 000 - 150 000 000)茧痕,所以凍結過程會以迫切模式執(zhí)行,如下所示恼除。注意踪旷,這里是 9.5 或更低版本的行為,最新版本的行為將在第 3.3 節(jié)中描述豁辉。
? 圖3 凍結舊元組——迫切模式(9.5或更低版本)
第0頁:即使所有元組都被凍結令野,也會檢查 Tuple_2 和 Tuple_3。
第1頁:此頁面中的三條元組都會被凍結徽级,因為所有元組的 t_xmin 值都小于 freezeLimit_txid气破。注意,在惰性模式下會跳過此頁面餐抢。
第2頁: 將 Tuple_10凍結现使,而 Tuple_11 沒有凍結。
凍結完一張表的所有元組后旷痕,更新系統(tǒng)視圖 pg_class 的 relfrozenxid 為 freezeLimit_txid:
凍結一張表后碳锈,目標表的 pg_class.relfrozenxid 將被更新。pg_class是一個系統(tǒng)視圖苦蒿,每個pg_class.relfrozenxid 列都保存著相應表的最近凍結的事務標識殴胧。本例中表1的 pg_class.relfrozenxid 會被更新為當前的 freezeLimit_txid(即100 002000),這意味著表 1 中 t_xmin 小于100 002 000 的所有元組都已被凍結佩迟。
如果當前數(shù)據(jù)庫中的所有關系都以迫切模式凍結团滥,則更新此數(shù)據(jù)庫的pg_database. datfrozenxid :
在完成清理過程之前,必要時會更新 pg_database.datfrozenxid报强。每個 pg_database. datfrozenxid 列都包含相應數(shù)據(jù)庫中的最小 pg_class.relfrozenxid灸姊。如果在迫切模式下僅僅對表 1 做凍結處理,則不會更新該數(shù)據(jù)庫的 pg_database. datfrozenxid秉溉,因為其他關系的 pg_class.relfrozenxid(當前數(shù)據(jù)庫可見的其他表和系統(tǒng)視圖)還沒有發(fā)生變化力惯,如圖4(1)所示碗誉。如果當前數(shù)據(jù)庫中的所有關系都以迫切模式凍結,則數(shù)據(jù)庫的 pg_database. datfrozenxid 就會被更新父晶,因為此數(shù)據(jù)庫的所有關系的 pg_class.relfrozenxid 都被更新為當前的 freezeLimit_txid哮缺,如圖4(2)所示。
? 圖4 pg_database.datfrozenxid 與 pg_class.relfrozenxid 之間的關系
如何查詢 pg_class.relfrozenxid 與 pg_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選項:
帶有 FREEZE 選項的 VACUUM 命令會強制凍結指定表中的所有事務標識甲喝。雖然這是在迫切模式下執(zhí)行的尝苇,但是這里 freezeLimit 會被設置為 OldestXmin 而不是OldestXmin -vacuum_freeze_min_age。例如埠胖,當txid=5000的事務執(zhí)行 VACUUM FULL 命令糠溜,且沒有其他正在運行的事務時,OldesXmin 會被設置為 5000直撤,而t_xmin 小于 5000的元組將會被凍結非竿。
3.3 改進迫切模式中的凍結過程(9.6版本及更高版本)
9.5或更低版本中的迫切模式效率不高,因為它始終會掃描所有頁面谋竖。比如在第 3.2 節(jié)的例子中红柱,盡管第0頁中所有元組都被凍結,但還是會被掃描圈盔。
為了解決這一問題豹芯,9.6版本改進了可見性映射VM與凍結過程。新VM包含著每個頁面中所有元組是否都已被凍結的信息驱敲。在迫切模式下進行凍結處理時,可以跳過僅包含凍結元組的頁面宽闲。
圖 5 給出了一個例子众眨。根據(jù)VM中的信息,凍結此表時會跳過第0頁容诬。在更新完1號頁面后娩梨,相關的VM信息會被更新,因為該頁中所有的元組都已經(jīng)被凍結了览徒。
? 圖 5 凍結舊元組——迫切模式(9.6或更高版本)
4. 移除不必要的 CLOG 文件
CLOG 中存儲著事務的狀態(tài)狈定。當更新 pg_database.datfrozenxid 時, PostgreSQL 會嘗試刪除不必要的CLOG 文件习蓬。注意纽什,相應的 CLOG 頁面也會被刪除。圖 6 給出了一個例子躲叼。如果 CLOG 文件 0002 中包含最小的pg_database.datfrozenxid芦缰,則可以刪除舊文件(0000 和0001),因為存儲在這些文件中的所有事務在整個數(shù)據(jù)庫集簇中已經(jīng)被視為凍結了枫慷。
? 圖 6 刪除不必要的CLOG文件和頁面
5. 自動清理守護進程
自動清理守護進程已經(jīng)將清理過程自動化让蕾,因此 PostgreSQL 運維起來非常簡單浪规。自動清理守護程序周期性地喚起幾個 autovacuum_worker 進程,默認情況下每分鐘喚醒一次(由參數(shù) autovacuum_naptime 定義)探孝,每次喚起三個工作進程(由 autovacuum_max_works 定義)笋婿。
自動清理守護進程喚起的 autovacuum 工作進程會依次對各個表執(zhí)行并發(fā)清理,從而將對數(shù)據(jù)庫活動的影響降至最低顿颅。
6. 完整清理(FULL VACUUM)
死元組雖然都被移除了萌抵,但表的尺寸沒有減小。這種情況既浪費了磁盤空間元镀,又會對數(shù)據(jù)庫性能產(chǎn)生負面影響.
? 圖 7 完整清理模式概述
完整清理的偽代碼如下所示
(1) FOR each table
(2) 獲取表上的AccessExclusiveLock鎖
(3) 創(chuàng)建一個新的表文件
(4) FOR 每條活元組in原表
(5) 將活元組復制到新表中
(6) 如果有必要绍填,凍結該元組
END FOR
(7) 移除舊的表文件
(8) 重建所有索引
(9) 更新FSM與VM
(10) 更新統(tǒng)計信息
釋放AccessExclusiveLock鎖
END FOR
(11) 移除不必要的CLOG文件
1.當執(zhí)行完整清理時,沒有人可以訪問(讀/寫)表栖疑。
2.最多會臨時使用兩倍于表的磁盤空間讨永;因此在處理大表時,有必要檢查剩余磁盤容量遇革。