postgresql流复制原理以及流复制和逻辑复制的区别说明
流复制的原理:
物理复制也叫流复制,流复制的原理是主库把WAL发送给备库,备库接收WAL后,进行重放。
逻辑复制的原理:
逻辑复制也是基于WAL文件,在逻辑复制中把主库称为源端库,备库称为目标端数据库,源端数据库根据预先指定好的逻辑解析规则对WAL文件进行解析,把DML操作解析成一定的逻辑变化信息(标准SQL语句),源端数据库把标准SQL语句发给目标端数据库,目标端数据库接收到之后进行应用,从而实现数据同步。
流复制和逻辑复制的区别:
流复制主库上的事务提交不需要等待备库接收到WAL文件后的确认,逻辑复制相反。
流复制要求主备库的大版本一致,逻辑复制可以跨大版本的数据同步,也可以实现异构数据库的数据同步。
流复制的主库可读写,从库只允许读,逻辑复制的目标端数据库要求可读写
流复制是对实例级别的复制(整个postgresql数据库),逻辑复制是选择性的复制一些表,所以是对表级别的复制。
流复制有主库的DDL、DML操作,逻辑复制只有DML操作。
补充:PostgreSQL同步流复制原理和代码浅析
背景
数据库ACID中的持久化如何实现
数据库ACID里面的D,持久化。指的是对于用户来说提交的事务,数据是可靠的,即使数据库crash了,在硬件完好的情况下,也能恢复回来。
PostgreSQL是怎么做到的呢,看一幅图,画得比较丑,凑合看吧。
假设一个事务,对数据库做了一些操作,并且产生了一些脏数据,首先这些脏数据会在数据库的sharedbuffer中。
同时,产生这些脏数据的同时也会产生对应的redo信息,产生的REDO会有对应的LSN号(你可以理解为REDO的虚拟地址空间的一个唯一的OFFSET,每一笔REDO都有),这个LSN号也会记录到sharedbuffer中对应的脏页中。
walwriter是负责将walbufferflush到持久化设备的进程,同时它会更新一个全局变量,记录已经flush的最大的LSN号。
bgwriter是负责将sharedbuffer的脏页持久化到持久化设备的进程,它在flush时,除了要遵循LRU算法之外,还要通过LSN全局变量的比对,来保证脏页对应的REDO记录已经flush到持久化设备了,如果发现还对应的REDO没有持久化,会触发WALwriter去flushwalbuffer。(即确保日志比脏数据先落盘)
当用户提交事务时,也会产生一笔提交事务的REDO,这笔REDO也携带了LSN号。backendprocess同样需要等待对应LSNflush到磁盘后才会返回给用户提交成功的信号。(保证日志先落盘,然后返回给用户)
数据库同步复制原理浅析
同步流复制,即保证standby节点和本地节点的日志双双落盘。
PostgreSQL使用另一组全局变量,记录同步流复制节点已经接收到的XLOGLSN,以及已经持久化的XLOGLSN。
用户在发起提交请求后,backendprocess除了要判断本地wal有没有持久化,同时还需要判断同步流复制节点的XLOG有没有接收到或持久化(通过synchronous_commit参数控制)。
如果同步流复制节点的XLOG还没有接收或持久化,backendprocess会进入等待状态。
数据库同步复制代码浅析
对应的代码和解释如下:
CommitTransaction@src/backend/access/transam/xact.c RecordTransactionCommit@src/backend/access/transam/xact.c
/* *Ifwedidn'tcreateXLOGentries,we'redonehere;otherwisewe *shouldtriggerflushingthoseentriesthesameasacommitrecord *would.ThiswillprimarilyhappenforHOTpruningandthelike;we *wantthesetobeflushedtodiskinduetime. */ if(!wrote_xlog)//没有产生redo的事务,直接返回 gotocleanup; if(wrote_xlog&&markXidCommitted)//如果产生了redo,等待同步流复制 SyncRepWaitForLSN(XactLastRecEnd);
SyncRepWaitForLSN@src/backend/replication/syncrep.c
/* *Waitforsynchronousreplication,ifrequestedbyuser. * *InitiallybackendsstartinstateSYNC_REP_NOT_WAITINGandthen *changethatstatetoSYNC_REP_WAITINGbeforeaddingourselves *tothewaitqueue.DuringSyncRepWakeQueue()aWALSenderchanges *thestatetoSYNC_REP_WAIT_COMPLETEoncereplicationisconfirmed. *ThisbackendthenresetsitsstatetoSYNC_REP_NOT_WAITING. */ void SyncRepWaitForLSN(XLogRecPtrXactCommitLSN) { ... /* *Fastexitifuserhasnotrequestedsyncreplication,orthereareno *syncreplicationstandbynamesdefined.Notethatthosestandbysdon't *needtobeconnected. */ if(!SyncRepRequested()||!SyncStandbysDefined())//如果不是同步事务或者没有定义同步流复制节点,直接返回 return; ... /* *Wedon'twaitforsyncrepifWalSndCtl->sync_standbys_definedisnot *set.SeeSyncRepUpdateSyncStandbysDefined. * *Alsocheckthatthestandbyhasn'talreadyreplied.Unlikelyrace *conditionbutwe'llbefetchingthatcachelineanywaysoit'slikely *tobealowcostcheck. */ if(!WalSndCtl->sync_standbys_defined|| XactCommitLSN<=WalSndCtl->lsn[mode])//如果没有定义同步流复制节点,或者判断到commitlsn小于已同步的LSN,说明XLOG已经flush了,直接返回。 { LWLockRelease(SyncRepLock); return; } ... //进入循环等待状态,说明本地的xlog已经flush了,只是等待同步流复制节点的REDO同步状态。 /* *WaitforspecifiedLSNtobeconfirmed. * *Eachprochasitsownwaitlatch,soweperformanormallatch *check/waitloophere. */ for(;;)//进入等待状态,检查latch是否满足释放等待的条件(walsender会根据REDO的同步情况,实时更新对应的latch) { intsyncRepState; /*Mustresetthelatchbeforetestingstate.*/ ResetLatch(&MyProc->procLatch); syncRepState=MyProc->syncRepState; if(syncRepState==SYNC_REP_WAITING) { LWLockAcquire(SyncRepLock,LW_SHARED); syncRepState=MyProc->syncRepState; LWLockRelease(SyncRepLock); } if(syncRepState==SYNC_REP_WAIT_COMPLETE)//说明XLOG同步完成,退出等待 break; //如果本地进程挂了,输出的消息内容是,本地事务信息已持久化,但是远程也许还没有持久化 if(ProcDiePending) { ereport(WARNING, (errcode(ERRCODE_ADMIN_SHUTDOWN), errmsg("cancelingthewaitforsynchronousreplicationandterminatingconnectionduetoadministratorcommand"), errdetail("Thetransactionhasalreadycommittedlocally,butmightnothavebeenreplicatedtothestandby."))); whereToSendOutput=DestNone; SyncRepCancelWait(); break; } //如果用户主动cancelquery,输出的消息内容是,本地事务信息已持久化,但是远程也许还没有持久化 if(QueryCancelPending) { QueryCancelPending=false; ereport(WARNING, (errmsg("cancelingwaitforsynchronousreplicationduetouserrequest"), errdetail("Thetransactionhasalreadycommittedlocally,butmightnothavebeenreplicatedtothestandby."))); SyncRepCancelWait(); break; } //如果postgres主进程挂了,进入退出流程。 if(!PostmasterIsAlive()) { ProcDiePending=true; whereToSendOutput=DestNone; SyncRepCancelWait(); break; } //等待walsender来修改对应的latch /* *Waitonlatch.Anyconditionthatshouldwakeusupwillsetthe *latch,sononeedfortimeout. */ WaitLatch(&MyProc->procLatch,WL_LATCH_SET|WL_POSTMASTER_DEATH,-1);
注意用户进入等待状态后,只有主动cancel,或者kill(terminate),或者主进程die才能退出无限的等待状态。后面会讲到如何将同步级别降级为异步。
前面提到了,用户端需要等待LATCH的释放信号。
那么谁来给它这个信号了,是walsender进程,源码和解释如下:
src/backend/replication/walsender.c
StartReplication WalSndLoop ProcessRepliesIfAny ProcessStandbyMessage ProcessStandbyReplyMessage if(!am_cascading_walsender)//非级联流复制节点,那么它将调用SyncRepReleaseWaiters修改backendprocess等待队列中它们对应的latch。 SyncRepReleaseWaiters(); SyncRepReleaseWaiters@src/backend/replication/syncrep.c /* *UpdatetheLSNsoneachqueuebaseduponourlateststate.This *implementsasimplepolicyoffirst-valid-standby-releases-waiter. * *Otherpoliciesarepossible,whichwouldchangewhatwedohereandwhat *perhapsalsowhichinformationwestoreaswell. */ void SyncRepReleaseWaiters(void) { ... //释放满足条件的等待队列 /* *Setthelsnfirstsothatwhenwewakebackendstheywillreleaseupto *thislocation. */ if(walsndctl->lsn[SYNC_REP_WAIT_WRITE]write) { walsndctl->lsn[SYNC_REP_WAIT_WRITE]=MyWalSnd->write; numwrite=SyncRepWakeQueue(false,SYNC_REP_WAIT_WRITE); } if(walsndctl->lsn[SYNC_REP_WAIT_FLUSH] flush) { walsndctl->lsn[SYNC_REP_WAIT_FLUSH]=MyWalSnd->flush; numflush=SyncRepWakeQueue(false,SYNC_REP_WAIT_FLUSH); } ...
SyncRepWakeQueue@src/backend/replication/syncrep.c
/* *Walkthespecifiedqueuefromhead.Setthestateofanybackendsthat *needtobewoken,removethemfromthequeue,andthenwakethem. *Passall=truetowakewholequeue;otherwise,justwakeupto *thewalsender'sLSN. * *MustholdSyncRepLock. */ staticint SyncRepWakeQueue(boolall,intmode) { ... while(proc)//修改对应的backendprocess的latch { /* *AssumethequeueisorderedbyLSN */ if(!all&&walsndctl->lsn[mode]waitLSN) returnnumprocs; /* *Movetonextproc,sowecandeletethisprocfromthequeue. *thisprocisvalid,procmaybeNULLafterthis. */ thisproc=proc; proc=(PGPROC*)SHMQueueNext(&(WalSndCtl->SyncRepQueue[mode]), &(proc->syncRepLinks), offsetof(PGPROC,syncRepLinks)); /* *Setstatetocomplete;seeSyncRepWaitForLSN()fordiscussionof *thevariousstates. */ thisproc->syncRepState=SYNC_REP_WAIT_COMPLETE;//满足条件时,改成SYNC_REP_WAIT_COMPLETE ....
如何设置事务可靠性级别
PostgreSQL支持在会话中设置事务的可靠性级别。
off表示commit时不需要等待wal持久化。
local表示commit是只需要等待本地数据库的wal持久化。
remote_write表示commit需要等待本地数据库的wal持久化,同时需要等待syncstandby节点walwritebuffer完成(不需要持久化)。
on表示commit需要等待本地数据库的wal持久化,同时需要等待syncstandby节点wal持久化。
提醒一点,synchronous_commit的任何一种设置,都不影响wal日志持久化必须先于sharedbuffer脏数据持久化。所以不管你怎么设置,都不好影响数据的一致性。
synchronous_commit=off#synchronizationlevel; #off,local,remote_write,oron
如何实现同步复制降级
从前面的代码解析可以得知,如果backendprocess进入了等待循环,只接受几种信号降级。并且降级后会告警,表示本地wal已持久化,但是syncstandby节点不确定wal有没有持久化。
如果你只配置了1个standby,并且将它配置为同步流复制节点。一旦出现网络抖动,或者syncstandby节点故障,将导致同步事务进入等待状态。
怎么降级呢?
方法1.
修改配置文件并重置
$vipostgresql.conf synchronous_commit=local $pg_ctlreload
然后cancel所有query.
postgres=#selectpg_cancel_backend(pid)frompg_stat_activitywherepid<>pg_backend_pid();
收到这样的信号,表示事务成功提交,同时表示WAL不知道有没有同步到syncstandby。
WARNING:cancelingwaitforsynchronousreplicationduetouserrequest DETAIL:Thetransactionhasalreadycommittedlocally,butmightnothavebeenreplicatedtothestandby. COMMIT postgres=#showsynchronous_commit; synchronous_commit -------------------- off (1row)
同时它会读到全局变量synchronous_commit已经是local了。
这样就完成了降级的动作。
方法2.
方法1的降级需要对已有的正在等待walsync的pid使用cancel进行处理,有点不人性化。
可以通过修改代码的方式,做到更人性化。
SyncRepWaitForLSNfor循环中,加一个判断,如果发现全局变量synccommit变成local,off了,则告警并退出。这样就不需要人为的去cancelquery了.
WARNING:cancelingwaitforsynchronousreplicationduetouserrequest
DETAIL:Thetransactionhasalreadycommittedlocally,butmightnothavebeenreplicatedtothestandby.
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。