天天看點

PgSQL · 特性分析 · 資料庫崩潰恢複(上)

為了合并i/o提高性能,postgresql資料庫引入了共享緩沖區,當資料庫非正常關閉,比如伺服器斷電時,共享緩沖區即記憶體中的資料就會丢失,這個時候資料庫作業系統重新開機時就需要從非正常狀态中恢複過來,繼續提供服務。本文将具體分析在這種情況下,postgresql資料庫如何從崩潰狀态中恢複。

資料庫作業系統如何識别到自己是非正常狀态(崩潰狀态)

資料庫如何找到合适的checkpoint作為基礎

為什麼應用xlog日志可以恢複資料庫資料

資料庫如何應用xlog日志

在postgresql中,把資料庫分為以下幾種狀态:

postgresql的資料庫狀态被存儲在pg_control檔案中,可以執行pg_controldata指令,檢視目前的資料庫狀态,傳回結果如下:

其中 database cluster state: shut down指明目前資料庫的狀态為db_shutdowned,即正常關閉狀态。

pg_control檔案由對應的結構體controlfiledata存儲,controlfiledata資料結構如下:

每次postgresql資料庫啟動時,會讀取pg_control檔案擷取最後一次操作後的資料庫狀态,如果為非正常關閉狀态(db_shutdowned),則會執行崩潰恢複邏輯。

當資料庫意識到自己處于崩潰狀态後,會去選擇一個合适的checkpoint作為基礎,不斷應用在這之後的xlog日志。在postgresql中,最近一次檢查點的資訊會被存儲在pg_control檔案中,pg_control由對應的結構體controlfiledata存儲,controlfiledata資料結構如下:

在資料庫崩潰恢複過程中,一般會選取最近一次的檢查點作為恢複的基礎,但是因為一個檢查點的時間比較長,是以有可能資料庫系統在檢查點做完之前崩潰,這樣磁盤上的檢查點可能是不完全的,是以postgresql資料庫會多存儲一個檢查點的位置,即prevcheckpoint。

在資料庫崩潰恢複過程中,postgresql規定了三個在啟動之前必須恢複到的最小位點:

minrecoverypoint

資料庫在歸檔恢複過程中,minrecoverypoint被更新為最新被重新整理到磁盤的lsn。每次資料庫啟動時必須已經replay該位置的xlog日志記錄。

backupstartpoint

資料庫線上備份開始時,會調用pg_start_backup函數執行一次checkpoint,并生成backup_label檔案。當使用線上備份集進行恢複時,backupstartpoint就是上述checkpoint記錄對應的lsn,當達到了該lsn,該值置為0,在置為0之前,資料庫不能啟動。該值被記錄在backup_label檔案中如下,直到線上備份結束,pg_stop_backup将該檔案删除。這樣就保證了在備份過程中,資料庫崩潰了,可以預設從備份開始時的日志檢查點開始恢複。

backupendpoint

當資料庫從一個備庫做的線上備份集進行恢複時,backupendpoint表示備份結束的lsn,當達到該lsn,該值置為0,在置為0之前,資料庫不能啟動。

在恢複過程中,使用者可以通過使用recovery.conf檔案來指定恢複的各個參數,如下:

歸檔恢複設定

restore_command:用于擷取一個已歸檔段的xlog日志檔案的指令

archive_cleanup_command:清除不在需要的xlog日志檔案的指令

recovery_end_command:歸檔恢複結束後執行的指令

恢複目标設定(預設情況下,資料庫将會一直恢複到 wal 日志的末尾)

recovery_target = ’immediate’:在從一個線上備 份中恢複時,這意味着備份結束的那個點

recovery_target_name (string):這個參數指定(pg_create_restore_point()所建立)的已命名的恢複點,将恢複到該恢複點

recovery_target_time (timestamp):這個參數指定恢複到的時間戳

recovery_target_xid (string):這個參數指定恢複到的事務 id

recovery_target_inclusive (boolean):指定是否在指定的恢複目标之後停止(true),或者在恢複目标之前停止 (false);适用于recovery_target_time或者recovery_target_xid被指定的情況;這個設定分别控制事務是否有準确的目标送出時間或 id 是否将被包括在該恢複中;預設值為 true

recovery_target_timeline (string):指定恢複到一個特定的時間線

recovery_target_action (enum):指定在達到恢複目标時伺服器應該立刻采取的動作,包括pause(暫停)、promote(接受連接配接)、shutdown(停止伺服器),其中pause為預設動作

備庫參數設定

standby_mode(boolean):為on表示作為一個備庫,否則不為備庫

primary_conninfo (string):指定備庫連接配接主庫的連接配接字元串

primary_slot_name (string):通過流複制指定主庫的一個複制槽來複制主庫資料,如果沒有設定primary_conninfo,則此參數無效

trigger_file (string):指定一個觸發器檔案,該檔案存在可以結束備庫的恢複,即更新備庫為一個獨立的主庫

recovery_min_apply_delay (integer):這個參數允許将恢複延遲一段固定的時間,如果沒有指定機關則以毫秒為機關。

