PgSQL · 特性分析 · 數據庫崩潰恢複(上)
背景
為了合並I/O提高性能,PostgreSQL數據庫引入了共享緩衝區,當數據庫非正常關閉,比如服務器斷電時,共享緩衝區即內存中的數據就會丟失,這個時候數據庫操作係統重啟時就需要從非正常狀態中恢複過來,繼續提供服務。本文將具體分析在這種情況下,PostgreSQL數據庫如何從崩潰狀態中恢複。
上期月報PgSQL · 特性分析 · checkpoint機製淺析中介紹了PostgreSQL中的checkpoint機製。其中提到,當PostgreSQL數據庫崩潰恢複時,會以最近的checkpoint為基礎,不斷應用這之後的XLOG日誌。為了更好地理解PostgreSQL數據庫從崩潰中恢複的過程,我們需要弄清楚以下幾個問題:
- 數據庫操作係統如何識別到自己是非正常狀態(崩潰狀態)
- 數據庫如何找到合適的checkpoint作為基礎
- 為什麼應用XLOG日誌可以恢複數據庫數據
- 數據庫如何應用XLOG日誌
數據庫狀態
在PostgreSQL中,把數據庫分為以下幾種狀態:
typedef enum DBState
{
DB_STARTUP = 0,/*數據庫啟動*/
DB_SHUTDOWNED,/*數據庫正常關閉*/
DB_SHUTDOWNED_IN_RECOVERY,/*數據庫在恢複時關閉*/
DB_SHUTDOWNING,/*數據庫啟動到正常關閉過程中崩潰*/
DB_IN_CRASH_RECOVERY,/*數據庫在恢複過程中崩潰*/
DB_IN_ARCHIVE_RECOVERY,/*數據庫處於歸檔恢複*/
DB_IN_PRODUCTION/*數據庫處於正常工作狀態,等待接受事務處理*/
} DBState;
PostgreSQL的數據庫狀態被存儲在pg_control文件中,可以執行pg_controldata命令,查看當前的數據庫狀態,返回結果如下:
pg_control version number: 942
Catalog version number: 201409291
Database system identifier: 6403125794625722170
Database cluster state: shut down
...
其中 Database cluster state: shut down指明當前數據庫的狀態為DB_SHUTDOWNED,即正常關閉狀態。
pg_control文件由對應的結構體ControlFileData存儲,ControlFileData數據結構如下:
typedef struct ControlFileData
{
uint64 system_identifier; /*唯一係統標識符——保證控製文件和產生XLOG文件的數據庫一致*/
uint32 pg_control_version; /* 標識pg_control的版本*/
uint32 catalog_version_no; /*標識catalog的版本 */
DBState state; /*最後一次操作後的數據庫狀態 */
pg_time_t time; /*pg_control最近一次更新的時間時*/
...
pg_crc32 crc;
} ControlFileData;
每次PostgreSQL數據庫啟動時,會讀取pg_control文件獲取最後一次操作後的數據庫狀態,如果為非正常關閉狀態(DB_SHUTDOWNED),則會執行崩潰恢複邏輯。
checkpoint相關結構
ControlFileData結構
當數據庫意識到自己處於崩潰狀態後,會去選擇一個合適的checkpoint作為基礎,不斷應用在這之後的XLOG日誌。在PostgreSQL中,最近一次檢查點的信息會被存儲在pg_control文件中,pg_control由對應的結構體ControlFileData存儲,ControlFileData數據結構如下:
typedef struct ControlFileData
{
...
XLogRecPtr checkPoint; /*指向最近一次的檢查點位置*/
XLogRecPtr prevCheckPoint; /*指向最近一次檢查點的前一次檢查點的位置*/
CheckPoint checkPointCopy; /*最近一次檢查點控製信息的副本*/
XLogRecPtr minRecoveryPoint; /*歸檔恢複時必須恢複到的最小LSN*/
XLogRecPtr backupStartPoint; /*在線備份時進行的檢查點開始LSN*/
XLogRecPtr backupEndPoint; /*在線備份時進行的檢查點結束LSN*/
bool backupEndRequired; /* 用於判斷是否基於正確的在線備份集恢複*/
TimeLineID minRecoveryPointTLI; /* 必須恢複到的最小時間線 */
...
pg_crc32 crc;
} 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將該文件刪除。這樣就保證了在備份過程中,數據庫崩潰了,可以默認從備份開始時的日誌檢查點開始恢複。
``` START WAL LOCATION: 0/6000020 (file 000000040000000000000006) CHECKPOINT LOCATION: 0/6000020 BACKUP METHOD: pg_start_backup BACKUP FROM: master START TIME: 2017-05-15 10:18:55 HKT LABEL: zhuodao ```
-
- backupEndPoint
- 當數據庫從一個備庫做的在線備份集進行恢複時,backupEndPoint表示備份結束的LSN,當達到該LSN,該值置為0,在置為0之前,數據庫不能啟動。
recovery.conf文件
在恢複過程中,用戶可以通過使用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日誌中詳細地記錄了服務進程對數據庫的操作過程。之前的月報PgSQL · 特性分析 · Write-Ahead Logging機製淺析介紹過PostgreSQL WAL機製的實現,下麵將具體介紹XLOG日誌的組織結構。
概括起來,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日誌每頁的組織形式。
XLOG日誌頁的組織形式
在PostgreSQL中,XLOG日誌頁可以分為以下幾部分:
組成部分 | 具體含義 |
---|---|
PageHeaderData | XLOG日誌頁麵頭部信息 |
XLogRecord | XLog日誌記錄的頭部信息 |
Data of RMGR | 資源管理器的數據,長度xl_len |
Backup Block 0 | 備份數據塊頭部BkpBlock + 塊大小的備份數據 |
Backup Block 1 | 備份數據塊頭部BkpBlock + 塊大小的備份數據 |
Backup Block 2 | 備份數據塊頭部BkpBlock + 塊大小的備份數據 |
Backup Block 3 | 備份數據塊頭部BkpBlock + 塊大小的備份數據 |
XLOG日誌頁頭部信息
每個XLOG日誌頁分為頁麵頭部信息和日誌記錄,其頭部信息XLogPageHeaderData結構如下:
typedef struct XLogPageHeaderData
{
uint16 xlp_magic; /* 校驗位,用於識別不同的XLOG版本 */
uint16 xlp_info; /* flag bits, see below */
TimeLineID xlp_tli; /* 頁麵第一條記錄的時間線 */
XLogRecPtr xlp_pageaddr; /* XLOG頁麵的首地址 */
uint32 xlp_rem_len; /* 前XLOG頁麵最後一條記錄剩餘的長度 */
} XLogPageHeaderData;
其中,xlp_info是標誌位:
- 0x0001表示該頁麵包含一個跨頁麵的記錄(上個頁麵的最後一條記錄)
- 0x0002表示該頁麵為段文件的首個頁麵,頭部是一個長頭部
- 0x0004表示該頁麵備份數據塊是可選的
如果當前的頁麵沒有足夠的空間來存儲一個XLOG日誌記錄,係統允許將剩餘的數據存儲到下一個頁麵,但是XLog日誌記錄的頭部信息,即後文中的XLogRecord是不允許分開存儲到兩個不同的頁麵的。
如果該頁麵為段文件的首個頁麵,除了上麵的標準頁麵頭部信息外,還增加一個長頭部用來更精確地定位文件,即XLogLongPageHeaderData:
typedef struct XLogLongPageHeaderData
{
XLogPageHeaderData std; /* 標準頁麵頭部信息 */
uint64 xlp_sysid; /* pg_control 中的係統標識符*/
uint32 xlp_seg_size; /* 段的尺寸 */
uint32 xlp_xlog_blcksz; /* 頁(塊)的尺寸*/
} XLogLongPageHeaderData;
#### XLOG日誌記錄的頭部信息
每個XLOG日誌頁麵頭部之後才是真正的XLOG日誌記錄,XLogRecord記錄了XLOG的相關數據信息,具體結構如下:
typedef struct XLogRecord
{
uint32 xl_tot_len; /* 整條記錄的總長度*/
TransactionId xl_xid; /* 事務ID */
XLogRecPtr xl_prev; /* 上條XLOG日誌記錄的位置(LSN) */
uint8 xl_info; /* flag bits, see below */
RmgrId xl_rmid; /* 資源管理器ID */
/* 2 bytes of padding here, initialize to zero */
pg_crc32c xl_crc; /* 本記錄的CRC校驗碼 */
/* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
} XLogRecord;
其中,xl_rmid表示資源管理器ID,在PostgreSQL中,資源管理器根據資源種類,可以分為17類,其分別的ID按照以下順序分別為0-16:
PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, NULL, NULL)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, NULL, NULL)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, NULL, NULL)
PG_RMGR(RM_DBASE_ID, "Database", dbase_redo, dbase_desc, NULL, NULL)
PG_RMGR(RM_TBLSPC_ID, "Tablespace", tblspc_redo, tblspc_desc, NULL, NULL)
PG_RMGR(RM_MULTIXACT_ID, "MultiXact", multixact_redo, multixact_desc, NULL, NULL)
PG_RMGR(RM_RELMAP_ID, "RelMap", relmap_redo, relmap_desc, NULL, NULL)
PG_RMGR(RM_STANDBY_ID, "Standby", standby_redo, standby_desc, NULL, NULL)
PG_RMGR(RM_HEAP2_ID, "Heap2", heap2_redo, heap2_desc, NULL, NULL)
PG_RMGR(RM_HEAP_ID, "Heap", heap_redo, heap_desc, NULL, NULL)
PG_RMGR(RM_BTREE_ID, "Btree", btree_redo, btree_desc, NULL, NULL)
PG_RMGR(RM_HASH_ID, "Hash", hash_redo, hash_desc, NULL, NULL)
PG_RMGR(RM_GIN_ID, "Gin", gin_redo, gin_desc, gin_xlog_startup, gin_xlog_cleanup)
PG_RMGR(RM_GIST_ID, "Gist", gist_redo, gist_desc, gist_xlog_startup, gist_xlog_cleanup)
PG_RMGR(RM_SEQ_ID, "Sequence", seq_redo, seq_desc, NULL, NULL)
PG_RMGR(RM_SPGIST_ID, "SPGist", spg_redo, spg_desc, spg_xlog_startup, spg_xlog_cleanup)
其中,上述引用代碼中PG_RMGR函數的參數依次為:
參數名稱 | 具體含義 |
---|---|
symname | 資源管理器ID |
name | 資源名稱 |
redo | redo恢複函數 |
desc | 描述函數 |
startup | 啟動函數 |
cleanup | 清理函數 |
在PostgreSQL中,用xl_rmid和xl_info高4位來唯一地標示該XLOG日誌記錄對應的數據庫操作,例如事務資源管理器(RM_XACT_ID),對應XLogRecord中xl_info字段高4位:
#define XLOG_XACT_COMMIT 0x00
#define XLOG_XACT_PREPARE 0x10
#define XLOG_XACT_ABORT 0x20
#define XLOG_XACT_COMMIT_PREPARED 0x30
#define XLOG_XACT_ABORT_PREPARED 0x40
#define XLOG_XACT_ASSIGNMENT 0x50
#define XLOG_XACT_COMMIT_COMPACT 0x60
例如元組管理器(RM_HEAP_ID),對應xl_info的高4位:
#define XLOG_HEAP_INSERT 0x00
#define XLOG_HEAP_DELETE 0x10
#define XLOG_HEAP_UPDATE 0x20
/* 0x030 is free, was XLOG_HEAP_MOVE */
#define XLOG_HEAP_HOT_UPDATE 0x40
#define XLOG_HEAP_NEWPAGE 0x50
#define XLOG_HEAP_LOCK 0x60
#define XLOG_HEAP_INPLACE 0x70
xl_info字段是個xl_info低4位表示當前XLOG記錄數據塊備份的情況:
#define XLR_BKP_BLOCK_MASK 0x0F /* all info bits used for bkp blocks */
#define XLR_MAX_BKP_BLOCKS 4
#define XLR_BKP_BLOCK(iblk) (0x08 >> (iblk)) /* iblk in 0..3 */
當日誌記錄涉及到的緩衝區Buffer從上個checkpoint後第一次被修改,則將該Buffer備份附加到XLOG日誌的備份塊iblk中,對應修改xl_info的XLR_BKP_BLOCK(iblk)位。這是為了保證每個寫入到磁盤的數據都是完整的頁,當寫入某個整頁的過程中出現崩潰,即寫入的頁麵不是完整的,則可以從XLOG日誌中知直接將備份塊恢複過來。
除此之外,XLogRecord的xl_crc記錄XLOG日誌記錄的CRC校驗,保證寫入到磁盤的XLOG記錄都是完整的,如果應用不完整的日誌記錄,PostgreSQL會報錯。
XLOG日誌記錄的資源管理器數據
XLOG日誌記錄的資源管理器數據由一係列XLogRecData結構體鏈表組成,之所以要用XLogRecData鏈,是因為在所要處理的日誌記錄實體數據在內存空間可能不是連續存儲的,而且數據可能分布在多個緩衝區內,需要用XlogRecData鏈表將它們組織起來。XlogRecData數據結構如下:
typedef struct XLogRecData
{
char *data; /*資源管理器包含數據的開始*/
uint32 len; /*資源管理器包含的數據大小*/
Buffer buffer; /*如果有buffer指明第幾個緩衝區*/
bool buffer_std; /*是否含有標準的pd_lower/pd_upper結構*/
struct XLogRecData *next; /*指向下一個結構體*/
} XLogRecData;
其中,buffer_std該值為true,則容許XLOG釋放備份頁的空閑空間,空閑空間由pd_lower和pd_upper限定:
- pd_lower表示頁麵起始位置與未分配空間開頭的字節偏移
- pd_upper表示頁麵末尾位置與未分配空間末尾的字節偏移
XLogRecData中data保存每條XLOG日誌記錄中的數據信息,以INSERT、UPDATE、DELETE為例,XLogRecData中data的大體內容如下(該圖引自《Internals Of PostgreSQL Wal》):
可以看出,根據XLogRecData的信息,我們很容易恢複出對應的數據。
備份數據塊
備份數據塊包含一個頭部信息BkpBlock和一塊大小的備份數據,其中BkpBlock結構如下:
typedef struct BkpBlock
{
RelFileNode node; /* 用於唯一標示該塊所屬的關係表,包括表空間OID,數據庫OID,關係表OID等*/
ForkNumber fork; /*一個關係表在存儲上可能由多個分支組成,每個分支以文件單獨存儲,RelFileNode對應關係表的分支號*/
BlockNumber block; /*對應塊的塊號*/
uint16 hole_offset; /*空洞偏移量*/
uint16 hole_length; /* 空洞長度*/
} BkpBlock;
如果需要備份的塊存在空洞,則備份的時候隻記錄這個空洞的偏移量和長度,但沒有實際備份它,從而提高備份效率。
備份數據塊頭部後緊跟一個塊大小的備份數據,該塊可以在數據庫崩潰恢複時直接恢複。
Redo恢複的具體步驟
每次postmaster進程啟動時,都會調用StartupXLOG函數對數據庫崩潰進行恢複,由於該過程非常繁瑣,為了更好的理解,本文把Redo恢複分為三個階段:
- 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恢複之前的準備工作,確定恢複起始的位置,而本階段主要是基於上個階段,進行真正的恢複操作:
-
初始化恢複環境,啟動各種需要恢複的資源,即調用對應資源管理器的啟動函數:
RmgrTable[rmid].rm_startup();
-
設置需要Redo的日誌記錄的起始位置(離上個階段checkPoint最近的一條日誌記錄),把起始位置處的日誌記錄讀入record,進入循環,不斷地進行redo操作
a. 如果record不空,從record開始循環執行redo操作,處理完一條需要redo的記錄,即調用對應資源管理器的redo操作:
RmgrTable[record->xl_rmid].rm_redo(EndRecPtr, record);
b. 如果record為空,不需要進行redo操作
-
讀取下一條記錄到record中,不斷進行redo操作,直到執行到了我們所要求的時間線的位置,或者已經把所有的日誌記錄中需要redo的record執行完畢
Redo恢複後
這個階段主要是對Redo恢複的環境進行清理,並啟動需要的輔助進程。
- 本次恢複結束之際,確定是否需要再設置一個新的時間線。如果是恢複到某個指定的時間點上而不是全部恢複,則生成一個新的時間線
- 更新XlogCtl控製結構體中的recoveryLastRecPtr
-
當參數InRecovery的值為true時,執行初始環境的清理工作,調用:
RmgrTable[rmid].rm_cleanup();
- 執行CreateCheckPoint,強迫恢複的內容刷到磁盤
- 調用PreaaalocXlogFiles為新日誌記錄重新分配日誌段文件,同時釋放日誌恢複時申請的內存。調用ShutdownRecoveryTransactionEnvironment關閉恢複環境。再次更新控製結構體ControlFile、Xlogctl等。
- 開始啟動clog、prepared transactions需要的資源或環境等內容,為恢複結束後、係統正常運行做準備工作
- startupXlOG結束,係統正常啟動
總結
至此,我們分析了PostgreSQL數據庫在崩潰時恢複的具體過程,其中具體的Redo恢複過程,實際上是通過資源管理器獲取對應的redo函數接口來執行恢複操作,每種資源管理器其處理過程不盡相同,這裏我們不再一一介紹,後麵的月報我們會去分析各種資源的redo函數具體操作。
最後更新:2017-05-21 09:01:43