PostgreSQL複製槽相關機制在各版本調整

T1YSL發表於2022-10-16

提到PostgreSQL流複製和邏輯複製這兩種複製方式,不得不想到的一個東西,就是複製槽(Replication Slot)。
複製槽在PostgreSQL 9.4版本中被引入,引入之初是為了防止備庫需要的WAL日誌在主庫被刪除,確保主庫在所有的備庫收到wal之前不會移除它們,主庫會會根據備庫返回的資訊確認哪些WAL已不再需要,才能進行清理。Replication Slot能夠確保在主備斷連後主庫的WAL仍不被清理,因為Replication Slot的狀態資訊是持久化儲存的,即便從庫斷掉或主庫重啟,這些資訊仍然不會丟掉或失效。
複製槽分為物理複製槽(Physical Replication Slot)和邏輯複製槽(Logic Replication Slot)。物理複製槽一般結合流複製一起使用,能夠很好的保證備庫需要的日誌不會在主庫刪除。
而邏輯複製槽是PostgreSQL自身提供的WAL(WAL解析)功能,將資料的資料操作按照事務,依次放到邏輯複製槽中,(複製槽中可以用一些解析外掛WAL解析為各種形式)然後能過walsender發出。他最主要的作用是來記錄和做事務切換,這樣才能保證不會丟事務或者重發事務。
複製槽實質上是記憶體中的一些資料結構,加上持久化儲存到pg_replslot/目錄中的二進位制狀態檔案。在PostgreSQL啟動的時候,預先在共享記憶體中分配好這些資料結構所用記憶體(即一個大小為max_replication_slots的陣列)。需要注意的一點是,我們在進行basebackup的時候是會把pg_replslot這個目錄排除掉的,所以就算主庫上存在邏輯複製槽,在用basebackup搭建流複製的時候,也是不會把原本的主庫上的邏輯複製槽複製到備節點。如下為PostgreSQL資料庫原始碼裡的一部分,位置在src/backend/replication/basebackup.c,這裡麵包含了basebackup命令執行過程會忽略的目錄,

static const char *const excludeDirContents[] =
{
	PG_STAT_TMP_DIR,
	"pg_replslot",
	PG_DYNSHMEM_DIR,
	"pg_notify",
	"pg_serial",
	"pg_snapshots",
	"pg_subtrans",
	NULL
};

這些目錄的內容在伺服器啟動時被刪除或重新建立,因此它們不包括在備份中。 這些目錄本身被保留並作為空目錄包括在內,以保持訪問許可權。這一點需要我們尤為注意,不要錯誤的認為pg_basebackup會備份資料目錄下的所有東西。
如果真的想要在其他節點去轉移複製槽的話,其實可以選擇手動複製這個pg_replslot目錄,並且複製完之後,需要資料庫重啟才能在資料庫裡檢視到這個複製槽的資訊,因為複製槽是在資料庫的啟動階段,把pg_replslot目錄下的複製槽資訊載入到資料庫裡的。如果在高可用環境,主庫發生了當機,那麼此時主庫作為釋出端的邏輯複製就可能存在問題,而現階段的高可用方案,大多都做不到複製槽的自動故障轉移,其中Patroni是個個例,它支援邏輯複製槽的故障轉移。
編輯Patroni的配置檔案,在“slots:”部分可以定義永久複製槽。複製槽將在切換/故障轉移期間保留。應用更改後,將在主節點上建立邏輯複製槽,同樣的複製槽也將在備用資料庫上建立。Patroni在內部將複製槽資訊從主節點複製到所有符合條件的備用節點。 Patroni 叢集的所有備用節點中的 Replication Slot 資訊也隨著邏輯複製從主端進行而提前,當 LSN 編號在主節點上的相應插槽上增加時,自動增加備用節點插槽上的 LSN 號。這樣備庫的LSN推進也不會因為LSN的原因造成日誌的累積。在切換或故障轉移的情況下,不會丟失任何插槽資訊,因為它們已經在備用節點上維護。因此在嚴格意義上講,並不能算複製槽的故障轉移,而是在所有的節點上都維護一個相同的複製槽。
Patroni的這個方案需要PostgreSQL 11 以上版本,因為它使用從 PostgreSQL 11 開始可用的pg_replication_slot_advance()函式來推進插槽。Patroni使用pg_read_binary_file() 函式來讀取槽的二進位制資訊,永久插槽資訊將被新增到 DCS中,並由Patroni 的主例項持續維護。此外必須在需要維護邏輯複製槽的所有備用節點上啟用hot_standby_feedback ,必須啟用Patroni引數postgresql.use_slots以確保每個備用節點都使用主節點上的插槽。

之前提到了,複製槽可以保證備庫需要的日誌不會在主庫刪除。資料庫為複製槽所保留的最早的的LSN就是邏輯複製槽記錄的restart_lsn。在PostgreSQL原始碼的src/backend/replication/slot.c裡,透過ReplicationSlotsComputeRequiredLSN()函式我們能清晰得看到,它計算了所有槽位的最老的restart_lsn,並通知xlog模組,把最老的restart_lsn的值賦給了min_required變數,也就是把這個LSN作為資料庫保留的最小得LSN,即作為資料庫還需要的最小的LSN。

