創(chuàng)新互聯(lián)是一家專業(yè)提供武清企業(yè)網(wǎng)站建設(shè),專注與網(wǎng)站制作、做網(wǎng)站、H5頁面制作、小程序制作等業(yè)務(wù)。10年已為武清眾多企業(yè)、政府機構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)絡(luò)公司優(yōu)惠進行中。
原文: https://www.enmotech.com/web/detail/1/701/1.html
如果是之前學習別的數(shù)據(jù)庫的人,看PostgreSQL會感覺到有句話非常奇怪:“PostgreSQL的回滾是立即完成的,不會受到事務(wù)大小本身的影響”。
奇怪在哪里呢?比方我曾經(jīng)遇到過一次 MySQL 的故障,一個開發(fā)給生產(chǎn)數(shù)據(jù)庫導入數(shù)據(jù),用的是 Python 腳本,但是,他沒有注意一個事情, Python 的 MySQLdb 默認情況下,是設(shè)置 autocommit 為 的,于是這哥們導數(shù)據(jù)(這里說的導入,不是普通那種 load data ,而是帶有業(yè)務(wù)操作的 SQL 語句,所以需要腳本操作)腳本跑了一天之后,整個數(shù)據(jù)庫的狀況就變得極為糟糕了:他導入所用的,是一個業(yè)務(wù)的核心表,一堆業(yè)務(wù)操作都需要操作這個表,但隨著這個導入動作跑了一天,占掉了大量的行鎖(幾百萬行鎖)之后,整個業(yè)務(wù)系統(tǒng)的對外服務(wù)都會處于一個無法求到鎖的狀況了(還摻和著 MySQL 間隙鎖的坑坑洼洼),業(yè)務(wù)服務(wù)停擺,于是,作為 DBA 來說,最終的決策,只有殺掉這個”大”事務(wù)了。一個 kill 命令過去之后,我們當時倆 DBA 開始慢慢數(shù)—小螞蟻慢慢爬——碰到—顆大豆芽——碰到兩顆大豆芽——
最終在將近三個小時的 rollback 之后,這個事務(wù)完成回滾,業(yè)務(wù)系統(tǒng)恢復。
所以看到 PostgreSQL 的這個描述之后,我第一時間的反應(yīng)是, why ? how ? what ?
于是就有了這一篇文章,我從
PG
的事務(wù)可見性判斷講起,整理一下
PG
核心文件
clog
的機理
與作用
。
另注:從
pg 10
以后,
clog
改名為
xact
,主要原因,是很多人習慣性地使用
*log
刪除日志文件,總是會不小心刪除掉原先的
xlog
與
clog
文件,導致數(shù)據(jù)庫不可用,所以分別改名為
wal
與
xact
,后文依然以
clog
為討論單詞,需要注意。
clog簡介
第一個問題,什么是 clog ?或者換個說法, PG 到底有哪些日志,它們分別是干啥的?
除了理所當前的各路文本記錄(比方數(shù)據(jù)庫的運行報錯日志之類) , PG 的二進制類日志文件主要有兩個,一個就是對應(yīng)傳統(tǒng)數(shù)據(jù)庫理論的 redo 日志,理論上,所有數(shù)據(jù)的修改操作都會被記錄到這個日志,在事務(wù)提交的時候確保操作都記錄到磁盤中,這樣講即便發(fā)生宕機,數(shù)據(jù)庫也能以不丟數(shù)據(jù)的形態(tài)重新復活。
但是,各個數(shù)據(jù)庫在這個點上都有不同的實現(xiàn),比方 MySQL 會有一個 binlog 用于跨存儲引擎的主從同步,而在 PG 中,主從同步已經(jīng)通過 redo 日志( PG 術(shù)語為 XLOG )同步的情況下,為了處理沒有 undo 帶來的一系列問題,其中可見性判斷這個功能,就是交給 clog 日志文件解決的。
Clog 中記錄了每一個事務(wù)相關(guān)的 xid (記得之前曾吐槽過這個玩意的大小問題帶來的 freeze 問題)以及 xid 對應(yīng)的事務(wù)的提交狀態(tài)。提交狀態(tài)包括以下一些:執(zhí)行中,已提交,已中斷,已提交的子事務(wù)??吹竭@里,就可以明白,只要事務(wù)提交的時候,設(shè)置狀態(tài)為已提交,而事務(wù)回滾的時候,設(shè)置狀態(tài)為已中斷,就可以達到目的,的確避免了操作數(shù)百萬行的事務(wù)突然要回滾時候的巨大代價。
但我看到這里的時候,就產(chǎn)生一個疑惑,這樣的話,我查數(shù)據(jù)的時候,見到一行的
xid
之后,需要馬上確認其可見性,就需要去查
clog
,這個查詢頻率勢必極高而且隨機性很大,這個問題該怎么解決呢?
#define CLOG_BITS_PER_XACT 2
#define CLOG_XACTS_PER_BYTE 4
#define CLOG_XACTS_PER_PAGE ( BLCKSZ * CLOG_XACTS_PER_BYTE )
#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1)
#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE)
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)
#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)
#define TransactionIdToBIndex ( xid ) (( xid ) % ( TransactionId ) CLOG_XACTS_PER_BYTE )
PG 代碼給了一個非常精彩的回答。
還記得之前 vacuum 那個里面,我大力吐槽 PG 對 32 位 xid 的執(zhí)著,但這個 32 位 id 果真一無是處嗎?看到這里才明白,還留著這么一筆思路。
一個簡單的算術(shù),每個事務(wù)標記占據(jù) 2 個比特位(無符號 0 1 2 3 對應(yīng)前面提到的事務(wù)狀態(tài)),也就是說,每個字節(jié)可以保存 4 個事務(wù),每當 PG 需要確定當前事務(wù)狀態(tài)的時候,就直接根據(jù)當前事務(wù) id 計算得到對應(yīng)的 clog 頁位置(除每頁 clog 之后的整數(shù)商是頁數(shù)字,而余數(shù)則是在頁中的具體位置)。真是把文件當 hash 表用的典范啊。
在 32 位 xid 的情況下,假設(shè) xid 限制是 20 億,每個 8K 的 clog 頁存儲 32k 事務(wù)位的情況下, clog 最大也才五百來 MB ,這部分交給操作系統(tǒng)的文件緩存足以保障訪問效率了。
真是一個絕妙的主意不是么?如果不考慮 64 位 xid 的情況下, clog 大小完全不可控的情況的話。
還是把話題集中在 clog , 下面我們來探討的是,當事務(wù)提交或者回滾的時候,其內(nèi)部的運作機理又是如何呢?
以及,前文中可以看到的一個明顯問題,
pg
這種操作的話,寫入的行必然是一個”執(zhí)行中事務(wù)狀態(tài)”的行,這種行難道是每次查的時候,都得去找
clog
判斷嗎?如果頻繁掃他幾百萬行,是不是會有問題?
clog實現(xiàn)內(nèi)部
前面提到, clog 里面會記錄的是 xid 對應(yīng)的事務(wù)狀態(tài)。在 PG 里面, xid 是一個珍貴的資源(考慮到每 20 億大限的成住空壞),因此并不是每個事務(wù)都會被分配到 xid 。
一般來說,只有一個事務(wù)進行了數(shù)據(jù)修改(比如 insert , update , delete )之類的操作,才會被分配給一個 xid 。
當 這個事務(wù)最終提交或者回滾的時候,其最終狀態(tài)就會被記錄入 clog 。
事務(wù)提交與回滾時候的clog操作
首先來說提交。
拋開其他各種過程,每次事務(wù)提交的時候,主要的調(diào)用路徑是: CommitTransaction (提交事務(wù)時候調(diào)用)-> RecordTransactionCommit(記錄事務(wù)為已提交)-> TransactionIdCommitTree(同步標記事務(wù)為提交)/TransactionIdAsyncCommitTree(異步標記事務(wù)為提交,調(diào)用下一步需要提供lsn)-> TransactionIdSetTreeStatus(設(shè)置事務(wù)與子事務(wù)狀態(tài))-> TransactionIdSetPageStatus(設(shè)置單數(shù)據(jù)頁內(nèi)事務(wù)狀態(tài))-> TransactionIdSetPageStatusInternal(設(shè)置實際文件頁)-> TransactionIdSetStatusBit(設(shè)置比特位)
其中值得拿出來講的,主要是 TransactionIdSetTreeStatus 這個方法。
這里涉及到一個概念,子事務(wù)。在 PG 這個地方,子事務(wù)的概念主要指:事務(wù)從開始到結(jié)束,期間可以 savepoint ,之后 rollback 到 savepoint 而不是事務(wù)起點,在實際情況中多有應(yīng)用,因此這里父事務(wù)與子事務(wù)(比如事務(wù)最終提交,但期間有回滾的情況,或者事務(wù)期間多次 save point )必須盡可能原子性的方式寫入,否則事務(wù)可見性就會出現(xiàn)問題。
在代碼注釋里面,對這里的寫入做了一個比較直觀的例子:
比如一個事務(wù)t,有子事務(wù) t1,t2,t3,t4,其中t,t1被映射到clog頁p1,t2和t3在p2,t4在頁p3。那么寫入的時候,順序如下:
設(shè)置p2 的t2 t3為子提交,之后設(shè)置p3的t4位子提交
設(shè)置t1為子提交,之后設(shè)置t為已提交,之后設(shè)置t1為已提交
設(shè)置 t2 t3 為已提交,設(shè)置t4位已提交
對于回滾,實際上也是調(diào)用TransactionIdSetTreeStatus方法,只是上層函數(shù)是TransactionIdAbortTree,設(shè)置的標記是TRANSACTIONSTATUSABORTED,也就是記錄事務(wù)為中斷。語義上來說,對于事務(wù)中斷,由于事務(wù)的原子性要求,中斷的事務(wù)數(shù)據(jù)就是不可見的了,沒啥問題。
數(shù)據(jù)行事務(wù)可見性的判斷與clog
眾所周知的是,pg新增行都會對原先的行打一個刪除標記,然后寫在原先行的旁邊,理所當然地,每個數(shù)據(jù)行都會記錄一個事務(wù)標記(當然還有數(shù)據(jù)行對應(yīng)的事務(wù)id),來確??梢娦?,避免看到事務(wù)層面已經(jīng)rollback的事務(wù)。
首先,寫入的當時,事務(wù)沒有結(jié)束的時候,必然是”執(zhí)行中”這個狀態(tài)。當事務(wù)之后提交,或者回滾的時候,pg是必然不會回頭改這個標記的,否則無論提交還是回滾,都是一個代價巨大的事情。
就前文所言,pg的事務(wù)可見性,是通過行的事務(wù)id,找到clog里面對應(yīng)的標記位置,然后判斷的,這里非常理所當然的一個事情是,這種判斷,每一行做一次就足夠了,判斷清楚后,修改掉這個事務(wù)標記為已提交或者是中斷事務(wù),后續(xù)讀取的時候,就不需要回查clog了。
PG當然就是這么干的。
也就是說,前一個事務(wù)所有修改的數(shù)據(jù),它沒有在提交或者回滾的當時改掉所有的修改標記,而是把爛攤子丟給后來的人。
而這里還藏著一個問題:你既然修改了行的標記,那理所當然地,行所在數(shù)據(jù)塊的校驗和就變了,校驗和變了,那塊是不是就必須得傳到wal緩存走流程了?即便沒有涉及數(shù)據(jù)的變更?而且考慮到從庫查詢的時候,查數(shù)據(jù)也可以直接走從庫的clog流程,這個數(shù)據(jù)塊是不是必須傳給從庫?
那么,現(xiàn)在就有一個現(xiàn)成的面試問題了:PostgreSQL單純的select執(zhí)行,會不會產(chǎn)生WAL日志?
事實上,這里的事務(wù)標記帶來的校驗和的問題,在PG里面的處理是比較特殊的。
PostgreSQL里面,當且僅當設(shè)置了walloghints或者初始化時候,initdb啟用了checksum的情況下,才會在設(shè)置標記為的時候去寫WAL日志。
而且這里還不是每次設(shè)置標記位都會寫。
必須得是,前一次checkpoint之后,數(shù)據(jù)塊第一次被修改就是sethintbit操作的情況下,才會寫整個數(shù)據(jù)塊到WAL。
clog的一些衍生思考
實際上就清理過期數(shù)據(jù),MySQL也是用delete+insert替代update,但在清理以及處理上,并沒有搞到vacuum這么大代價,比如MySQL的purge線程的執(zhí)行,一般很少需要特別關(guān)注,而PostgreSQL的vacuum雖然說是并行化,但是在單表內(nèi)卻是串行的,民間貢獻的表內(nèi)并行vacuum的補丁因為各種bug遲遲沒有合并(目前來看PG12沒戲了),這個事情為什么會這樣呢?
因為clog畢竟只是事務(wù)可見性的標記,而不是事務(wù)的修改關(guān)聯(lián)。在傳統(tǒng)的undo類實現(xiàn)中,修改的數(shù)據(jù),以及關(guān)聯(lián)的事務(wù)等,都在undo按照順序存儲,purge執(zhí)行的之后,直接從undo就可以找到對應(yīng)的需要處理的數(shù)據(jù)塊直接處理。
但是對于PG來說,由于僅僅只有事務(wù)標記,vacuum必須掃描所有的數(shù)據(jù)文件的數(shù)據(jù)塊來處理這個問題,雖然pg里面,vacuum和統(tǒng)計信息采集合二為一(統(tǒng)計信息采集是傳統(tǒng)數(shù)據(jù)庫最大的全庫掃描行為了),但必然需要付出的全庫掃描代價卻一個都不會少。
因此vacuum對超大表非常慢,極端情況下在vacuum freezen時候?qū)е氯珟觳豢捎茫╢reezen結(jié)束前不允許執(zhí)行新事務(wù)),就是有極大可能的事情了。
為了解決超大表,傳統(tǒng)建議是使用分區(qū)表,但PostgreSQL的官方實現(xiàn)里面,分區(qū)表一直不太穩(wěn)定,并且支持不足,因此又不得不引入pathman這個外部組件來協(xié)調(diào)處理,導致運維復雜度的進一步上升,就成了理所當然的事情。
不過目前就PostgreSQL 12來說,已經(jīng)在逐漸開放存儲引擎層面的接口,而社區(qū)中實現(xiàn)的undo版本的存儲引擎,雖然因為完成度問題沒有在本次release中發(fā)布,但未來可期,相信vacuum這一類問題,在未來必然會得到更好的處理。
想了解更多關(guān)于數(shù)據(jù)庫、云技術(shù)的內(nèi)容嗎?
快來關(guān)注“數(shù)據(jù)和云”公眾號、“云和恩墨”官方網(wǎng)站,我們期待與大家一同學習和進步!
(掃描上方二維碼,關(guān)注“數(shù)據(jù)和云”公眾號,即可查看更多科技文章)