天天看點

資料庫核心月報 - 2015 / 09-PgSQL · 特性分析 · 談談checkpoint的排程

在pg的衆多參數中,參數checkpoint相關的幾個參數頗為神秘。這些參數與checkpoint的排程有關,對系統的穩定性還是比較重要的,下面我們為大家解析一下,這要先從pg的資料同步機制談起。

衆所周知,資料庫的背景程序在執行使用者事務時,發生的資料更改是先寫入緩沖池中,對應pg就是shared buffers。pg的緩沖池一般設定為總記憶體的1/4左右,緩沖池裡面的這些資料更改,在事務送出時,是無需同步寫入到磁盤的。因為在事務送出時,會先寫入wal日志,有了wal日志,就可以在異常情況下将資料恢複,保障資料安全,是以資料本身是否在送出時寫入磁盤就沒那麼重要了。pg是隻是在需要的時候,例如髒頁較多時、或一定時間間隔後,才将資料寫回磁盤。

髒頁處理的過程分為幾個步驟。首先是由background writer将shared buffers裡面的被更改過的頁面(即髒頁),通過調用write寫入作業系統page cache。在函數bgbuffersync可以看到,pg的background writer程序,會根據lru連結清單,掃描shared buffers(實際上是每次掃描一部分),如果發現髒頁,就調用系統調用write。可以通過設定bgwriter_delay參數,來控制background writer每次掃描之間的時間間隔。background writer在對一個頁面調用write後,會将該頁面對應的檔案(實際上是表的segement,每個表可能有多個segment,對應多個實體檔案)記錄到共享記憶體的數組<code>checkpointershmem-&gt;requests</code>中,調用順序如下:

這些request最終會被checkpointer程序讀取,放入<code>pendingopstable</code>,而真正将髒頁回寫到磁盤的操作,是由checkpointer程序完成的。checkpointer每次也會調用smgrwrite,把所有的shared buffers髒頁(即還沒有被background writer清理過得髒頁)寫入作業系統的page cache,并存入<code>pendingopstable</code>。這樣<code>pendingopstable</code>存放了所有write過的髒頁,包括之前background writer已經處理的髒頁。随後pg的checkpointer程序會根據<code>pedingopstable</code>的記錄,進行髒頁回寫操作(注意每次調用fysnc,都會sync資料表的一個檔案,檔案中所有髒頁都會寫入磁盤),調用順序如下:

如果checkpointer做磁盤寫入的頻率過高,則每次可能隻寫入很少的資料。我們知道,磁盤對于順序寫入批量資料比随機寫的效率要高的多,每次寫入很少資料,就造成大量随機寫;而如果我們放慢checkpoint的頻率,多個随機頁面就有可能組成一次順序批量寫入,效率大大提高。另外,checkpoint會進行fsync操作,大量的fsync可能造成系統io阻塞,降低系統穩定性,是以checkpoint不能過于頻繁。但checkpoint的間隔也不能無限制放大。因為如果出現系統當機,在進行恢複時,需要從上一次checkpoint的時間點開始恢複,如果checkpoint間隔過長,會造成恢複時間緩慢,降低可用性。整個同步機制如下圖所示:

資料庫核心月報 - 2015 / 09-PgSQL · 特性分析 · 談談checkpoint的排程

圖1. 資料同步機制

那麼如何排程checkpoint,即控制checkpoint的間隔呢?pg提供了幾個參數:<code>checkpoint_segments</code>、<code>checkpoint_completion_target</code>和<code>checkpoint_timeout</code>。

決定是否做checkpoint有兩個名額次元:

系統的資料修改量。

評估修改量,有兩種方法:一種是記錄shared buffer裡面的髒頁有多少,占所有buffer的多大比例;另外一種,記錄使用者事務的資料修改量是多少。如果用系統的髒頁數量或所占比例,來評估修改量,會不太準确,使用者有可能反複修改相同的頁面,髒頁不多,但實際修改量很大,這時候也是應該盡快進行checkpoint,減少恢複時間的。而通過記錄wal日志的産生量,可以很好的評估這個修改量,是以就有了<code>checkpoint_segments</code>這個參數,它用于指定産生多少wal日志後,進行一次checkpoint。例如設定為16時,産生16個wal日志檔案後(如果每個日志檔案的大小為16m,即産生16*16m位元組的日志),進行一次checkpoint。判斷是否觸發checkpoint的調用如下:

