pg_rewind
的功能是在主備切換後回退舊主庫上多餘的事務變更,以便可以作為新主的備機和新主建立複制關系。通過
pg_rewind
可以在故障切換後快速恢複舊主,避免整庫重建。對于大庫,整庫重建會很耗時間。
如何識别舊主上多餘的變更?
這就用到了PostgreSQL獨有的時間線技術,資料庫執行個體的初始時間線是1。以後每次主備切換時,需要提升備庫為新主。提升操作會将新主的時間線加1,并且會記錄提升時間線的WAL位置(LSN)。這個LSN位點我們稱其為新主舊主在時間線上的分叉點。
我們隻要掃描舊主上在分叉點之後的WAL記錄,就能找到舊主上所有多餘的變更。
如何回退舊主上多餘的變更?
可能有人會想到可以通過解析分叉點以後的WAL,生成undo SQL,再逆序執行undo SQL實作事務回退。這也是mysql上常用的實作方式。 PG同樣也有walminer插件可以支援WAL到undo SQL的解析。但是,這種方式存在很多限制,資料一緻性也難以保證。
pg_rewind
使用的是不同的方式,過程概述如下
- 解析舊主上分叉點後的WAL,記錄這些事務修改了哪些資料塊
- 對資料檔案以外的檔案,直接從新主上拉取後覆寫舊主上的檔案
- 對于資料檔案,隻從新主拉取被舊主修改了的資料塊,并覆寫舊主資料檔案中對應的資料塊
- 從新主上拉取最新的WAL,覆寫舊主的WAL
- 把舊主改成恢複模式,恢複的起點則設定為分叉點前的最近一次checkpoint
- 啟動舊主,舊主進入當機恢複過程,舊主應用完從新主拷貝來的所有WAL後,資料就和新主一緻了。
如何保證主備一緻?
分叉點之後,新主和舊主上可能有各種各樣的變更。除了正常的資料的增删改,還有truncate,表結構變更,表的DROP和CREATE,資料庫參數配置變更等等。如何保證
pg_rewind
之後這些都能一緻呢?
下面我們重點看一下
pg_rewind
對資料檔案的拷貝處理。
- 通過比較target和source節點的資料目錄,建構filemap
filemap中對每個檔案記錄了不同的處理方式(即action),對于資料檔案,如下
- 僅存在于新主:FILE_ACTION_COPY
- 僅存在于舊主:FILE_ACTION_REMOVE
- 新主檔案size>舊主:FILE_ACTION_COPY_TAIL
- 新主檔案size<舊主:FILE_ACTION_TRUNCATE
- 新主檔案size=舊主:FILE_ACTION_NONE
- 讀取舊主本地WAL,擷取并記錄影響的資料塊到filemap中對應的
中的pagemapfile_entry
pagemap屬于塊級别的拷貝。為了避免檔案級别的拷貝做重複的事情,提取影響的塊号是做了一些過濾,具體如下:
- FILE_ACTION_NONE:隻記錄小于等于新主size的塊
- FILE_ACTION_TRUNCATE:隻記錄小于等于新主size的塊
- FILE_ACTION_COPY_TAIL:隻記錄小于等于舊主size的塊
- 其他:不記錄
- 周遊filemap,對其中每個
,從新主拷貝必要的資料file_entry
- 從新主拷貝pagemap中記錄的塊覆寫舊主
- 根據action,執行不同的檔案拷貝操作
- FILE_ACTION_NONE:無需處理
- FILE_ACTION_COPY:從新主拷貝資料,隻拷貝到生成action時看到的新主上的size
- FILE_ACTION_TRUNCATE:舊主truncate到新主的size
- FILE_ACTION_COPY_TAIL:從新主拷貝尾部資料,即新主size超出舊主的部分
- FILE_ACTION_CREATE:建立目錄
- FILE_ACTION_REMOVE:删除檔案(或目錄)
上面的過程彙總後如下:
- 僅存在于新主(FILE_ACTION_COPY)
- 從新主拷貝資料,隻拷貝到生成action時看到的新主上的size
- 僅存在于舊主(FILE_ACTION_REMOVE)
- 删除檔案
- 新主檔案 size > 舊主(FILE_ACTION_COPY_TAIL)
- 對偏移位置小于等于舊主檔案 size 的塊,從新主拷貝受舊主分叉後更新影響的塊
- 偏移位置為舊主檔案 size ~ 新主檔案 size 之間的塊,從新主拷貝
- 新主檔案 size < 舊主(FILE_ACTION_TRUNCATE)
- 對偏移位置小于等于新主檔案 size 的塊,從新主拷貝受舊主分叉後更新影響的塊
- 對偏移位置大于新主檔案 size 的塊,truncate 掉
- 新主檔案 size = 舊主(FILE_ACTION_NONE)
針對上面的流程,現在回答幾個關鍵的問題
pg_rewind
拷貝資料時,新主還處于活動中,拷貝的這些資料塊不在同一個事務一緻點上,如何将不一緻的資料狀态變成一緻的?
這裡用到的技術,就是資料塊最擅長的當機恢複的技術。通過啟動舊主後,回放WAL使資料庫達到一緻的狀态。
我們以一個微觀的資料塊為例進行說明。
具體到某一個資料塊,隻有三種情況,我們分别讨論
- 需要從新主拷貝
- 如果拷貝時發現新主上這個塊所在的檔案被删掉了,那麼也會删掉舊主上的檔案。
- 如果拷貝時新主上這個塊被 truncate 掉了,會忽略這個塊的拷貝。
- 如果拷貝時這個塊正在被修改,可能導緻
讀到了一個不一緻的塊。一半是修改前的,另一半是修改後的。pg_rewind
運作的前提條件時資料庫必須開啟pg_rewind
,開啟full_page_write
後WAL中會記錄每個checkpoint後第一次修改的page的完整鏡像,當機恢複時,使用這個鏡像,就可以修複資料檔案中損壞的資料塊。full_page_write
- 保留舊主
- 如果後來新主上這個塊變更了,回放WAL時自然可以追到一緻的狀态
- 需要從舊主删除
- 如果後來新主上有新增了這個資料塊,同樣,回放WAL時自然可以追到一緻的狀态
如果一個表先被删了,之後又建立一個表結構不一樣的同名的表,
pg_rewind
處理這個表時會不會有問題?
PostgreSQL中資料檔案名是filenode,初始時它等于表的oid,當表被重寫(比如執行truncate或vacuum full)後,會指派為下一個oid。是以先後建立的同名表,它們對應的檔案名是不一樣的(MySQL采用表名作為資料檔案名。雖然比較直覺,但會有很多潛在的問題,比如特殊字元,PostgreSQL的filenode的方式要嚴謹得多)。
PostgreSQL回放WAL時如何保證可正常執行?
具體要回答幾個問題
- 當機恢複階段,回放建表的WAL時,如果對應的檔案已存在,結果如何?
- 當機恢複階段,回放删表的WAL時,如果對應的檔案不存在,結果如何?
- 當機恢複階段,回放extend資料檔案的WAL時,如果對應的塊已存在,結果如何?
- 當機恢複階段,回放write資料檔案的WAL時,如果對應的塊或者檔案不存在,結果如何?
- 當機恢複階段,回放truncate資料檔案的WAL時,如果對應的塊或者檔案不存在,結果如何?
存儲層的一些接口,已經考慮到REDO的使用場景,做了一些容錯,支援幂等性。
- REDO中建立檔案時,容忍檔案已存在
void mdcreate(SMgrRelation reln, ForkNumber forkNum, bool isRedo) { ... fd = PathNameOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY); if (fd < 0) { int save_errno = errno; if (isRedo) fd = PathNameOpenFile(path, O_RDWR | PG_BINARY); ...
- 删除檔案時,容忍檔案不存在
static void mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo) { ... ret = unlink(pcfile_path); if (ret < 0 && errno != ENOENT) ereport(WARNING, (errcode_for_file_access(), errmsg("could not remove file \"%s\": %m", pcfile_path)));
- REDO中打開檔案時,都會帶上
flag,檔案不存在時會建立一個空的O_CREAT
static MdfdVec * _mdfd_getseg(SMgrRelation reln, ForkNumber forknum, BlockNumber blkno, bool skipFsync, int behavior) { if ((behavior & EXTENSION_CREATE) || (InRecovery && (behavior & EXTENSION_CREATE_RECOVERY))) { ... flags = O_CREAT;
- 讀或寫資料塊時,可以 seek 到超出檔案大小的位置
void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer, bool skipFsync) { ... v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); ... nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_WRITE);
- REDO中 truncate 時,如果檔案大小已小于 truncate 目标,無視
void mdtruncate(SMgrRelation reln, ForkNumber forknum, BlockNumber nblocks) { ... curnblk = mdnblocks(reln, forknum); if (nblocks > curnblk) { /* Bogus request ... but no complaint if InRecovery */ if (InRecovery) return;
- 回放truncate記錄時,先強制執行一次建立關系的操作
void smgr_redo(XLogReaderState *record) { ... else if (info == XLOG_SMGR_TRUNCATE) { ... /* * Forcibly create relation if it doesn't exist (which suggests that * it was dropped somewhere later in the WAL sequence). As in * XLogReadBufferForRedo, we prefer to recreate the rel and replay the * log as best we can until the drop is seen. */ smgrcreate(reln, MAIN_FORKNUM, true); ... smgrtruncate(reln, forks, nforks, blocks);