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