概述
主從復制,是指將一臺Redis服務(wù)器的數(shù)據(jù),復制到其他的Redis服務(wù)器。前者稱為主節(jié)點(master),后者稱為從節(jié)點(slave);數(shù)據(jù)的復制是單向的,只能由主節(jié)點到從節(jié)點。默認情況下,每臺Redis服務(wù)器都是主節(jié)點;且一個主節(jié)點可以有多個從節(jié)點(或沒有從節(jié)點),但一個從節(jié)點只能有一個主節(jié)點。
主從復制的作用主要包括:
數(shù)據(jù)冗余
主從復制實現(xiàn)了數(shù)據(jù)的熱備份,是持久化之外的一種數(shù)據(jù)冗余方式。故障恢復
當主節(jié)點出現(xiàn)問題時,可以由從節(jié)點提供服務(wù),實現(xiàn)快速的故障恢復;實際上是一種服務(wù)的冗余。負載均衡:在主從復制的基礎(chǔ)上,配合讀寫分離,可以由主節(jié)點提供寫服務(wù),由從節(jié)點提供讀服務(wù)(即寫Redis數(shù)據(jù)時應(yīng)用連接主節(jié)點,讀Redis數(shù)據(jù)時應(yīng)用連接從節(jié)點),分擔服務(wù)器負載;尤其是在寫少讀多的場景下,通過多個從節(jié)點分擔讀負載,可以大大提高Redis服務(wù)器的并發(fā)量。
高可用基石:除了上述作用以外,主從復制還是哨兵和集群能夠?qū)嵤┑幕A(chǔ),因此說主從復制是Redis高可用的基礎(chǔ)。
主從復制的流程
建立連接階段

數(shù)據(jù)同步階段
主從節(jié)點之間的連接建立以后,便可以開始進行數(shù)據(jù)同步,該階段可以理解為從節(jié)點數(shù)據(jù)的初始化。具體執(zhí)行的方式是:從節(jié)點向主節(jié)點發(fā)送psync命令(Redis2.8以前是sync命令),開始同步。
數(shù)據(jù)同步階段是主從復制最核心的階段,根據(jù)主從節(jié)點當前狀態(tài)的不同,可以分為全量復制和部分復制。
- 全量復制:用于初次復制或其他無法進行部分復制的情況,將主節(jié)點中的所有數(shù)據(jù)都發(fā)送給從節(jié)點,是一個非常重型的操作。
- 部分復制:用于網(wǎng)絡(luò)中斷等情況后的復制,只將中斷期間主節(jié)點執(zhí)行的寫命令發(fā)送給從節(jié)點,與全量復制相比更加高效。需要注意的是,如果網(wǎng)絡(luò)中斷時間過長,導致主節(jié)點沒有能夠完整地保存中斷期間執(zhí)行的寫命令,則無法進行部分復制,仍使用全量復制。
需要注意的是,在數(shù)據(jù)同步階段之前,從節(jié)點是主節(jié)點的客戶端,主節(jié)點不是從節(jié)點的客戶端;而到了這一階段及以后,主從節(jié)點互為客戶端。原因在于:在此之前,主節(jié)點只需要響應(yīng)從節(jié)點的請求即可,不需要主動發(fā)請求,而在數(shù)據(jù)同步階段和后面的命令傳播階段,主節(jié)點需要主動向從節(jié)點發(fā)送請求(如推送緩沖區(qū)中的寫命令),才能完成復制。
全量 or 部分
由于全量復制在主節(jié)點數(shù)據(jù)量較大時效率太低,因此Redis2.8開始提供部分復制,用于處理網(wǎng)絡(luò)中斷時的數(shù)據(jù)同步。部分復制的實現(xiàn),依賴于三個重要的概念:
復制偏移量
主節(jié)點和從節(jié)點分別維護一個復制偏移量(offset),代表的是主節(jié)點向從節(jié)點傳遞的字節(jié)數(shù);主節(jié)點每次向從節(jié)點傳播N個字節(jié)數(shù)據(jù)時,主節(jié)點的offset增加N;從節(jié)點每次收到主節(jié)點傳來的N個字節(jié)數(shù)據(jù)時,從節(jié)點的offset增加N。復制積壓緩沖區(qū)
復制積壓緩沖區(qū)是由主節(jié)點維護的、固定長度的、先進先出(FIFO)隊列,默認大小1MB;當主節(jié)點開始有從節(jié)點時創(chuàng)建,其作用是備份主節(jié)點最近發(fā)送給從節(jié)點的數(shù)據(jù)。注意,無論主節(jié)點有一個還是多個從節(jié)點,都只需要一個復制積壓緩沖區(qū)。服務(wù)器運行id
每個Redis節(jié)點(無論主從),在啟動時都會自動生成一個隨機ID(每次啟動都不一樣),由40個隨機的十六進制字符組成;runid用來唯一識別一個Redis節(jié)點。
判斷流程

