淺談 Postgres 同步複製原理
關於 PolarDB PostgreSQL 版
PolarDB PostgreSQL 版是一款阿里雲自主研發的雲原生關係型數據庫產品,100% 兼容 PostgreSQL,高度兼容 Oracle 語法;採用基於 Shared-Storage 的存儲計算分離架構,具有極致彈性、毫秒級延遲、HTAP 、Ganos 全空間數據處理能力和高可靠、高可用、彈性擴展等企業級數據庫特性。同時,PolarDB PostgreSQL 版具有大規模並行計算能力,可以應對 OLTP 與 OLAP 混合負載。
背景
postgres 支持主備場景下,通過流複製進行同步,本文將以 pg 14 爲例介紹同步複製的模式、相關內核代碼的原理。
同步複製的模式
postgres 共有兩個參數用來控制同步複製,分別是 synchronous_standby_names 和 synchronous_commit。
- synchronous_commit 控制同步策略級別,共有五種模式,分別是:
off:異步複製,提交後立刻返回 COMMIT 響應。
local:異步複製,主庫寫入 WAL 刷盤後,返回 COMMIT 響應。
remote_write:同步複製,主庫提交,並等到 WAL 傳輸到備庫後,返回 COMMIT 響應。
on(remote_flush):默認的同步模式,在備庫寫入 WAL 之後,返回 COMMIT 響應。
remote_apply:最高級別的同步模式,要等待備庫回放 WAL 完成後,主庫纔會提交 commit。
- synchronous_standby_names 控制哪些 standby 被應用同步策略。當沒有指定 standby 的時候,同步複製會自動退化到 local,即只保證主庫寫入 WAL。
相關代碼分析
同步複製的代碼主要實現在 syncrep.c 中,基本都是在主節點上執行的。核心的流複製傳輸仍在 walreceiver/walsender 模塊中進行。這種設計的核心思想是將所有關於等待 / 釋放的邏輯隔離在主節點上。主節點定義了它希望等待的備節點。備節點完全不知道主節點上事務的同步要求,從而降低了代碼的複雜性。
首先介紹插入數據後,生成的 WAL 寫入磁盤的過程,主要由 xact.c 中的 RecordTransactionCommit() 函數完成,保證已提交的數據不會丟失。
/* 需要同步提交的情況 */
if ((wrote_xlog && markXidCommitted &&
synchronous_commit > SYNCHRONOUS_COMMIT_OFF) ||
forceSyncCommit || nrels > 0)
{
/* 首先WAL落盤 */
XLogFlush(XactLastRecEnd);
if (markXidCommitted)
TransactionIdCommitTree(xid, nchildren, children);
}
else//異步提交情況,不會調用fsync刷盤,會由wal writer等進程完成wal刷盤。但在數據庫崩潰時可能丟失數據。
{
/*
* 設置異步提交LSN
*/
XLogSetAsyncXactLSN(XactLastRecEnd);
if (markXidCommitted)
TransactionIdAsyncCommitTree(xid, nchildren, children, XactLastRecEnd);
}
if (markXidCommitted)
{
MyPgXact->delayChkpt = false;
END_CRIT_SECTION();
}
latestXid = TransactionIdLatest(xid, nchildren, children);
/*
* 調用SyncRepWaitForLSN,等待同步複製完成
*/
if (wrote_xlog && markXidCommitted)
SyncRepWaitForLSN(XactLastRecEnd, true);
接下來介紹下主庫等待、喚醒這一套的實現機制,核心函數爲 SyncRepWaitForLSN()。
SyncRepWaitForLSN(XLogRecPtr lsn, bool commit)
{
//...
/* 如果無需同步等待,直接返回,例如沒有配置synchronous_standby_names。*/
if (!SyncRepRequested() ||
!((volatile WalSndCtlData *) WalSndCtl)->sync_standbys_defined)
return;
//...
/*
* 如果沒有定義同步流複製節點,或者判斷到commit lsn小於已同步的LSN,說明WAL已經flush了,直接返回。
*/
if (!WalSndCtl->sync_standbys_defined ||
lsn <= WalSndCtl->lsn[mode])
{
LWLockRelease(SyncRepLock);
return;
}
/*
* 該本進程插入 SyncRepQueue 隊列中,然後開始等待
*/
MyProc->waitLSN = lsn;
MyProc->syncRepState = SYNC_REP_WAITING;
SyncRepQueueInsert(mode);
Assert(SyncRepQueueIsOrderedByLSN(mode));
LWLockRelease(SyncRepLock);
// ...
/*
* 進入循環等待狀態,說明本地的WAL已經flush了,只是等待同步流複製節點返回狀態
*/
for (;;)
{
// ...
/*
* SYNC_REP_WAIT_COMPLETE狀態表示同步完成,退出
*/
if (MyProc->syncRepState == SYNC_REP_WAIT_COMPLETE)
break;
// ...
/*
* 用戶主動cancel query,退出
*/
if (QueryCancelPending)
{
QueryCancelPending = false;
ereport(WARNING,
(errmsg("canceling wait for synchronous replication due to user request"),
errdetail("The transaction has already committed locally, but might not have been replicated to the standby.")));
SyncRepCancelWait();
break;
}
/*
* 等待備節點 LATCH 的釋放信號
*/
rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_POSTMASTER_DEATH, -1,
WAIT_EVENT_SYNC_REP);
/*
* 如果postmaster掛了的話,直接退出
*/
if (rc & WL_POSTMASTER_DEATH)
{
ProcDiePending = true;
whereToSendOutput = DestNone;
SyncRepCancelWait();
break;
}
}
// ...
}
還包括以下相關函數:
-
SyncRepQueueInsert:主庫 SyncRepWaitForLSN 函數調用,作用是把該進程插入 SyncRepQueue 隊列中,然後開始等待;
-
SyncRepCancelWait:停止等待,並將該進程從隊列中移除;
-
SyncRepWakeQueue:喚醒隊列中所有等待的進程,並將所有進程移除隊列;
同步複製流程
完整的同步流程如下:
-
主庫插入數據,生成 WAL;
-
調用 SyncRepWaitForLSN 等待,並將本進程插入 SyncRepQueue 隊列中;
-
備庫 walreceiver 進程將 WAL 刷入磁盤,並且通知主庫的 walsender 進程;
-
主庫 walsender 進程收到備庫的消息,根據同步策略的級別,使用 SyncRepWakeQueue 喚醒所有等待隊列中的進程,並將其移出隊列;
-
主庫執行 SQL 的進程繼續執行,通知其他進程本事務已提交。
同步複製的問題
同步複製能夠保證異常場景下數據不丟失,但是也有一些缺點。
-
性能明顯下降。在主庫與備庫網絡條件差的情況下,性能下降會更加明顯。
-
影響主庫可用性。當主庫與備庫在 synchronous_commit 爲 on 的同步模式下,備庫掛了會導致主庫也 hang 住。這在大部分使用場景下,是用戶很難接受的。
小結
異步複製的性能較同步複製有明顯的提升,但是犧牲了數據安全性,在主庫崩潰的時候存在丟失數據的可能。同步複製又可能導致備庫異常情況下,主庫狀態受影響。因此建議選用合適的同步策略級別,或是日常配置爲同步模式,利用外部 HA 組件,在備庫異常情況下,配置參數轉爲異步複製。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/LJIwTZZNoYYx3MK478e33A