因為最近在處理wal歸檔的問題,加上強同步的復(fù)制模式的疑問,以及遇到了一些walreceiver異常退出的問題排查,所以想整體對于復(fù)制關(guān)系做一些通用性梳理。
??wal日志中記錄了什么?
WAL日志中記錄了自數(shù)據(jù)庫創(chuàng)建以來對數(shù)據(jù)的所有修改歷史(因為是修改歷史,所以查詢不算),因此通過這些日志可以恢復(fù)出一個相同的數(shù)據(jù)庫鏡像。為了提高數(shù)據(jù)庫的可用性,可以選擇建立一個或幾個備機,實時接收主機的WAL日志,并對這些日志進行回放,這樣就可以形成基于日志復(fù)制的高可用集群。
??pg的復(fù)制模式有哪幾類?
從日志復(fù)制的時機上來看,PostgreSQL的日志復(fù)制可以分為異步復(fù)制和同步復(fù)制,主要區(qū)別是事務(wù)提交時是否需要等待備機的日志持久性消息。從日志的形態(tài)來看,又可以分為邏輯復(fù)制和物理復(fù)制,它們發(fā)送的分別是邏輯日志和物理日志。
??物理復(fù)制的過程是什么,用到了哪些進程?
用到的進程有,postmaster主進程、walreceiver進程、walsender進程、startup進程。
物理復(fù)制主要就是將主機產(chǎn)生的WAL日志同步發(fā)送給備機。主機和備機分別創(chuàng)建WalSender進程和WalReceiver進程,主機上的WalSender進程負責(zé)將Backends產(chǎn)生的WAL日志發(fā)送給備機,備機則通過WalReceiver進程接收WAL日志信息,并且將其刷入磁盤文件,備機還會通過Startup進程對接收的WAL日志進行回放。
在Host?Standby模式下,備機還可以接收用戶的只讀請求,如下圖右邊的小人。非hot?standby模式讀取備機,可能會有數(shù)據(jù)不一致(同步模式下主機發(fā)了wal,備機也收了但還沒回放/異步模式下備機還沒收完wal)。
主備進程示意圖:

主機最多可以支持max_wal_sender個WalSender進程同時向備機發(fā)送日志,每個備機在啟動后,都會向主機發(fā)送一個連接請求,這時主機就會啟動一個WalSender進程和備機中的WalReceiver進程對應(yīng)。?那也就是,主機可以連max_wal_sender個備機,默認是10,想改的話要重啟集群。
主機和備機之間通過libpq進行通信,它們之間的通信遵守一套自定義的“復(fù)制”協(xié)議,備機通過libpqwalreceiver.c中的函數(shù)拼接協(xié)議字符串,主機則通過exec_replication_command對備機發(fā)送過來的協(xié)議字符串進行解析。例如在備機啟動之后,會和主機進行系統(tǒng)的一致性校驗,備機會發(fā)送“IDENTIFY_SYSTEM”給主機,主機會將自己的sysid、時間線信息返回給備機,備機會檢查自己的sysid與主機的sysid是否一致。
當(dāng)WalReceiver發(fā)送START?REPLICATION命令后,主機的WalSender進程就開始發(fā)送物理日志給WalReceiver
WalReceiver和WalSender詳細函數(shù)圖:

??更細粒度的劃分,同步復(fù)制/異步復(fù)制?
物理復(fù)制還可以分為同步復(fù)制和異步復(fù)制。
同步復(fù)制是指在主機的事務(wù)提交時,必須保證該事務(wù)產(chǎn)生的事務(wù)日志(WAL)已經(jīng)在備機中刷入或回放,否則主機的事務(wù)不能提交;而異步復(fù)制則不必關(guān)心事務(wù)日志在備機中是否已經(jīng)寫入。異步復(fù)制只能保證主備數(shù)據(jù)的最終一致性而非實時一致性。

同步復(fù)制需要在主機上設(shè)置synchronous_standby_names參數(shù),每個備機都可以在創(chuàng)建復(fù)制槽(Replication?Slot)時指定一個名字,synchronous_standby_names參數(shù)就通過這個名字來確定哪個備機需要同步復(fù)制。
synchronous_commit參數(shù)說明見以下表格。有標(biāo)識根據(jù)不同落盤方式的事物提交方式。

local通常上就是異步了,只保證刷到主機磁盤。on的話是以主備機都落盤為維度的。remote_apply這個是最嚴(yán)格的,需要備機保證wal日志已回放完成,主事物才能提交。所以在執(zhí)行語句后一直沒有返回,可以看下主機同步類型,以及備機日志回放狀態(tài)。

