從一道PG知識的選擇題談起

qing_yun發表於2023-12-26

昨天一個網友問我一道關於PG的選擇題:Postgresql資料庫中哪些程式可以將shared buffers中的髒資料回寫到資料檔案?A) BACKEND B) BGWRITER C)CHECKPOINTER D) WALWRITER。

稍微懂點PG資料庫的人不難回答,答案是A、B、C。一些Oracle DBA可能會覺得這個答案有點出乎意料。因為在Oracle資料庫中,回寫DB CACHE髒資料的只有DBWR。可能這些人不太清楚的CKPT負責回寫部分髒資料是80年代早期關係型資料庫的共同特點,Oracle資料庫中,CKPT也曾經負責過寫髒塊。後來隨著資料庫規模的增大,CKPT的功能被獨立出來了,只負責CKPT的推進工作,不再負責寫髒塊了。在Oracle資料庫中,為了更快的將髒塊回寫,透過將DB CACHE分割槽,採用多個DBWR程式可以併發寫髒塊,從而滿足大型資料庫的需要。

PG在刷髒塊的演算法方面的設計比較傳統,這種設計是從90年代一脈相承的,改起來成本較大,所以就一直沿用下來了。只不過讓backend也去寫髒塊,這未免也有點太出乎一般人的想象了。這會產生幾個比較奇怪的現象,一個是某條SQL語句,可能在執行計劃相同、資料量相同的情況下,不同的時間執行,其執行效率有較大的不同。另外一個奇怪現象是某條SELECT語句可能會產生較多的寫操作。實際上Oracle資料庫中也會存在類似的現象,這種情況是由延遲塊清理產生的。當一個大型事務結束後,ITL的狀態清理可能並沒有完成,如果馬上有SELECT操作訪問這些資料,那麼執行SELECT的前臺程式將負責完成這些塊的清理,這時候select操作也會產生大量的寫操作,產生大量的REDO,同時這條SELECT比起平時會慢很多。

透過pg_stat_bgwriter系統表中的buffers_backend和buffers_backend_fsync可以查詢到系統中的backend寫髒塊和fsync的計數。

從我們的一個D-SMART的PG資料庫中看到一個十分神奇的情況,按理說被用來寫髒塊的bgwriter居然很小(buffers_clean),大多數髒塊都是checkpointer和backend寫的 。如果你去檢查一下你們的PG資料庫,可能bgwriter寫髒塊的比例也不是很高。

為什麼會出現這種情況,為什麼大量的寫髒塊工作不由本應該處理髒塊的 bgwriter去做,而讓Backend去寫髒塊呢?這方面的資料很少,我們只能從 PG的程式碼上去找找答案。首先我們看一下src/backend/storage/bufmgr.c,在這裡可以看到backend從shared buffers中分配空閒buffer的演算法。一般我們都能理解從shared buffers中查詢空閒的buffer,如果找不到非髒的空閒塊,那麼就有可能找到一個儲存了髒資料的資料塊,這時候才需要backend去寫髒塊。在Oracle資料庫中,前臺程式是順著lru鏈的冷端去查詢空閒緩衝塊的,如果前臺程式發現了某個沒有被Pin住的塊是髒塊,就會把這個資料塊移到lru-w中,然後繼續往下搜尋,如果連續搜素到了N個髒塊,無法獲得所需要的空閒塊的時候,就會發出一個free buffer requests的事件,讓DBWR加快刷髒塊,然後再去重試。從PG的程式碼上看,PG的大體思想類似,不過策略要複雜得多。

BufferAlloc裡包含了backend查詢某個buffer的頂層邏輯。不閱讀一下還真沒發現PG這方面的程式碼邏輯會搞得如此複雜。要想訪問某個buffer,先要生成一個BUFFER TAG(關於這方面的詳細演算法請參考我2021年寫過的一篇8000多字的長篇《PG SHARED BUFFER POOL的最佳化》)。然後查詢這個BUFFER。

如果BUFFER存在,還有兩種可能性,一種是成功的PIN住了這個BUFFER,那麼就可以返回這個BUFFER了。不過BUFFER存在還有一種可能性是無法PIN住,無法PIN住的原因是可能被其他的會話PIN住了,也可能是一些其他的原因。這種情況,Oracle被稱為read by other session或者buffer busy waits。這個部分不是我們今天分析的重點,我們繼續往下看程式碼。

GetVictimBuffer函式透過時鐘掃描演算法去找一個空閒的buffer。這裡就涉及到查詢空閒shared buffers了。我們下鑽到這個函式的程式碼中去繼續分析。

首先呼叫StrategyGetBuffer去找一個BUFFER。

如果發現找到的是一個髒塊,那麼就把髒塊刷盤,這就是BACKEND也需要刷髒塊的原因之一。作為資料庫緩衝的演算法,我們肯定應該儘可能的找到非髒塊來複用,總是讓BACKEND寫髒塊肯定會降低資料庫的整體效能。

