淺談 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。

off:異步複製,提交後立刻返回 COMMIT 響應。

local:異步複製,主庫寫入 WAL 刷盤後,返回 COMMIT 響應。

remote_write:同步複製,主庫提交,並等到 WAL 傳輸到備庫後,返回 COMMIT 響應。

on(remote_flush):默認的同步模式,在備庫寫入 WAL 之後,返回 COMMIT 響應。

remote_apply:最高級別的同步模式,要等待備庫回放 WAL 完成後,主庫纔會提交 commit。

相關代碼分析

同步複製的代碼主要實現在 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;
  }
 }
    // ...
}

還包括以下相關函數:

同步複製流程

完整的同步流程如下:

  1. 主庫插入數據,生成 WAL;

  2. 調用 SyncRepWaitForLSN 等待,並將本進程插入 SyncRepQueue 隊列中;

  3. 備庫 walreceiver 進程將 WAL 刷入磁盤,並且通知主庫的 walsender 進程;

  4. 主庫 walsender 進程收到備庫的消息,根據同步策略的級別,使用 SyncRepWakeQueue 喚醒所有等待隊列中的進程,並將其移出隊列;

  5. 主庫執行 SQL 的進程繼續執行,通知其他進程本事務已提交。

同步複製的問題

同步複製能夠保證異常場景下數據不丟失,但是也有一些缺點。

  1. 性能明顯下降。在主庫與備庫網絡條件差的情況下,性能下降會更加明顯。

  2. 影響主庫可用性。當主庫與備庫在 synchronous_commit 爲 on 的同步模式下,備庫掛了會導致主庫也 hang 住。這在大部分使用場景下,是用戶很難接受的。

小結

異步複製的性能較同步複製有明顯的提升,但是犧牲了數據安全性,在主庫崩潰的時候存在丟失數據的可能。同步複製又可能導致備庫異常情況下,主庫狀態受影響。因此建議選用合適的同步策略級別,或是日常配置爲同步模式,利用外部 HA 組件,在備庫異常情況下,配置參數轉爲異步複製。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LJIwTZZNoYYx3MK478e33A