從一道PG知識(shí)的選擇題談起,你悟到了些什么?
昨天一個(gè)網(wǎng)友問我一道關(guān)于PG的選擇題:Postgresql數(shù)據(jù)庫中哪些進(jìn)程可以將shared buffers中的臟數(shù)據(jù)回寫到數(shù)據(jù)文件?A) BACKEND B) BGWRITER C)CHECKPOINTER D) WALWRITER。
稍微懂點(diǎn)PG數(shù)據(jù)庫的人不難回答,答案是A、B、C。一些Oracle DBA可能會(huì)覺得這個(gè)答案有點(diǎn)出乎意料。因?yàn)樵贠racle數(shù)據(jù)庫中,回寫DB CACHE臟數(shù)據(jù)的只有DBWR。可能這些人不太清楚的CKPT負(fù)責(zé)回寫部分臟數(shù)據(jù)是80年代早期關(guān)系型數(shù)據(jù)庫的共同特點(diǎn),Oracle數(shù)據(jù)庫中,CKPT也曾經(jīng)負(fù)責(zé)過寫臟塊。后來隨著數(shù)據(jù)庫規(guī)模的增大,CKPT的功能被獨(dú)立出來了,只負(fù)責(zé)CKPT的推進(jìn)工作,不再負(fù)責(zé)寫臟塊了。在Oracle數(shù)據(jù)庫中,為了更快的將臟塊回寫,通過將DB CACHE分區(qū),采用多個(gè)DBWR進(jìn)程可以并發(fā)寫臟塊,從而滿足大型數(shù)據(jù)庫的需要。
PG在刷臟塊的算法方面的設(shè)計(jì)比較傳統(tǒng),這種設(shè)計(jì)是從90年代一脈相承的,改起來成本較大,所以就一直沿用下來了。只不過讓backend也去寫臟塊,這未免也有點(diǎn)太出乎一般人的想象了。這會(huì)產(chǎn)生幾個(gè)比較奇怪的現(xiàn)象,一個(gè)是某條SQL語句,可能在執(zhí)行計(jì)劃相同、數(shù)據(jù)量相同的情況下,不同的時(shí)間執(zhí)行,其執(zhí)行效率有較大的不同。另外一個(gè)奇怪現(xiàn)象是某條SELECT語句可能會(huì)產(chǎn)生較多的寫操作。實(shí)際上Oracle數(shù)據(jù)庫中也會(huì)存在類似的現(xiàn)象,這種情況是由延遲塊清理產(chǎn)生的。當(dāng)一個(gè)大型事務(wù)結(jié)束后,ITL的狀態(tài)清理可能并沒有完成,如果馬上有SELECT操作訪問這些數(shù)據(jù),那么執(zhí)行SELECT的前臺(tái)進(jìn)程將負(fù)責(zé)完成這些塊的清理,這時(shí)候select操作也會(huì)產(chǎn)生大量的寫操作,產(chǎn)生大量的REDO,同時(shí)這條SELECT比起平時(shí)會(huì)慢很多。
圖片
通過pg_stat_bgwriter系統(tǒng)表中的buffers_backend和buffers_backend_fsync可以查詢到系統(tǒng)中的backend寫臟塊和fsync的計(jì)數(shù)。
圖片
從我們的一個(gè)D-SMART的PG數(shù)據(jù)庫中看到一個(gè)十分神奇的情況,按理說被用來寫臟塊的bgwriter居然很?。╞uffers_clean),大多數(shù)臟塊都是checkpointer和backend寫的 。如果你去檢查一下你們的PG數(shù)據(jù)庫,可能bgwriter寫臟塊的比例也不是很高。
為什么會(huì)出現(xiàn)這種情況,為什么大量的寫臟塊工作不由本應(yīng)該處理臟塊的 bgwriter去做,而讓Backend去寫臟塊呢?這方面的資料很少,我們只能從 PG的代碼上去找找答案。首先我們看一下src/backend/storage/bufmgr.c,在這里可以看到backend從shared buffers中分配空閑buffer的算法。一般我們都能理解從shared buffers中查找空閑的buffer,如果找不到非臟的空閑塊,那么就有可能找到一個(gè)存儲(chǔ)了臟數(shù)據(jù)的數(shù)據(jù)塊,這時(shí)候才需要backend去寫臟塊。在Oracle數(shù)據(jù)庫中,前臺(tái)進(jìn)程是順著lru鏈的冷端去查找空閑緩沖塊的,如果前臺(tái)進(jìn)程發(fā)現(xiàn)了某個(gè)沒有被Pin住的塊是臟塊,就會(huì)把這個(gè)數(shù)據(jù)塊移到lru-w中,然后繼續(xù)往下搜索,如果連續(xù)搜素到了N個(gè)臟塊,無法獲得所需要的空閑塊的時(shí)候,就會(huì)發(fā)出一個(gè)free buffer requests的事件,讓DBWR加快刷臟塊,然后再去重試。從PG的代碼上看,PG的大體思想類似,不過策略要復(fù)雜得多。
圖片
BufferAlloc里包含了backend查找某個(gè)buffer的頂層邏輯。不閱讀一下還真沒發(fā)現(xiàn)PG這方面的代碼邏輯會(huì)搞得如此復(fù)雜。要想訪問某個(gè)buffer,先要生成一個(gè)BUFFER TAG(關(guān)于這方面的詳細(xì)算法請(qǐng)參考我2021年寫過的一篇8000多字的長篇《PG SHARED BUFFER POOL的優(yōu)化》)。然后查找這個(gè)BUFFER。
圖片
如果BUFFER存在,還有兩種可能性,一種是成功的PIN住了這個(gè)BUFFER,那么就可以返回這個(gè)BUFFER了。不過BUFFER存在還有一種可能性是無法PIN住,無法PIN住的原因是可能被其他的會(huì)話PIN住了,也可能是一些其他的原因。這種情況,Oracle被稱為read by other session或者buffer busy waits。這個(gè)部分不是我們今天分析的重點(diǎn),我們繼續(xù)往下看代碼。
圖片
GetVictimBuffer函數(shù)通過時(shí)鐘掃描算法去找一個(gè)空閑的buffer。這里就涉及到查找空閑shared buffers了。我們下鉆到這個(gè)函數(shù)的代碼中去繼續(xù)分析。
圖片
首先調(diào)用StrategyGetBuffer去找一個(gè)BUFFER。
圖片
如果發(fā)現(xiàn)找到的是一個(gè)臟塊,那么就把臟塊刷盤,這就是BACKEND也需要刷臟塊的原因之一。作為數(shù)據(jù)庫緩沖的算法,我們肯定應(yīng)該盡可能的找到非臟塊來復(fù)用,總是讓BACKEND寫臟塊肯定會(huì)降低數(shù)據(jù)庫的整體性能。
StrategyGetBuffer函數(shù)在src/backend/buffer/freelist.c中定義。首先,它會(huì)檢查是否有一個(gè)策略對(duì)象(strategy),如果有,就調(diào)用GetBufferFromRing函數(shù),從策略對(duì)象的環(huán)形緩沖區(qū)(ring buffer)中獲取一個(gè)緩沖區(qū)。如果獲取成功,就返回這個(gè)緩沖區(qū),并設(shè)置from_ring標(biāo)志為true。如果沒能正常找到free buffer,它會(huì)嘗試喚醒bgwriter,讓它刷新臟的緩沖區(qū)到磁盤,以便釋放一些空間。接下來,backend會(huì)檢查StrategyControl->firstFreeBuffer變量,如果大于等于0,就表示有空閑的緩沖區(qū),那么就通過一個(gè)循環(huán)從空閑鏈表中獲取一個(gè)緩沖區(qū)。這部分算法與Oracle的free buffer requests十分類似。
此時(shí)如果空閑鏈表為空,backend會(huì)進(jìn)入另一個(gè)循環(huán),嘗試從victim pool中選擇一個(gè)緩沖區(qū),victim pool是一個(gè)循環(huán)隊(duì)列,存儲(chǔ)了最近被訪問過的緩沖區(qū)的編號(hào)。從nextVictimBuffer的當(dāng)前位置開始,順時(shí)針掃描victim pool,尋找一個(gè)既不被鎖定,也沒有被引用,也不是臟的緩沖區(qū)。如果找到了,就返回這個(gè)緩沖區(qū),并將其從victim pool中移除。如果沒有找到合適的緩沖區(qū),它會(huì)繼續(xù)掃描victim pool,尋找一個(gè)既不被鎖定,也沒有被引用,但是是臟塊的緩沖區(qū)。如果找到了,就將這個(gè)緩沖區(qū)的內(nèi)容寫入磁盤,然后返回這個(gè)緩沖區(qū)(這是代碼中backend中另外一個(gè)寫臟塊的地方),并將其從victim pool中移除,并添加到策略對(duì)象的環(huán)形緩沖區(qū)中。
我們先不去吐槽PG在這塊代碼的質(zhì)量問題,僅僅從算法來看,backend直接刷臟塊的機(jī)會(huì)也應(yīng)該是比較小的。那么為什么我們會(huì)在pg_stat_bgwriter中看到如此奇葩的數(shù)據(jù)呢?這里面其實(shí)也有一個(gè)十分有意思的邏輯。
首先是關(guān)于buffers_backend這個(gè)指標(biāo),本身這個(gè)指標(biāo)就有一定的誤導(dǎo)性,我們今天看的代碼上包含了處寫臟塊的地方,其實(shí)不用看代碼我們都能想到第三處,那就是VACUUM。因?yàn)閎ackend中還包含了auto vacuum,vacuum操作等寫臟塊的統(tǒng)計(jì)數(shù)據(jù),因此我們可能會(huì)被這個(gè)指標(biāo)誤導(dǎo)。PG社區(qū)中十多年前就有人希望PG代碼中把這些情況區(qū)分開來,從而讓buffers_clean更有指向性,不過沒有獲得PG社區(qū)核心研發(fā)的認(rèn)同。
另外一點(diǎn)是PG數(shù)據(jù)庫的策略是盡可能讓數(shù)據(jù)在內(nèi)存中多存放一段時(shí)間,而不急著把臟數(shù)據(jù)寫盤。因此在PG數(shù)據(jù)庫中,還是將檢查點(diǎn)進(jìn)程作為寫臟塊的主力,如果你的系統(tǒng)中的buffers_alloc增長很緩慢的話,那么只要按照checkpointer的節(jié)奏慢慢寫臟塊就可以了,backend總是能夠找到所需要的buffer,因此也就沒必要讓bgwriter去寫臟塊了。這種算法對(duì)于早些年比較緩慢的IO子系統(tǒng)來說是十分友好的,不過對(duì)于當(dāng)今高性能的IO系統(tǒng)來說,不夠高效,比較適合目前IO性能一般的云上小型數(shù)據(jù)庫,而對(duì)于采用高性能IO設(shè)備的大型數(shù)據(jù)庫來說,并不一定是很優(yōu)化的。
基于上面的分析我們可以了解到,如果你看到你的系統(tǒng)中的buffers_clean總是為0或者總是慢速增長,那么并不說明系統(tǒng)存在問題,而是說明你的系統(tǒng)寫負(fù)載還不算太高,bgwriter還犯不著去幫你刷盤而已。對(duì)于IO性能還不錯(cuò)的系統(tǒng),或者說規(guī)模不算太大的數(shù)據(jù)庫來說,PG的這種刷臟塊的方法還是可以勝任的,在一些超大型系統(tǒng)中,可能這方面會(huì)成為瓶頸。我看到Polardb-PG、openGauss等基于PG代碼的數(shù)據(jù)庫產(chǎn)品中,對(duì)這方面都做了一些優(yōu)化,引入了專門的機(jī)制來替換BGWRITER。目前還沒有對(duì)這些代碼進(jìn)行分析,因此不知道這方面的改善如何。
今天分析PG這方面源代碼的另外一個(gè)收獲是從中學(xué)到一些PG的SHARED BUFFERS相關(guān)的優(yōu)化策略的。首先shared buffers不能設(shè)置得太少,否則backend真正開始大量刷臟塊了,那么SQL的性能是會(huì)受到很大的影響的。其次是CHECKPOINTER的相關(guān)參數(shù)設(shè)置要合理,根據(jù)底層IO的能力配置合適的參數(shù),讓CHECKPOINTER刷盤的速度能夠跟得上buffers_alloc的速度。如果我們發(fā)現(xiàn)buffers_clean的增長比較快了,那么說明目前系統(tǒng)的負(fù)載對(duì)shared buffers 有一定的壓力了,那么我們就需要考慮調(diào)整bgwriter相關(guān)的參數(shù)了。
最后的源碼鏈接是我兩年多前寫的一篇關(guān)于PG SHARED BUFFERS的內(nèi)部結(jié)構(gòu)的分析文章,文章很長,有8000多字,有興趣的朋友可以閱讀一下。文中有些觀點(diǎn)可能和今天的文章有些不大一致了,如果存在這方面的觀點(diǎn),那么就以今天的文章為準(zhǔn)吧。對(duì)PG數(shù)據(jù)庫的理解都是一點(diǎn)一點(diǎn)的從模糊到清晰,從不大準(zhǔn)確到相對(duì)準(zhǔn)確的。認(rèn)知的提升是從一個(gè)個(gè)案例,一段段源碼的分析中逐漸完成的。