PG 的 Bgwriter 為什么搞得那么復雜
我是從C程序員轉DBA的,所以遇到數據庫的一些問題,我總是喜歡從一個碼農的角度去發出靈魂拷問:為什么會這樣?為什么要這樣?這么做有啥必要?經過二十多年時間,Oracle的一些犄角旮旯的問題都被我理解和接受了。剛剛轉向PG數據庫的學習的時候,對PG這個簡單但是龐大的數據庫系統的一些行為十分不解。今天我們就來聊聊我對BGWRITER的一些思考。
PG的后臺寫入器(Background Writer, bgwriter)負責將臟頁(即被修改但尚未寫入磁盤的數據頁)從共享緩沖區寫回到磁盤,從而減少直接由前端查詢進程執行寫操作的需求,BGWRITER對數據庫的性能優化具有重要作用。對Oracle的dbwr的算法比較了解的DBA剛開始可能很難理解PG的刷臟機制。因為除了BGWriter之外,Checkpoint、Backend都有刷臟的行為。Checkpoint刷臟比較容易理解,因為早期Oracle的CKPT也是有刷臟行為的。需要Checkpoint的時候,如果發現某些需要寫入的臟塊暫時還沒有寫入,并且這些數據塊當前處于可以寫入的狀態,這種情況下CKPT順手就做了。后來為了解決高并發環境下的閂鎖開銷問題,O記才讓CKPT不再刷臟,提高Checkpoint推進的速度。
PG和O記最大的不同是Backend也是有刷臟行為的,這個設計剛開始的時候讓我感到十分不解。BGWriter和Checkpoint都可以刷臟對于閂鎖爭用來說問題還不是太大,頂多是兩個會話協同好就行了,而Backend的數量可能很龐大,如果同時又刷臟需求的話,協調好共享池相關數據結構的LWLOCK成本就低不了。
BGWriter在掃描共享緩沖區時,采用了一種基于LRU(Least Recently Used,最近最少使用)算法的啟發式方法來選擇要刷新的頁面。具體來說BGWriter會優先對最近最少被使用的臟頁進行回寫,即這些頁面在未來短時間內不太可能被再次訪問,因此將它們寫回到磁盤可以釋放內存而不大影響性能。如果一個頁面當前正被其他后端進程讀取或修改,那么BGWriter會跳過該頁面。除此之外,BGWriter的行為受bgwriter_lru_maxpages和bgwriter_lru_multiplier等參數的影響。每次循環中,BGWriter嘗試寫的最大臟頁數由bgwriter_lru_maxpages指定,而實際嘗試寫的頁數則根據系統近期需求動態調整,這取決于bgwriter_lru_multiplier。如果當前循環中已達到設定的最大寫入頁數限制,則即使還有未處理的臟頁,也會暫時跳過。
bgwriter_lru_maxpages參數比較容易理解,就是bgwriter一個批處理任務中刷臟的最大臟頁數量,刷夠了這些頁,bgwriter就結束這次任務,進入休眠。bgwriter_lru_multiplier 是一個用于調整PostgreSQL后臺寫入器(Background Writer, BGWriter)行為的重要參數。它決定了BGWriter嘗試預測需要刷新多少頁面到磁盤的積極程度,基于最近幾次前臺請求所需頁面數量進行計算。BGWriter會監控最近幾次前臺請求所需的新緩沖區頁面數,并將這個數值乘以 bgwriter_lru_multiplier 來決定下一次循環應該嘗試刷新多少個臟頁回到磁盤。假設最近幾次前臺請求所需的平均新緩沖區頁面數為N,則在下一個周期內,BGWriter將會嘗試刷新 N * bgwriter_lru_multiplier 個臟頁。這意味著如果 bgwriter_lru_multiplier 設置得較高,BGWriter就會更積極地嘗試將更多的臟頁寫回磁盤;反之,如果設置得較低,則BGWriter的行為會更加保守。
與BGWriter相對保守的刷臟策略相比,Checkpoint是一種強制刷臟機制。Checkpoint的主要目的是縮短崩潰恢復時間,因為它限定了恢復過程中需要回放的WAL(Write-Ahead Logging)日志的范圍。Checkpoint可以由多種條件觸發,包括達到checkpoint_timeout設定的時間間隔(默認為5分鐘),或WAL文件使用量達到了max_wal_size。此外,也可以手動觸發Checkpoint。當Checkpoint開始時,PostgreSQL會強制將所有臟數據頁寫回到磁盤。這包括但不限于共享緩沖區中的數據頁。為了保證一致性,Checkpoint期間不允許新的事務提交(盡管允許讀取和正在進行的事務繼續)。整個過程包括了標記一個檢查點記錄到WAL日志中,并更新控制文件以反映最新的檢查點信息。與BGWriter不同,Checkpoint刷臟的主要目的是提供一個已知的一致狀態點,以便于系統崩潰后快速恢復。通過定期創建這些一致狀態點,可以限制恢復過程中需要處理的日志量。
上面的刷臟機制還是比較容易理解的,PG中最不好理解的就是Backend也有寫臟塊的行為,而在其他數據庫中,這個工作完全是由后臺進程來完成的。當一個Backend進程需要讀取一個新的數據塊到共享緩沖區中,但當前沒有可用的空閑緩沖區時,就會觸發緩沖區替換機制(如使用 ClockSweep 算法)。如果選中的替換頁面是臟頁(Dirty Page),那么該頁面必須被寫回磁盤。
這個機制存在一個比較大的問題,那就是很可能一個SELECT操作操作會產生寫IO操作,從而讓某個SELECT操作的執行延時加大。我前兩天也談到Oracle也存在一些引起執行效率不穩定的行為。不過Oracle的這些行為發生的概率極低,甚至在一些負載不是特別大的系統中很少遇到。而在PG中我們經常會遇到這些情況的。
圖片
這是我們生產環境的一個PG 14數據庫,可以看出,真正由BGWriter刷臟的數量只有162,而Backend刷臟的數量為120萬塊。似乎BGWriter有點不務正業了。
這種情況的發生與PG數據庫的設計初衷有關,PG數據庫作為最為流行的開源數據塊之一,從開始并不是為高并發、高負載的企業級應用設計的,雖然這些年其企業級特性越來越多,但是一些根子上的問題還是沿用了早期的設計。為了適應早期的低成本IO設備,PG在性能上做出了大量的妥協。因為FULL PAGE WRITE機制的存在,導致了PG數據庫總是希望臟塊能夠盡可能比較晚地回寫到存儲系統中,Checkpoint也盡可能的不要太快。將刷臟工作分散到大量的Backend中去,也是對IO的一種妥協,一方面可以緩解Checkpoint過重對IO的影響,一方面也簡化了Backend在共享緩沖區不足時重用臟塊的處置方法。
曾經在和一個搞PG的朋友談到今天討論的問題的時候,他十分興奮地提出他的觀點,他認為PG這方面的設計十分優秀,十分巧妙。我倒是有些不同的觀點,認為這是PG數據庫成為高負載的企業級數據庫路上的一個必須優化的攔路虎。在現代硬件條件下,這方面的設計完全可以做優化了。我想隨著AIO/DIO等IO技術的引進,消除Backend寫臟塊的條件也逐步成熟了。
今天時間有限,我們先談到這里吧,明天我將繼續這個話題,討論一下,基于如此復雜的PG刷臟機制,DBA改如何去調整他們的數據庫,從而適應自己的業務場景。明天再聊吧。




