全量復制

1、主節(jié)點接收到全量同步的請求時,fork一個子進程進行bgsave,同時將接下來的寫操作保存至復制緩沖區(qū);
2、RDB保存完畢后,向從服務(wù)器發(fā)送;
3、從服務(wù)器清除當前的內(nèi)存數(shù)據(jù)
4、阻塞客戶端請求,拒絕服務(wù)
5、從RDB加載數(shù)據(jù),執(zhí)行這些寫命令,將數(shù)據(jù)庫狀態(tài)更新至主節(jié)點的最新狀態(tài)
6、若開啟了AOF,則會觸發(fā)bgrewriteaof的執(zhí)行,從而保證AOF文件更新至主節(jié)點的最新狀態(tài)
部分復制
在部分復制階段,從服務(wù)器只要拉取并執(zhí)行部分主服務(wù)的寫命令即可。
命令傳播階段
當從服務(wù)器完成了對主服務(wù)器的同步操作,就進入了命令傳播階段。在這個階段,主服務(wù)器會將自己的寫命令同步給從服務(wù)器,從而保持數(shù)據(jù)一致性
除了發(fā)送寫命令,主從節(jié)點還維持著心跳機制:PING和REPLCONF ACK。心跳機制對于主從復制的超時判斷、數(shù)據(jù)安全等有作用。
主 -> 從:PING
每隔指定的時間,主節(jié)點會向從節(jié)點發(fā)送PING命令,這個PING命令的作用,主要是為了讓從節(jié)點進行超時判斷。
PING發(fā)送的頻率由repl-ping-slave-period參數(shù)控制,單位是秒,默認值是10s。
從 -> 主:REPLCONF ACK
在命令傳播階段,從節(jié)點會向主節(jié)點發(fā)送REPLCONF ACK命令,頻率是每秒1次。
命令格式為:REPLCONF ACK {offset},其中offset指從節(jié)點保存的復制偏移量。
REPLCONF ACK命令的作用包括:
- 實時監(jiān)測主從節(jié)點網(wǎng)絡(luò)狀態(tài)
該命令會被主節(jié)點用于復制超時的判斷。此外,在主節(jié)點中使用info Replication,可以看到其從節(jié)點的狀態(tài)中的lag值,代表的是主節(jié)點上次收到該REPLCONF ACK命令的時間間隔,在正常情況下,該值應(yīng)該是0或1。 - 檢測命令丟失
從節(jié)點發(fā)送了自身的offset,主節(jié)點會與自己的offset對比,如果從節(jié)點數(shù)據(jù)缺失(如網(wǎng)絡(luò)丟包),主節(jié)點會推送缺失的數(shù)據(jù)(這里也會利用復制積壓緩沖區(qū))。注意,offset和復制積壓緩沖區(qū),不僅可以用于部分復制,也可以用于處理命令丟失等情形;區(qū)別在于前者是在斷線重連后進行的,而后者是在主從節(jié)點沒有斷線的情況下進行的。 - 輔助保證從節(jié)點的數(shù)量和延遲
Redis主節(jié)點中使用min-slaves-to-write和min-slaves-max-lag參數(shù),來保證主節(jié)點在不安全的情況下不會執(zhí)行寫命令;所謂不安全,是指從節(jié)點數(shù)量太少,或延遲過高。例如min-slaves-to-write和min-slaves-max-lag分別是3和10,含義是如果從節(jié)點數(shù)量小于3個,或所有從節(jié)點的延遲值都大于10s,則主節(jié)點拒絕執(zhí)行寫命令。而這里從節(jié)點延遲值的獲取,就是通過主節(jié)點接收到REPLCONF ACK命令的時間來判斷的。
問題
數(shù)據(jù)一致性問題
數(shù)據(jù)延遲
由于主從復制的命令傳播是異步的,延遲與數(shù)據(jù)的不一致不可避免。如果應(yīng)用對數(shù)據(jù)不一致的接受程度程度較低,可能的優(yōu)化措施包括:(1) 優(yōu)化主從節(jié)點之間的網(wǎng)絡(luò)環(huán)境(如在同機房部署);(2) 監(jiān)控主從節(jié)點延遲(通過offset)判斷,如果從節(jié)點延遲過大,通知應(yīng)用不再通過該從節(jié)點讀取數(shù)據(jù);(3) 使用集群同時擴展寫負載和讀負載等。
在命令傳播階段以外的其他情況下,從節(jié)點的數(shù)據(jù)不一致可能更加嚴重,例如連接在數(shù)據(jù)同步階段,或從節(jié)點失去與主節(jié)點的連接時等。從節(jié)點的slave-serve-stale-data參數(shù)便與此有關(guān):它控制這種情況下從節(jié)點的表現(xiàn);如果為yes(默認值),則從節(jié)點仍能夠響應(yīng)客戶端的命令,如果為no,則從節(jié)點只能響應(yīng)info、slaveof等少數(shù)命令。該參數(shù)的設(shè)置與應(yīng)用對數(shù)據(jù)一致性的要求有關(guān);如果對數(shù)據(jù)一致性要求很高,則應(yīng)設(shè)置為no。數(shù)據(jù)過期
在單機版Redis中,存在兩種刪除策略。
惰性刪除:服務(wù)器不會主動刪除數(shù)據(jù),只有當客戶端查詢某個數(shù)據(jù)時,服務(wù)器判斷該數(shù)據(jù)是否過期,如果過期則刪除。
定期刪除:服務(wù)器執(zhí)行定時任務(wù)刪除過期數(shù)據(jù),但是考慮到內(nèi)存和CPU的折中(刪除會釋放內(nèi)存,但是頻繁的刪除操作對CPU不友好),該刪除的頻率和執(zhí)行時間都受到了限制。
在主從復制場景下,為了主從節(jié)點的數(shù)據(jù)一致性,從節(jié)點不會主動刪除數(shù)據(jù),而是由主節(jié)點控制從節(jié)點中過期數(shù)據(jù)的刪除。由于主節(jié)點的惰性刪除和定期刪除策略,都不能保證主節(jié)點及時對過期數(shù)據(jù)執(zhí)行刪除操作,因此,當客戶端通過Redis從節(jié)點讀取數(shù)據(jù)時,很容易讀取到已經(jīng)過期的數(shù)據(jù)。
Redis 3.2中,從節(jié)點在讀取數(shù)據(jù)時,增加了對數(shù)據(jù)是否過期的判斷:如果該數(shù)據(jù)已過期,則不返回給客戶端;將Redis升級到3.2可以解決數(shù)據(jù)過期問題。故障切換
在沒有使用哨兵的讀寫分離場景下,應(yīng)用針對讀和寫分別連接不同的Redis節(jié)點;當主節(jié)點或從節(jié)點出現(xiàn)問題而發(fā)生更改時,需要及時修改應(yīng)用程序讀寫Redis數(shù)據(jù)的連接;連接的切換可以手動進行,或者自己寫監(jiān)控程序進行切換,但前者響應(yīng)慢、容易出錯,后者實現(xiàn)復雜,成本都不算低。
連接超時
意義
- 如果主節(jié)點判斷連接超時,其會釋放相應(yīng)從節(jié)點的連接,從而釋放各種資源,否則無效的從節(jié)點仍會占用主節(jié)點的各種資源(輸出緩沖區(qū)、帶寬、連接等);此外連接超時的判斷可以讓主節(jié)點更準確的知道當前有效從節(jié)點的個數(shù),有助于保證數(shù)據(jù)安全(配合前面講到的min-slaves-to-write等參數(shù))。
- 如果從節(jié)點判斷連接超時,則可以及時重新建立連接,避免與主節(jié)點數(shù)據(jù)長期的不一致。
判斷機制
主從復制超時判斷的核心,在于repl-timeout參數(shù),該參數(shù)規(guī)定了超時時間的閾值(默認60s),對于主節(jié)點和從節(jié)點同時有效;主從節(jié)點觸發(fā)超時的條件分別如下:
- 主節(jié)點:每秒1次調(diào)用復制定時函數(shù)replicationCron(),在其中判斷當前時間距離上次收到各個從節(jié)點REPLCONF ACK的時間,是否超過了repl-timeout值,如果超過了則釋放相應(yīng)從節(jié)點的連接。
- 從節(jié)點:從節(jié)點對超時的判斷同樣是在復制定時函數(shù)中判斷,基本邏輯是:
(1) 如果當前處于連接建立階段,且距離上次收到主節(jié)點的信息的時間已超過repl-timeout,則釋放與主節(jié)點的連接;
(2) 如果當前處于數(shù)據(jù)同步階段,且收到主節(jié)點的RDB文件的時間超時,則停止數(shù)據(jù)同步,釋放連接;
(3) 如果當前處于命令傳播階段,且距離上次收到主節(jié)點的PING命令或數(shù)據(jù)的時間已超過repl-timeout值,則釋放與主節(jié)點的連接。
問題
連接超時會使主從進入重連階段,若超時時間較短,而進入部分同步階段,此時從服務(wù)器阻塞;若超時時間較長,則進入全量同步階段,主服務(wù)器需要消耗大量的內(nèi)存和cpu時間用于bgsave,同時rdb的傳輸會占據(jù)主服務(wù)器的大部分帶寬,直接影響了主服務(wù)器的吞吐量。在加載RDB階段,從服務(wù)器會阻塞客戶端請求,拒絕服務(wù)。所以我們應(yīng)該盡量避免超時問題。
實際問題:
- 數(shù)據(jù)同步階段
在主從節(jié)點進行全量復制bgsave時,主節(jié)點需要首先fork子進程將當前數(shù)據(jù)保存到RDB文件中,然后再將RDB文件通過網(wǎng)絡(luò)傳輸?shù)綇墓?jié)點。如果RDB文件過大,主節(jié)點在fork子進程+保存RDB文件時耗時過多,可能會導致從節(jié)點長時間收不到數(shù)據(jù)而觸發(fā)超時;此時從節(jié)點會重連主節(jié)點,然后再次全量復制,再次超時,再次重連……這是個悲傷的循環(huán)。為了避免這種情況的發(fā)生,除了注意Redis單機數(shù)據(jù)量不要過大,另一方面就是適當增大repl-timeout值,具體的大小可以根據(jù)bgsave耗時來調(diào)整。 - 命令傳播階段:在該階段主節(jié)點會向從節(jié)點發(fā)送PING命令,頻率由repl-ping-slave-period控制;該參數(shù)應(yīng)明顯小于repl-timeout值(后者至少是前者的幾倍)。否則,如果兩個參數(shù)相等或接近,網(wǎng)絡(luò)抖動導致個別PING命令丟失,此時恰巧主節(jié)點也沒有向從節(jié)點發(fā)送數(shù)據(jù),則從節(jié)點很容易判斷超時。
- 慢查詢導致的阻塞:如果主節(jié)點或從節(jié)點執(zhí)行了一些慢查詢(如
keys *或者對大數(shù)據(jù)的hgetall等),導致服務(wù)器阻塞;阻塞期間無法響應(yīng)復制連接中對方節(jié)點的請求,可能導致復制超時。