主從復制概述
主從復制,是指將一臺Redis服務器的數據,復制到其他的Redis服務器。前者稱為主節點(master),后者稱為從節點(slave);
數據的復制是單向的,只能由主節點到從節點。
默認情況下,每臺Redis服務器都是主節點,且一個主節點可以有多個從節點(或沒有從節點),但一個從節點只能有一個主節點。
目前很多中小企業都沒有使用到 Redis 的集群,但是至少都做了主從。有了主從,當 master 掛掉的時候,運維讓從庫過來接管,服務就可以繼續,否則 master 需要經過數據恢復和重啟的過程,這就可能會拖很長的時間,影響線上業務的持續服務。
Redis主從復制服務器架構圖如下:
主從復制的作用
主從復制的作用主要包括:
- 數據冗余:主從復制實現了數據的熱備份,是持久化之外的一種數據冗余方式。
- 故障恢復:當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復;實際上是一種服務的冗余。
- 負載均衡:在主從復制的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫Redis數據時應用連接主節點,讀Redis數據時應用連接從節點),分擔服務器負載;尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高Redis服務器的并發量。
- 高可用基石:主從復制還是哨兵和集群能夠實施的基礎,因此說主從復制是Redis高可用的基礎。
CAP 原理
在了解 Redis 的主從復制之前,讓我們先來理解一下現代分布式系統的理論基石——CAP 原理。
CAP 原理就好比分布式領域的牛頓定律,它是分布式存儲的理論基石。自打 CAP 的論文發表之后,分布式存儲中間件猶如雨后春筍般一個一個涌現出來。理解這個原理其實很簡單,本節我們首先對這個原理進行一些簡單的講解。
- C - Consistent ,一致性
- A - Availability ,可用性
- P - Partition tolerance ,分區容忍性
分布式系統的節點往往都是分布在不同的機器上進行網絡隔離開的,這意味著必然會有網絡斷開的風險,這個網絡斷開的場景的專業詞匯叫著「網絡分區」。
在網絡分區發生時,兩個分布式節點之間無法進行通信,我們對一個節點進行的修改操作將無法同步到另外一個節點,所以數據的「一致性」將無法滿足,因為兩個分布式節點的數據不再保持一致。除非我們犧牲「可用性」,也就是暫停分布式節點服務,在網絡分區發生時,不再提供修改數據的功能,直到網絡狀況完全恢復正常再繼續對外提供服務。
一句話概括 CAP 原理就是——網絡分區發生時,一致性和可用性兩難全
。
Redis 主從同步最終一致性
Redis 的主從數據是異步
同步的,所以分布式的 Redis 系統并不滿足「一致性」要求。當客戶端在 Redis 的主節點修改了數據后,立即返回,即使在主從網絡斷開的情況下,主節點依舊可以正常對外提供修改服務,所以 Redis 滿足「可用性」。
Redis 保證「最終一致性」,從節點會努力追趕主節點,最終從節點的狀態會和主節點的狀態將保持一致。如果網絡斷開了,主從節點的數據將會出現大量不一致,一旦網絡恢復,從節點會采用多種策略努力追趕上落后的數據,繼續盡力保持和主節點一致。
主從復制的實現原理
總的來說主從復制功能的詳細步驟可以分為7個步驟:
- 設置主節點的地址和端口
- 建立套接字連接
- 發送PING命令
- 權限驗證
- 同步
- 命令傳播
接下來分別敘述每個步驟,整個流程圖如下:
為了測試,我在本地機開啟兩個Redis節點,分別監聽:
127.0.0.1 6379(主)
127.0.0.1 6380(從)
1. 設置主服務器的地址和端口
第一步首先是在從服務器設置需要同步的主服務器信息,包括機器IP, 端口。
主從復制的開啟,完全是在從節點發起的;不需要我們在主節點做任何事情。
從節點開啟主從復制,有3種方式:
(1)配置文件
在從服務器的配置文件中加入:slaveof masterip masterport
(2)啟動命令
redis-server啟動命令后加入 --slaveof masterip masterport
(3)客戶端命令
Redis服務器啟動后,直接通過客戶端執行命令:slaveof masterip masterport,則該Redis實例成為從節點。
上述3種方式是等效的,下面以客戶端命令的方式為例,看一下當執行了slaveof后,Redis主節點和從節點的變化。
完成上面的配置后, 從服務器會將主服務器的ip地址和端口號保存到服務器狀態的屬性里面。可以Redis使用info Replication
命令分別查看從服務器和主服務器的主從信息
2. 建立套接字連接
在slaveof命令執行之后,從服務器會根據設置的ip和端口,向主服務器簡歷socket連接。
在6380從服務器里面執行完slave of 127.0.0.1 6379
后意味著,從服務器向主服務器發起socket連接
在執行info Replication 命令,可以看到6380服務器的角色是slave了
而6379 服務器已經成為主服務器角色:
3. 發送PING命令
從節點成為主節點的客戶端之后,發送ping命令進行首次請求,目的是:檢查socket連接是否可用,以及主節點當前是否能夠處理請求。
從節點發送ping命令后,可能出現3種情況:
(1)返回pong:說明socket連接正常,且主節點當前可以處理請求,復制過程繼續。
(2)超時:一定時間后從節點仍未收到主節點的回復,說明socket連接不可用,則從節點斷開socket連接,并重連。
(3)返回pong以外的結果:如果主節點返回其他結果,如正在處理超時運行的腳本,說明主節點當前無法處理命令,則從節點斷開socket連接,并重連。
主從發送PING命令流程圖如下:
4. 身份驗證
如果從節點中設置了masterauth選項,則從節點需要向主節點進行身份驗證;沒有設置該選項,則不需要驗證。從節點進行身份驗證是通過向主節點發送auth命令進行的,auth命令的參數即為配置文件中的masterauth的值。
如果主節點設置密碼的狀態,與從節點masterauth的狀態一致(一致是指都存在,且密碼相同,或者都不存在),則身份驗證通過,復制過程繼續;如果不一致,則從節點斷開socket連接,并重連。
主從身份驗證流程圖如下:
5. 同步
同步就是將從節點的數據庫狀態更新成主節點當前的數據庫狀態。具體執行的方式是:從節點向主節點發送psync命令(Redis2.8以前是sync命令),開始同步。
數據同步階段是主從復制最核心的階段,根據主從節點當前狀態的不同,可以分為全量復制
和部分復制
下面會有詳細介紹全量復制和部分復制內容,這里暫不詳述
6. 命令傳播
經過上面同步操作,此時主從的數據庫狀態其實已經一致了,但這種一致的狀態的并不是一成不變的。
在完成同步之后,也許主服務器馬上就接受到了新的寫命令,執行完該命令后,主從的數據庫狀態又不一致。
數據同步階段完成后,主從節點進入命令傳播階段;在這個階段主節點將自己執行的寫命令發送給從節點,從節點接收命令并執行,從而保證主從節點數據的一致性。
另外命令轉播我們需要關注兩個點: 延遲與不一致
和 心跳機制
我們下面介紹一下
延遲與不一致
需要注意的是,命令傳播是異步的過程,即主節點發送寫命令后并不會等待從節點的回復;因此實際上主從節點之間很難保持實時的一致性,延遲在所難免。數據不一致的程度,與主從節點之間的網絡狀況、主節點寫命令的執行頻率、以及主節點中的repl-disable-tcp-nodelay
配置等有關。
repl-disable-tcp-nodelay
配置如下:
- 假如設置成yes,則redis會合并小的TCP包從而節省帶寬,但會增加同步延遲(40ms),造成master與slave數據不一致
- 假如設置成no,則redis master會立即發送同步數據,沒有延遲
概括來說就是:前者關注性能,后者關注一致性
具體發送頻率與Linux內核的配置有關,默認配置為40ms。當設置為no時,TCP會立馬將主節點的數據發送給從節點,帶寬增加但延遲變小。
一般來說,只有當應用對Redis數據不一致的容忍度較高,且主從節點之間網絡狀況不好時,才會設置為yes;多數情況使用默認值no
Redis是如何保證主從服務器一致處于連接狀態以及命令是否丟失?
答:命令傳播階段,從服務器會利用心跳檢測機制定時的向主服務發送消息。
心跳機制我們下面再詳細說。
全量復制和部分復制
在Redis2.8以前,從節點向主節點發送sync命令請求同步數據,此時的同步方式是全量復制;在Redis2.8及以后,從節點可以發送psync命令請求同步數據,此時根據主從節點當前狀態的不同,同步方式可能是全量復制或部分復制。后文介紹以Redis2.8及以后版本為例。
- 全量復制:用于初次復制或其他無法進行部分復制的情況,將主節點中的所有數據都發送給從節點,是一個非常重型的操作。
- 部分復制:用于網絡中斷等情況后的復制,只將中斷期間主節點執行的寫命令發送給從節點,與全量復制相比更加高效。需要注意的是,如果網絡中斷時間過長,導致主節點沒有能夠完整地保存中斷期間執行的寫命令,則無法進行部分復制,仍使用全量復制。
全量復制
Redis通過psync命令進行全量復制的過程如下:
(1)從節點判斷無法進行部分復制,向主節點發送全量復制的請求;或從節點發送部分復制的請求,但主節點判斷無法進行部分復制;具體判斷過程需要在講述了部分復制原理后再介紹。
(2)主節點收到全量復制的命令后,執行bgsave,在后臺生成RDB文件,并使用一個緩沖區(稱為復制緩沖區)記錄從現在開始執行的所有寫命令
(3)主節點的bgsave執行完成后,將RDB文件發送給從節點;從節點首先清除自己的舊數據,然后載入接收的RDB文件,將數據庫狀態更新至主節點執行bgsave時的數據庫狀態
(4)主節點將前述復制緩沖區中的所有寫命令發送給從節點,從節點執行這些寫命令,將數據庫狀態更新至主節點的最新狀態
(5)如果從節點開啟了AOF,則會觸發bgrewriteaof的執行,從而保證AOF文件更新至主節點的最新狀態
通過全量復制的過程可以看出,全量復制是非常重型的操作:
(1)主節點通過bgsave命令fork子進程進行RDB持久化,該過程是非常消耗CPU、內存(頁表復制)、硬盤IO的;關于bgsave的性能問題,可以參考我另外一篇文章: 深入剖析Redis高可用系列:持久化 AOF和RDB
(2)主節點通過網絡將RDB文件發送給從節點,對主從節點的帶寬都會帶來很大的消耗
(3)從節點清空老數據、載入新RDB文件的過程是阻塞的,無法響應客戶端的命令;如果從節點執行bgrewriteaof,也會帶來額外的消耗
部分復制
由于全量復制在主節點數據量較大時效率太低,因此Redis2.8開始提供部分復制,用于處理網絡中斷時的數據同步。
部分復制的實現,依賴于三個重要的概念:
- 復制偏移量
- 復制積壓緩沖區
- 服務器運行ID(runid)
下面我們分別講解一下這三個概念:
1. 復制偏移量:
執行復制的雙方,主從節點,分別會維護一個復制偏移量offset:
主節點每次向從節點同步了N字節數據后,將修改自己的復制偏移量offset+N
從節點每次從主節點同步了N字節數據后,將修改自己的復制偏移量offset+N
offset用于判斷主從節點的數據庫狀態是否一致:
如果二者offset相同,則一致;
如果offset不同,則不一致,此時可以根據兩個offset找出從節點缺少的那部分數據。
例如,如果主節點的offset是1000,而從節點的offset是500,那么部分復制就需要將offset為501-1000的數據傳遞給從節點。而offset為501-1000的數據存儲的位置,就是下面要介紹的復制積壓緩沖區。
2. 復制積壓緩沖區:
主節點內部維護了一個固定長度的、先進先出(FIFO)隊列 作為復制積壓緩沖區,其默認大小為1MB
在主節點進行命令傳播時,不僅會將寫命令同步到從節點,還會將寫命令寫入復制積壓緩沖區。
由于復制積壓緩沖區定長且是先進先出,所以它保存的是主節點最近執行的寫命令;時間較早的寫命令會被擠出緩沖區。因此,當主從節點offset的差距過大超過緩沖區長度時,將無法執行部分復制,只能執行全量復制。
為了提高網絡中斷時部分復制執行的概率,可以根據需要增大復制積壓緩沖區的大小(通過配置repl-backlog-size
);例如如果網絡中斷的平均時間是60s,而主節點平均每秒產生的寫命令(特定協議格式)所占的字節數為100KB,則復制積壓緩沖區的平均需求為6MB,保險起見,可以設置為12MB,來保證絕大多數斷線情況都可以使用部分復制。
從節點將offset發送給主節點后,主節點根據offset和緩沖區大小決定能否執行部分復制:
- 如果offset偏移量之后的數據,仍然都在復制積壓緩沖區里,則執行部分復制;
- 如果offset偏移量之后的數據已不在復制積壓緩沖區中(數據已被擠出),則執行全量復制。
復制積壓緩沖區示意圖:
3. 服務器運行ID(runid):
每個Redis節點,都有其運行ID,運行ID由節點在啟動時自動生成,主節點會將自己的運行ID發送給從節點,從節點會將主節點的運行ID存起來。
從節點Redis斷開重連的時候,就是根據運行ID來判斷同步的進度:
- 如果從節點保存的runid與主節點現在的runid相同,說明主從節點之前同步過,主節點會繼續嘗試使用部分復制(到底能不能部分復制還要看offset和復制積壓緩沖區的情況);
- 如果從節點保存的runid與主節點現在的runid不同,說明從節點在斷線前同步的Redis節點并不是當前的主節點,只能進行全量復制。
psync 命令的執行
在了解了復制偏移量、復制積壓緩沖區、節點運行id之后,本節將介紹psync命令的參數和返回值,從而說明psync命令執行過程中,主從節點是如何確定使用全量復制還是部分復制的。
psync命令流程圖如下:
psync命令的大體流程如下:
- 如果從節點之前沒有復制過任何主節點,或者之前執行過slaveof no one命令,從節點就會向主節點發送
psync
命令,請求主節點進行數據的全量同步 - 如果前面從節點已經同步過部分數據,此時從節點就會發送psync {runid} {offset}命令給主節點,其中runid是上一次主節點的運行ID,offset是當前從節點的復制偏移量
主節點收到psync命令后,會出現以下三種可能:
- 主節點返回 fullresync {runid} {offset}回復,表示主節點要求與從節點進行數據的完整全量復制,其中runid表示主節點的運行ID,offset表示當前主節點的復制偏移量
- 如果主服務器返回 +continue,表示主節點與從節點會進行部分數據的同步操作,將從服務器缺失的數據復制過來即可
- 如果主服務器返回 -err,表示主服務器的Redis版本低于2.8,無法識別psync命令,此時從服務器會向主服務器發送sync命令,進行完整的數據全量復制
心跳檢測機制
心跳檢測機制的作用有三個:
- 檢查主從服務器的網絡連接狀態
- 輔助實現min-slaves選項
- 檢測命令丟失
檢查主從服務器的網絡連接狀態
主節點信息中可以看到所屬的從節點的連接信息:
- state 表示從節點狀態
- offset 表示復制偏移量
- lag 表示延遲值(幾秒之前有過心跳檢測機制)
- 在這里插入圖片描述
輔助實現min-slaves選項
Redis.conf配置文件中有下方兩個參數
# 未達到下面兩個條件時,寫操作就不會被執行
# 最少包含的從服務器
# min-slaves-to-write 3
# 延遲值
# min-slaves-max-lag 10
如果將兩個參數的注釋取消,那么如果從服務器的數量少于3個,或者三個從服務器的延遲(lag)大于等于10秒時,主服務器都會拒絕執行寫命令。
檢測命令丟失
在從服務器的連接信息中可以看到復制偏移量,如果此時主服務器的復制偏移量與從服務器的復制偏移量不一致時,主服務器會補發缺失的數據。
實踐中的問題
1.讀寫分離及其中的問題
在主從復制基礎上實現的讀寫分離,可以實現Redis的讀負載均衡:由主節點提供寫服務,由一個或多個從節點提供讀服務(多個從節點既可以提高數據冗余程度,也可以最大化讀負載能力);在讀負載較大的應用場景下,可以大大提高Redis服務器的并發量。下面介紹在使用Redis讀寫分離時,需要注意的問題。
1. 延遲與不一致問題
前面已經講到,由于主從復制的命令傳播是異步的,延遲與數據的不一致不可避免。如果應用對數據不一致的接受程度程度較低,可能的優化措施包括:優化主從節點之間的網絡環境(如在同機房部署);監控主從節點延遲(通過offset)判斷,如果從節點延遲過大,通知應用不再通過該從節點讀取數據;使用集群同時擴展寫負載和讀負載等。
在命令傳播階段以外的其他情況下,從節點的數據不一致可能更加嚴重,例如連接在數據同步階段,或從節點失去與主節點的連接時等。從節點的slave-serve-stale-data參數便與此有關:它控制這種情況下從節點的表現;如果為yes(默認值),則從節點仍能夠響應客戶端的命令,如果為no,則從節點只能響應info、slaveof等少數命令。該參數的設置與應用對數據一致性的要求有關;如果對數據一致性要求很高,則應設置為no。
2. 數據過期問題
在單機版Redis中,存在兩種刪除策略:
惰性刪除:服務器不會主動刪除數據,只有當客戶端查詢某個數據時,服務器判斷該數據是否過期,如果過期則刪除。
定期刪除:服務器執行定時任務刪除過期數據,但是考慮到內存和CPU的折中(刪除會釋放內存,但是頻繁的刪除操作對CPU不友好),該刪除的頻率和執行時間都受到了限制。
在主從復制場景下,為了主從節點的數據一致性,從節點不會主動刪除數據,而是由主節點控制從節點中過期數據的刪除。由于主節點的惰性刪除和定期刪除策略,都不能保證主節點及時對過期數據執行刪除操作,因此,當客戶端通過Redis從節點讀取數據時,很容易讀取到已經過期的數據。
Redis 3.2中,從節點在讀取數據時,增加了對數據是否過期的判斷:如果該數據已過期,則不返回給客戶端;將Redis升級到3.2可以解決數據過期問題。
3. 故障切換問題
在沒有使用哨兵的讀寫分離場景下,應用針對讀和寫分別連接不同的Redis節點;當主節點或從節點出現問題而發生更改時,需要及時修改應用程序讀寫Redis數據的連接;連接的切換可以手動進行,或者自己寫監控程序進行切換,但前者響應慢、容易出錯,后者實現復雜,成本都不算低。
4. 總結
在使用讀寫分離之前,可以考慮其他方法增加Redis的讀負載能力:如盡量優化主節點(減少慢查詢、減少持久化等其他情況帶來的阻塞等)提高負載能力;使用Redis集群同時提高讀負載能力和寫負載能力等。如果使用讀寫分離,可以使用哨兵,使主從節點的故障切換盡可能自動化,并減少對應用程序的侵入。
2. 復制超時問題
主從節點復制超時是導致復制中斷的最重要的原因之一,本小節單獨說明超時問題,下一小節說明其他會導致復制中斷的問題。
超時判斷意義
在復制連接建立過程中及之后,主從節點都有機制判斷連接是否超時,其意義在于:
如果主節點判斷連接超時,其會釋放相應從節點的連接,從而釋放各種資源,否則無效的從節點仍會占用主節點的各種資源(輸出緩沖區、帶寬、連接等);此外連接超時的判斷可以讓主節點更準確的知道當前有效從節點的個數,有助于保證數據安全(配合前面講到的min-slaves-to-write等參數)。
如果從節點判斷連接超時,則可以及時重新建立連接,避免與主節點數據長期的不一致。
判斷機制
主從復制超時判斷的核心,在于repl-timeout參數,該參數規定了超時時間的閾值(默認60s),對于主節點和從節點同時有效;主從節點觸發超時的條件分別如下:
主節點:每秒1次調用復制定時函數replicationCron(),在其中判斷當前時間距離上次收到各個從節點REPLCONF ACK的時間,是否超過了repl-timeout值,如果超過了則釋放相應從節點的連接。
-
從節點:從節點對超時的判斷同樣是在復制定時函數中判斷,基本邏輯是:
- 如果當前處于連接建立階段,且距離上次收到主節點的信息的時間已超過repl-timeout,則釋放與主節點的連接;
- 如果當前處于數據同步階段,且收到主節點的RDB文件的時間超時,則停止數據同步,釋放連接;
- 如果當前處于命令傳播階段,且距離上次收到主節點的PING命令或數據的時間已超過repl-timeout值,則釋放與主節點的連接。
需要注意的坑
下面介紹與復制階段連接超時有關的一些實際問題:
數據同步階段:在主從節點進行全量復制bgsave時,主節點需要首先fork子進程將當前數據保存到RDB文件中,然后再將RDB文件通過網絡傳輸到從節點。如果RDB文件過大,主節點在fork子進程+保存RDB文件時耗時過多,可能會導致從節點長時間收不到數據而觸發超時;此時從節點會重連主節點,然后再次全量復制,再次超時,再次重連……這是個悲傷的循環。為了避免這種情況的發生,除了注意Redis單機數據量不要過大,另一方面就是適當增大repl-timeout值,具體的大小可以根據bgsave耗時來調整。
命令傳播階段:如前所述,在該階段主節點會向從節點發送PING命令,頻率由repl-ping-slave-period控制;該參數應明顯小于repl-timeout值(后者至少是前者的幾倍)。否則,如果兩個參數相等或接近,網絡抖動導致個別PING命令丟失,此時恰巧主節點也沒有向從節點發送數據,則從節點很容易判斷超時。
慢查詢導致的阻塞:如果主節點或從節點執行了一些慢查詢(如keys *或者對大數據的hgetall等),導致服務器阻塞;阻塞期間無法響應復制連接中對方節點的請求,可能導致復制超時。
總結
Redis 高可用系列的第二彈:主從復制的核心內容已經講解完畢了,希望大家閱讀完會有一點收獲,另外如果大家覺得看完文章有幫助的話,可以點贊,收藏微微表示一下支持,好讓我能有動力繼續寫技術文章。
另外:我開了公眾號【碼農富哥】(id: coder2025),專注于寫后端,服務端的原創干文,持續更新手法文章,大家有興趣也可以關注一下!
歡迎關注公眾號:「碼農富哥」,致力于分享后端技術 (高并發架構,分布式集群系統,消息隊列中間件,網絡,微服務,Linux, TCP/IP, HTTP, MySQL, Redis), Python 等 原創干貨 和 面試指南!