void
ReplicationSlotsComputeRequiredLSN(void)
{
... ...
		if (restart_lsn != InvalidXLogRecPtr &&
			(min_required == InvalidXLogRecPtr ||
			 restart_lsn < min_required))
			min_required = restart_lsn;
	}
	LWLockRelease(ReplicationSlotControlLock);
	XLogSetReplicationSlotMinimumLSN(min_required);
}

我們在上邊搞清楚了資料庫會把所有複製槽裡restart_lsn最小的作為最老的LSN,這個LSN往後的所有較新的日誌都會保留下來。但是同時也面臨了新的問題:如果複製槽失效了怎麼辦?如果複製槽上的restart_lsn不推進了怎麼辦?沒錯,如果資料庫是一直正常執行著的,且有一定的業務,那麼,這兩個現象都可能會引起WAL日誌累計的問題,複製槽失效,那他的restart_lsn必然不會正常推進。從這個最小的restart_lsn往後的日誌都會保留下來,如果一段時間內業務量很大,而這個複製槽沒有有效處理的話,WAL數量可能會急劇增長,甚至超過WAL的保留數量,畢竟wal_keep_segments是一個軟限制,就像你設定它為1600,但有時候你會看到高於這個數字的WAL日誌數量。
因此我們應及時關注失效和不使用的邏輯複製槽。好在PostgreSQL13提供了max_slot_wal_keep_size,控制最大為複製槽保留多少WAL日誌。此外,在PostgreSQL13、14、15每個版本都針對這個失效複製槽的問題,進行了一些調整。
在PostgreSQL資料庫裡,每次發生檢查點的時候,都會觸發舊日誌(WAL)的清理動作。
我們去檢視原始碼的src/backend/access/transam/xlog.c檔案下,CreateCheckPoint()這個函式的定義,找到這一清理日誌的程式碼部分,其中,PostgreSQL12版本的如下:

	XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
	KeepLogSeg(recptr, &_logSegNo);
	_logSegNo--;
	RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);

這個功能呼叫的函式主要是這四行,第一個函式用來做segment段號的計算,第二個函式計算要保留的日誌段號,第三部分是一個簡單的日誌段號遞減1的過程,第四行是做老的日誌移除的函式。
這裡我們不去詳細看它的實現原理,僅僅看各個版本的這部分函式呼叫,就可以大致瞭解在最近幾個版本,PostgreSQL資料庫對這個問題做了什麼最佳化。
比如,PostgreSQL13版本的對應部分如下:

    XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
    KeepLogSeg(recptr, &_logSegNo);
    InvalidateObsoleteReplicationSlots(_logSegNo);
    _logSegNo--;
    RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);

PostgreSQL13版本在這一部分比PostgreSQL12版本多了一行函式呼叫,從字面意思也可以猜出個所以然來,這個函式的作用是,將任何指向比給定區段更早的LSN的槽標記為無效;這些無效的槽所需要的WAL日誌將被移除。看來,PostgreSQL的開發者們已經意識到這種失效的複製槽帶來的負面影響了,因此,在PostgreSQL13版本起,就引入了失效的複製槽的處理機制。
我們再來看PostgreSQL14版本的相關部分,PostgreSQL14版本在13版本的基礎上,又加了一個判斷,如果有失效的複製槽,則會重新計算要保留的最老的LSN。如果沒有失效的複製槽,則省去重複的處理部分。

    XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
    KeepLogSeg(recptr, &_logSegNo);
    if (InvalidateObsoleteReplicationSlots(_logSegNo))
    {
         XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
        KeepLogSeg(recptr, &_logSegNo);
    }
    _logSegNo--;
    RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);

再看一看最新的PostgreSQL15版本,PostgreSQL15版本的函式呼叫其實和14版本沒有什麼不同,只不過對於移除舊的WAL日誌的函式RemoveOldXlogFiles(),在原本的基礎上,傳入變數多了checkPoint.ThisTimeLineID,這個值是XLOG插入的當前時間軸。任何回收的部分應該在這個時間線重複使用。ThisTimeLineID其實並不重要,在比較中會忽略它。在決定是否仍然需要一個段時,忽略XLOG段識別符號的時間軸部分。這確保了我們不會過早地從父時間軸中刪除一個段。因為可能會更主動地刪除非父時間軸的片段,那樣會更加棘手。

    XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
    KeepLogSeg(recptr, &_logSegNo);
    if (InvalidateObsoleteReplicationSlots(_logSegNo))
    {
        XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
        KeepLogSeg(recptr, &_logSegNo);
    }
    _logSegNo--;
    RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr,
                       checkPoint.ThisTimeLineID);

從上邊的分析我們可以看到,失效的複製槽有WAL日誌堆積的風險,針對該問題,PostgreSQL的開發者們在PostgreSQL13、14、15版本逐步做了一些最佳化。此外,邏輯複製的效能和功能也在這幾個版本做了一定程度的提升,如果您對PostgreSQL資料庫的使用中,涉及到了複製槽或者邏輯複製的場景,我建議使用PostgreSQL 13+的版本,這樣可能會大大減少您在使用過程中的相關問題。除此之外,相關監控也是必不可少的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69990629/viewspace-2918604/,如需轉載,請註明出處,否則將追究法律責任。

相關文章