如果recovery.conf中同時指定了recoverytargetxid、recoverytargetname、recoverytargettime時,postgresql會按照recovery_target_xid> recovery_target_name > recovery_target_time的優先級來擷取最終的目标恢複位點。

如果在recovery.conf指定recovery_targettimeline為latest,則可以基于目前timelineid為起點尋找最新時間線:

尋找目前timelineid的時間線曆史檔案“xxx.history”,如果存在則繼續尋找,否則錯誤退出

timelineid是線性增長的,将目前timelineid自增1尋找是否存在時間線曆史檔案,直到不存在對應的時間線曆史檔案為止,即可找到最新的時間線。

概括起來,xlog日志分為多個xlog邏輯日志檔案,每個邏輯日志檔案包含多個xlog段檔案,每個xlog段檔案包含多個xlog日志頁:

每個xlog邏輯日志檔案都有一個id

實際xlog被分為pg_xlog目錄下多個大小為16mb的段檔案

檔案名由時間線timelineid(8位16進制)、邏輯日志檔案号(8位16進制)和段檔案id(8位16進制)組成

每個段檔案分為多個8kb的頁(塊)

每個頁包含一個頭部,頭部資訊之後才是真正的xlog日志記錄

其中,值得注意的是,每個xlog段檔案大小可以在編譯時使用–with-wal-segsize參數來指定,每頁的大小可以在編譯的時使用–with-wal-blocksize參數來指定,接下來主要介紹xlog日志每頁的組織形式。

在postgresql中,xlog日志頁可以分為以下幾部分:

組成部分

具體含義

pageheaderdata

xlog日志頁面頭部資訊

xlogrecord

xlog日志記錄的頭部資訊

data of rmgr

資料總管的資料,長度xl_len

backup block 0

備份資料塊頭部bkpblock + 塊大小的備份資料

backup block 1

backup block 2

backup block 3

每個xlog日志頁分為頁面頭部資訊和日志記錄,其頭部資訊xlogpageheaderdata結構如下:

其中,xlp_info是标志位:

0x0001表示該頁面包含一個跨頁面的記錄(上個頁面的最後一條記錄)

0x0002表示該頁面為段檔案的首個頁面,頭部是一個長頭部

0x0004表示該頁面備份資料塊是可選的

如果目前的頁面沒有足夠的空間來存儲一個xlog日志記錄,系統允許将剩餘的資料存儲到下一個頁面,但是xlog日志記錄的頭部資訊,即後文中的xlogrecord是不允許分開存儲到兩個不同的頁面的。

如果該頁面為段檔案的首個頁面,除了上面的标準頁面頭部資訊外,還增加一個長頭部用來更精确地定位檔案,即xloglongpageheaderdata:

#### xlog日志記錄的頭部資訊

每個xlog日志頁面頭部之後才是真正的xlog日志記錄,xlogrecord記錄了xlog的相關資料資訊,具體結構如下:

其中,xl_rmid表示資料總管id,在postgresql中,資料總管根據資源種類,可以分為17類,其分别的id按照以下順序分别為0-16:

其中,上述引用代碼中pg_rmgr函數的參數依次為:

參數名稱

symname

資料總管id

name

資源名稱

redo

redo恢複函數

desc

描述函數

startup

啟動函數

cleanup

清理函數

在postgresql中,用xl_rmid和xl_info高4位來唯一地标示該xlog日志記錄對應的資料庫操作,例如事務資料總管(rm_xact_id),對應xlogrecord中xl_info字段高4位:

例如元組管理器(rm_heap_id),對應xl_info的高4位:

xl_info字段是個xl_info低4位表示目前xlog記錄資料塊備份的情況:

當日志記錄涉及到的緩沖區buffer從上個checkpoint後第一次被修改,則将該buffer備份附加到xlog日志的備份塊iblk中,對應修改xl_info的xlr_bkp_block(iblk)位。這是為了保證每個寫入到磁盤的資料都是完整的頁,當寫入某個整頁的過程中出現崩潰,即寫入的頁面不是完整的,則可以從xlog日志中知直接将備份塊恢複過來。

除此之外,xlogrecord的xl_crc記錄xlog日志記錄的crc校驗,保證寫入到磁盤的xlog記錄都是完整的,如果應用不完整的日志記錄,postgresql會報錯。

xlog日志記錄的資料總管資料由一系列xlogrecdata結構體連結清單組成,之是以要用xlogrecdata鍊,是因為在所要處理的日志記錄實體資料在記憶體空間可能不是連續存儲的,而且資料可能分布在多個緩沖區内,需要用xlogrecdata連結清單将它們組織起來。xlogrecdata資料結構如下:

其中,buffer_std該值為true,則容許xlog釋放備份頁的空閑空間,空閑空間由pd_lower和pd_upper限定:

pd_lower表示頁面起始位置與未配置設定空間開頭的位元組偏移

pd_upper表示頁面末尾位置與未配置設定空間末尾的位元組偏移