StrategyGetBuffer函式在src/backend/buffer/freelist.c中定義。首先,它會檢查是否有一個策略物件(strategy),如果有,就呼叫GetBufferFromRing函式,從策略物件的環形緩衝區(ring buffer)中獲取一個緩衝區。如果獲取成功,就返回這個緩衝區,並設定from_ring標誌為true。如果沒能正常找到free buffer,它會嘗試喚醒bgwriter,讓它重新整理髒的緩衝區到磁碟,以便釋放一些空間。接下來,backend會檢查StrategyControl->firstFreeBuffer變數,如果大於等於0,就表示有空閒的緩衝區,那麼就透過一個迴圈從空閒連結串列中獲取一個緩衝區。這部分演算法與Oracle的free buffer requests十分類似。

此時如果空閒連結串列為空,backend會進入另一個迴圈,嘗試從victim pool中選擇一個緩衝區,victim pool是一個迴圈佇列,儲存了最近被訪問過的緩衝區的編號。從nextVictimBuffer的當前位置開始,順時針掃描victim pool,尋找一個既不被鎖定,也沒有被引用,也不是髒的緩衝區。如果找到了,就返回這個緩衝區,並將其從victim pool中移除。如果沒有找到合適的緩衝區,它會繼續掃描victim pool,尋找一個既不被鎖定,也沒有被引用,但是是髒塊的緩衝區。如果找到了,就將這個緩衝區的內容寫入磁碟,然後返回這個緩衝區(這是程式碼中backend中另外一個寫髒塊的地方),並將其從victim pool中移除,並新增到策略物件的環形緩衝區中。

我們先不去吐槽PG在這塊程式碼的質量問題,僅僅從演算法來看,backend直接刷髒塊的機會也應該是比較小的。那麼為什麼我們會在pg_stat_bgwriter中看到如此奇葩的資料呢?這裡面其實也有一個十分有意思的邏輯。

首先是關於buffers_backend這個指標,本身這個指標就有一定的誤導性,我們今天看的程式碼上包含了處寫髒塊的地方,其實不用看程式碼我們都能想到第三處,那就是VACUUM。因為backend中還包含了auto vacuum,vacuum操作等寫髒塊的統計資料,因此我們可能會被這個指標誤導。PG社群中十多年前就有人希望PG程式碼中把這些情況區分開來,從而讓buffers_clean更有指向性,不過沒有獲得PG社群核心研發的認同。

另外一點是PG資料庫的策略是儘可能讓資料在記憶體中多存放一段時間,而不急著把髒資料寫盤。因此在PG資料庫中,還是將檢查點程式作為寫髒塊的主力,如果你的系統中的buffers_alloc增長很緩慢的話,那麼只要按照checkpointer的節奏慢慢寫髒塊就可以了,backend總是能夠找到所需要的buffer,因此也就沒必要讓bgwriter去寫髒塊了。這種演算法對於早些年比較緩慢的IO子系統來說是十分友好的,不過對於當今高效能的IO系統來說,不夠高效,比較適合目前IO效能一般的雲上小型資料庫,而對於採用高效能IO裝置的大型資料庫來說,並不一定是很最佳化的。

基於上面的分析我們可以瞭解到,如果你看到你的系統中的buffers_clean總是為0或者總是慢速增長,那麼並不說明系統存在問題,而是說明你的系統寫負載還不算太高,bgwriter還犯不著去幫你刷盤而已。對於IO效能還不錯的系統,或者說規模不算太大的資料庫來說,PG的這種刷髒塊的方法還是可以勝任的,在一些超大型系統中,可能這方面會成為瓶頸。我看到Polardb-PG、openGauss等基於PG程式碼的資料庫產品中,對這方面都做了一些最佳化,引入了專門的機制來替換BGWRITER。目前還沒有對這些程式碼進行分析,因此不知道這方面的改善如何。

今天分析PG這方面原始碼的另外一個收穫是從中學到一些PG的SHARED BUFFERS相關的最佳化策略的。首先shared buffers不能設定得太少,否則backend真正開始大量刷髒塊了,那麼SQL的效能是會受到很大的影響的。其次是CHECKPOINTER的相關引數設定要合理,根據底層IO的能力配置合適的引數,讓CHECKPOINTER刷盤的速度能夠跟得上buffers_alloc的速度。如果我們發現buffers_clean的增長比較快了,那麼說明目前系統的負載對shared buffers 有一定的壓力了,那麼我們就需要考慮調整bgwriter相關的引數了。

最後的原始碼連結是我兩年多前寫的一篇關於PG SHARED BUFFERS的內部結構的分析文章,文章很長,有8000多字,有興趣的朋友可以閱讀一下。文中有些觀點可能和今天的文章有些不大一致了,如果存在這方面的觀點,那麼就以今天的文章為準吧。對PG資料庫的理解都是一點一點的從模糊到清晰,從不大準確到相對準確的。認知的提升是從一個個案例,一段段原始碼的分析中逐漸完成的。

來自 “ 白鱔的洞穴 ”, 原文作者:白鱔;原文連結:https://mp.weixin.qq.com/s/0w5t3rjuqsYlXXbMgeV9RA,如有侵權,請聯絡管理員刪除。

相關文章