距離上一次checkpoint的時間。

也就是在上一次checkpoint後,多長時間必須做一次checkpoint。pg提供了<code>checkpoint_timeout</code>這個參數,預設值為300秒,即如果上一次checkpoint後過了300秒沒有做checkpoint了,就強制做一次checkpoint。

那麼另外一個參數<code>checkpoint_completion_target</code>是做什麼的呢?

這個看似不起眼的參數其實對checkpoint排程的影響很大。它是怎麼使用的呢?checkpoint會調用buffersync,将所有shared buffers的頁面掃描一遍,如果發現髒頁即調用write,寫入page cache。每次write完一個髒頁後,會調用<code>ischeckpointonschedule()</code>這個函數。這個函數的主要邏輯是,判斷新産生的日志檔案數除以<code>checkpoint_segments</code>,結果是否小于<code>checkpoint_completion_target</code>。注意,這裡的新産生日志檔案數,是checkpoint開始後新産生的日志數,不是從上一次checkpoint結束後的新日志數。如果<code>ischeckpointonschedule()</code>傳回true,則checkpointer程序會進行sleep,sleep一定時間後,再讀取下一個shared buffers頁面進行write。這樣做的效果是,當所有頁面write完成時,新産生的日志頁面數占<code>checkpoint_segements</code>的比例約為<code>checkpoint_completion_target</code>的設定值。例如,如果<code>checkpoint_segements</code>為16,<code>checkpoint_completion_target</code>為0.9,則當上一次checkpoint後,新的第16個日志檔案産生後,寫日志的那個程序會觸發一次checkpoint。checkpoiter程序随即調用<code>createcheckpoint</code>,做一次checkpoint,checkpointer程序會調用<code>buffersync</code>,掃描shared buffers寫髒頁。此時每次write一個髒頁後,如果新産生的日志檔案數小于16*0.9,即15個日志檔案時,會進行sleep。最後當write髒頁完成時,從上次checkpoint開始新産生的日志檔案約為16+15=31個,即

由此可見,<code>checkpoint_completion_target</code>直接控制了checkpoint中的write髒頁的速度,使其完成時新産生日志檔案數為上述期望值。

除了日志檔案數,<code>ischeckpointonschedule()</code>還會檢查從checkpoint開始到現在的時間占<code>checkpoint_timeout</code>的比例,是否小于<code>checkpoint_completion_target</code>,以決定是否sleep。按<code>checkpoint_completion_target</code>為0.9,<code>checkpoint_timeout</code>為300秒計算,髒頁write的完成時間距離checkpoint開始的時間,大約是270秒。實際上,這個時間上的限制和産生日志檔案數的限制是同時起作用的。

當髒頁全部被write完,就要進行真正的磁盤操作了,即fsync。此時每個檔案的fsync之間沒有sleep,是盡快完成的。一般做fsync總時間不會超過10秒,是以會趕在時間間隔到達<code>checkpoint_timeout</code>或新日志檔案數到達<code>checkpoint_segments</code>前(都從checkpoint開始時間點開始算起)結束此次checkpoint。

總結起來,每次checkpoint所耗時間可以用下面的公式計算:

比如上面的例子,将會是:

而這個時間一般小于産生<code>checkpoint_segments</code>個日志或<code>checkpoint_timeout</code>的時間。這樣綜合的效果是,每産生<code>checkpoint_segments</code>個日志或經曆<code>checkpoint_timeout</code>的時間做一次checkpoint。在兩次checkpoint的開始時間之間,會在<code>checkpoint_completion_target</code>比例的時間點完成髒頁write,随後很快進行完fsync,如下圖所示:

資料庫核心月報 - 2015 / 09-PgSQL · 特性分析 · 談談checkpoint的排程

圖2. checkpoint過程

以上便是checkpoint的排程機制。我們要注意調整上述幾個參數時,不要讓checkpoint産生過于頻繁,否則頻繁的fsync操作會是系統不穩定。比如,<code>checkpoint_segments</code>一般設定為16個以上,<code>checkpoint_completion_target</code>設為0.9,<code>checkpoint_timeout</code>為300秒,這樣一般checkpoint的間隔能達到1分鐘以上。