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