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