PgSQL · 特性分析 · 資料庫崩潰恢複(上)

可以看出,根據xlogrecdata的資訊,我們很容易恢複出對應的資料。

備份資料塊包含一個頭部資訊bkpblock和一塊大小的備份資料,其中bkpblock結構如下:

如果需要備份的塊存在空洞,則備份的時候隻記錄這個空洞的偏移量和長度,但沒有實際備份它,進而提高備份效率。

備份資料塊頭部後緊跟一個塊大小的備份資料,該塊可以在資料庫崩潰恢複時直接恢複。

每次postmaster程序啟動時,都會調用startupxlog函數對資料庫崩潰進行恢複,由于該過程非常繁瑣,為了更好的了解,本文把redo恢複分為三個階段:

- redo恢複前

- redo恢複中

- redo恢複後

該階段主要是根據資料庫目前狀态判斷是否需要恢複,如果需要則擷取恢複的起始位點以及目标恢複時間線(recoverytargettli);如不需要則正常啟動系統。該階段具體操作如下:

讀取控制檔案pg_control,根據檔案中的資訊設定恢複參數

檢查pg_xlog和pg_xlog/archive_status檔案夾是否存在

讀取配置檔案recovery.conf,根據檔案中的資訊設定恢複參數

讀出時間線曆史記錄中的時間線清單expectedtlis,如果recoverytargettli不在時間線清單expectedtlis中,則系統報錯

檢測是否存在backup_label檔案,如果存在,則從備份标記定義的檢查點(checkpoint location)讀取檢查點的記錄到record中

a. 若record不空,則從record中的檢查點記錄為恢複起始位置,參數inrecovery參數設定為true

b. 若record為空,則系統報錯

如果不存在backup_label檔案,讀取pg_control檔案中的最近一次檢查點,并把它的記錄讀到record中。

a. 若record不空,則從record中的檢查點記錄為恢複起始位置

b. 若record為空,則讀取最近一次檢查點的前面一次檢查點(prevcheckpoint),并把它的記錄讀到record中

c. 如果新record不為空,把參數inrecovery參數設定為true,否則系統報錯

把record記錄中的值賦給一個檢查點結構體變量checkpoint,checkpoint的nextxid和nextoid賦給共享緩沖區中的變量緩沖區shmemvariablecache的nextxid和nextoid。把checkpoint的時間線id賦給thistimelineid

在checkpoint的redo指針和undo指針有效的情況下,把參數inrecovery參數設定為true。

pg_control中資料庫狀态不是db_shutdowned(系統正常關閉)時,把參數inrecovery參數設定為true

pg_control中參數inarchiverecovery為真,把參數inrecovery設定為true

當參數inrecovery的值為true時,執行恢複

總結起來,在postgresql中,如果啟動時遇到以下情況,需要進行恢複操作:

pg_control中的資料庫狀态不正常(非db_shutdowned)

pg_control中記錄的最新檢查點讀取不到xlog日志檔案

通過指定recovery.conf檔案,指定歸檔恢複

其中第三種情況是使用者通過配置檔案recovery.conf手動控制恢複過程。

上個階段主要是做redo恢複之前的準備工作,确定恢複起始的位置,而本階段主要是基于上個階段,進行真正的恢複操作:

初始化恢複環境,啟動各種需要恢複的資源,即調用對應資料總管的啟動函數:

設定需要redo的日志記錄的起始位置(離上個階段checkpoint最近的一條日志記錄),把起始位置處的日志記錄讀入record,進入循環,不斷地進行redo操作

a. 如果record不空,從record開始循環執行redo操作,處理完一條需要redo的記錄,即調用對應資料總管的redo操作:

b. 如果record為空,不需要進行redo操作

讀取下一條記錄到record中,不斷進行redo操作,直到執行到了我們所要求的時間線的位置,或者已經把所有的日志記錄中需要redo的record執行完畢

這個階段主要是對redo恢複的環境進行清理,并啟動需要的輔助程序。

本次恢複結束之際,确定是否需要再設定一個新的時間線。如果是恢複到某個指定的時間點上而不是全部恢複,則生成一個新的時間線

更新xlogctl控制結構體中的recoverylastrecptr

當參數inrecovery的值為true時,執行初始環境的清理工作,調用:

執行createcheckpoint,強迫恢複的内容刷到磁盤

調用preaaalocxlogfiles為新日志記錄重新配置設定日志段檔案,同時釋放日志恢複時申請的記憶體。調用shutdownrecoverytransactionenvironment關閉恢複環境。再次更新控制結構體controlfile、xlogctl等。

開始啟動clog、prepared transactions需要的資源或環境等内容,為恢複結束後、系統正常運作做準備工作

startupxlog結束,系統正常啟動

至此,我們分析了postgresql資料庫在崩潰時恢複的具體過程,其中具體的redo恢複過程,實際上是通過資料總管擷取對應的redo函數接口來執行恢複操作,每種資料總管其處理過程不盡相同,這裡我們不再一一介紹,後面的月報我們會去分析各種資源的redo函數具體操作。