PostgreSQL full_page_write機製
title: PgSQL · 特性分析· full_page_write機製
author: 康賢
PG默認每個page的大小為8K,PG數據頁寫入是以page為單位,但是在斷電等情況下,操作係統往往不能保證單個page原子地寫入磁盤,這樣就極有可能導致部分數據塊隻寫到4K(操作係統是一般以4K為單位),這些“部分寫”的頁麵包含新舊數據的混合。在崩潰後的恢複期間,在 xlog 裏麵存儲的記錄變化信息不夠完整,無法完全恢複該頁。PG為了解決這類問題,full_page_write機製孕育而生。
什麼是full_page_write?
PostgreSQL 在 checkpoint 之後在對數據頁麵的第一次寫的時候會將整個數據頁麵寫到 xlog 裏麵。當出現主機斷電或者OS崩潰時,redo操作時通過checksum發現“部分寫”的數據頁,並將xlog中保存的這個完整數據頁覆蓋當前損壞的數據頁,然後再繼續redo就可以恢複整個數據庫了。
除了能夠解決斷電等帶來壞數據頁問題外,full_page_write還應用在在線備份功能上。PG進行全量備份數據庫一般通過pg_basebackup工具實現,pg_basebackup類似於copy操作,在此期間,也會出現部分數據頁寫到一半時文件被copy走了,正是因為full_page_write存在,備份出來的數據庫才可以成功恢複啟動。所以即便full_page_write=off,在備份時也會被強製自動打開,保證備份成功。
實現原理
full_page_write主要在XLogInsert(插入一條xlog記錄)時發揮作用,通過full_page_writer開關狀態以及是否checkpoint後對數據頁麵的第一次修改(lsn<RedoRecPtr)判斷是否需要備份數據頁。如果需要備份,那麼則把數據頁存放在這條記錄的末尾,最終寫入到xlog中。
doPageWrites = Insert->fullPageWrites || Insert->forcePageWrites;
len = 0;
for (rdt = rdata;;)
{
if (rdt->buffer == InvalidBuffer)
{
/* Simple data, just include it */
len += rdt->len;
}
else
{
/* Find info for buffer */
for (i = 0; i < XLR_MAX_BKP_BLOCKS; i++)
{
if (rdt->buffer == dtbuf[i])
{
/* Buffer already referenced by earlier chain item */
if (dtbuf_bkp[i])
{
rdt->data = NULL;
rdt->len = 0;
}
else if (rdt->data)
len += rdt->len;
break;
}
if (dtbuf[i] == InvalidBuffer)
{
/* OK, put it in this slot */
dtbuf[i] = rdt->buffer;
if (doPageWrites && XLogCheckBuffer(rdt, true,
&(dtbuf_lsn[i]), &(dtbuf_xlg[i])))
{
dtbuf_bkp[i] = true;
rdt->data = NULL;
rdt->len = 0;
}
else if (rdt->data)
len += rdt->len;
break;
}
}
if (i >= XLR_MAX_BKP_BLOCKS)
elog(PANIC, "can backup at most %d blocks per xlog record",
XLR_MAX_BKP_BLOCKS);
}
/* Break out of loop when rdt points to last chain item */
if (rdt->next == NULL)
break;
rdt = rdt->next;
}
在redo恢複的時候隻要數據塊有備份,那麼就是用備份的數據。
/* If we have a full-page image, restore it and we're done */
if (record->xl_info & XLR_BKP_BLOCK(0))
{
(void) RestoreBackupBlock(lsn, record, 0, false, false);
return;
}
full_page_write不足之處
因為full_page_write需要在xlog中記錄數據頁,會寫更多xlog文件,不僅有數據變化信息,還有數據頁本身信息,這樣會增加額外的IO和磁盤消耗,同時也會引起主備延遲變大。
為了優化full_page_write,社區提供了一個patch,它的主要設計是創建兩個共享內存塊隊列,checkpoint專用buffer隊列和非checkpoint專用buffer隊列,同時關閉full_page_write。當用戶DML產生的數據buffer需要刷盤時,並不是立即刷到磁盤,而是先進入double write的buffer隊列,當buffer隊列滿時,則將buffer隊列裏麵的數據首先刷到特別的double write文件,然後再將數據刷到數據庫文件。通過這種設計就不需要在checkpoint 之後在對數據頁麵的第一次寫的時候會將整個數據頁麵寫到 xlog 裏麵。當數據庫需要恢複的時候,遍曆所有double write文件裏麵的記錄塊,找到每個記錄塊對應的數據庫page,然後對這個page進行checksum,如果page損壞,那麼直接把記錄塊裏麵的內容覆蓋到buffer數據。最後把double write文件刪除,重新初始化buffer隊列。
總結
把full_page_write這個選項關閉會提高數據庫執行速度以及減少xlog數量,但是可能導致係統崩潰或者掉電之後的數據庫損壞。 如果有減小部分頁麵寫入風險的硬件支持(比如電池供電的磁盤控製器), 或者文件係統支持(能夠保證page寫入原子性),可以把風險降低到一個可以接受的範圍, 那麼可以考慮關閉這個選項,其他情況下建議打開這個選擇。
最後更新:2017-04-01 13:44:33