根據(jù)synchronous_commit參數(shù),可以對應(yīng)同步復(fù)制的三種模式:WRITE、FLUSH、APPLY,在日志復(fù)制的過程中,每個備機都會有自己的復(fù)制模式,主機則根據(jù)不同的備機模式來決定事務(wù)提交時是否等待備機。上圖這種場景下,還是apply模式能保證數(shù)據(jù)的強一致性。
WalSender收到備機發(fā)送過來的WRITE、FLUSH、APPLY三個LSN,根據(jù)備機返回的LSN的情況,主機的WalSender進程會將集群的LSN不停地向前推進,每次推進LSN都會嘗試喚醒等待隊列中的PGPROC進程。一旦待提交事務(wù)的LSN被滿足,事務(wù)就可以提交了。
??hot?standby?
這個場景用于常見的一寫多讀集群,這樣就能將讀操作分散到不同的物理機上,提高系統(tǒng)的吞吐量。
只讀操作不會產(chǎn)生物理事務(wù)ID,而元組的可見性需要借助元組上的事務(wù)ID信息和當(dāng)前的活躍事務(wù)ID來共同完成判斷。也就是說,備機要支持只讀操作,除了做到數(shù)據(jù)一致,還需要能夠同步主機上的事務(wù)信息,這些事務(wù)信息包括活躍事務(wù)信息、鎖信息及失效消息信息。
主機會將這些信息記錄成WAL日志,備機在Startup進程中回放日志時可以基于這些WAL日志構(gòu)建備機上的事務(wù)信息。主機通過做snapshot收集活躍事務(wù)信息寫入wal文件,備機只有收到活躍事務(wù)信息后才能聲稱達到一致性(主機停機狀態(tài)shutdown下除外)。
這里從進程的角度解釋了同步復(fù)制為什么會卡住:
用戶在備機只能進行只讀操作,讀操作使用的鎖的級別只有AccessShareLock(1號鎖)為DDL語句加的鎖通常是AccessExclusiveLock(8號鎖),而備機中的只讀操作請求的鎖是AccessShareLock(1號鎖),根據(jù)鎖的沖突關(guān)系可以知道,8號鎖和1號鎖是沖突的(8號鎖是唯一和1號鎖沖突的鎖),因此備機必須要關(guān)注那些需要申請8號鎖的操作。為了能夠讓備機同步獲得8號鎖,可以在主機申請8號鎖時,記錄8號鎖的WAL日志。
回放wal的時候,那么主機是用戶進程(backends)持有8號鎖,備機是startup進程持有8號鎖。
假如備機上有一個讀事務(wù)獲得了1號鎖,Startup進程需要獲得8號鎖,則Startup進程需要等待讀事務(wù)結(jié)束才能繼續(xù)回放WAL日志。如果備機上的讀事務(wù)是短事務(wù),則Startup進程的等待還是可以接受的;但如果備機上的讀事務(wù)是長事務(wù),則Startup進程被阻塞會帶來WAL日志回放落后的問題。另外,在Startup進程等待鎖的過程中會觸發(fā)死鎖檢測。當(dāng)檢測到死鎖時,我們不希望死鎖檢測機制把Startup進程終止掉,而是應(yīng)該終止讀事務(wù)所在的進程。因此,PostgreSQL提供了參數(shù)max_standby_streaming_delay來處理等待超時的情況(與參數(shù)max_standby_archive_delay類似)。也就是說,如果Startup進程等待的時間超過了max_standby_streaming_delay的限制,則考慮終止讀事務(wù)。同時,在備機申請鎖時,它不再進行死鎖檢測,它會嘗試將那些沖突的讀事務(wù)終止。
??邏輯復(fù)制vs物理復(fù)制?
物理復(fù)制和邏輯復(fù)制都考sloat復(fù)制槽和walsender,walreceiver進程來復(fù)制。
在源碼中,XLogSendPhysical是用來做物理復(fù)制的,主要是讀取wal日志并將其發(fā)送到備機。物理復(fù)制通常用于創(chuàng)建一個與主數(shù)據(jù)庫完全相同的備數(shù)據(jù)庫。該函數(shù)在實現(xiàn)的時候不需要解析wal記錄的內(nèi)容,只需要按順序發(fā)送即可。如過程中發(fā)生錯誤,那么需要從上次成功的地方重新開始。優(yōu)點是簡單、快速,缺點是不能選擇性地復(fù)制數(shù)據(jù)。
在源碼中,XLogSendLogical是用來做邏輯復(fù)制的,它可以選擇性地復(fù)制數(shù)據(jù),例如只復(fù)制某些表或某些列,這個函數(shù)不僅需要讀取wal日志,還需要解析wal記錄的內(nèi)容。優(yōu)點是靈活,缺點是需要更多cpu資源,并且可能需要處理數(shù)據(jù)轉(zhuǎn)換的問題。