浅谈 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:唤醒队列中所有等待的进程,并将所有进程移除队列;
同步复制流程
完整的同步流程如下:
1. 主库插入数据,生成WAL;
2. 调用 SyncRepWaitForLSN 等待,并将本进程插入 SyncRepQueue 队列中;
3. 备库 walreceiver 进程将 WAL 刷入磁盘,并且通知主库的 walsender 进程;
4. 主库 walsender 进程收到备库的消息,根据同步策略的级别,使用 SyncRepWakeQueue 唤醒所有等待队列中的进程,并将其移出队列;
5. 主库执行 SQL 的进程继续执行,通知其他进程本事务已提交。
同步复制的问题
同步复制能够保证异常场景下数据不丢失,但是也有一些缺点。
性能明显下降。在主库与备库网络条件差的情况下,性能下降会更加明显。
影响主库可用性。当主库与备库在synchronous_commit为on的同步模式下,备库挂了会导致主库也hang住。这在大部分使用场景下,是用户很难接受的。
小结
异步复制的性能较同步复制有明显的提升,但是牺牲了数据安全性,在主库崩溃的时候存在丢失数据的可能。同步复制又可能导致备库异常情况下,主库状态受影响。因此建议选用合适的同步策略级别,或是日常配置为同步模式,利用外部HA组件,在备库异常情况下,配置参数转为异步复制。