1. 程式人生 > >8 Redis主從複製及哨兵模式

8 Redis主從複製及哨兵模式

Redis主從複製及哨兵模式

1.主從複製

1.1配置主從複製

  • 參與複製的Redis例項劃分為主節點(master) 和從節點(slave) 。 預設情況下, Redis都是主節點。 每個從節點只能有一個主節點, 而主節點可以同時具有多個從節點。 複製的資料流是單向的, 只能由主節點複製到從節點。 配置複製的方式有以下三種:

    1. 在從節點的配置檔案redis.conf中加入slaveof{masterHost}{masterPort}隨Redis啟動生效。
    2. 在redis-server啟動命令後加入–slaveof{masterHost}{masterPort}生效。
    3. 在redis客戶端連線從節點
      後直接使用命令: slaveof{masterHost}{masterPort}生效。
  • 執行redis客戶端info replication可以檢視此節點的複製資訊

129:6381:0>info replication
"# Replication
role:master
connected_slaves:2
slave0:ip=172.16.10.129,port=6383,state=online,offset=17281721,lag=1
slave1:ip=172.16.10.129,port=6382,state=online,offset=17281721,lag=1
master_replid:91b8764503bafef32afb4eb4b67758cae97a3ec6
master_replid2:020324c0fce02e8704b33cd3cb719c6ad0fdb7b6
master_repl_offset:17281721
second_repl_offset:14246373
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:16233146
repl_backlog_histlen:1048576
  • 如下表為複製緩衝區狀態說明:

  • 可以在客戶端通過執行==slaveof no one ===命令斷開主從複製關係,從節點後斷開並不會拋棄原有數。
  • 從節點斷開後可以切換到其它主機,但是與其它主機建立關係後會清空原有資料,若線上人工操作要小心。
  • 為了保證安全性,主從節點最好都配置requirepass引數進行密碼驗證, 從節點配置masterauth引數進行驗證,為了方便維護從主從節點最好配置同樣的密碼,在哨兵模式中,任何一臺從機都有可能成為主機,因此密碼及密碼驗證最好都做成相同的配置。
  • 預設情況下從節點使用slave-read-only=yes配置為只讀模式。 由於複製只能從主節點到從節點, 對於從節點的任何修改主節點都無法感知, 修改從節點會造成主從資料不一致。 因此建議線上不要修改從節點的只讀模式。
  • repl-disable-tcp-nodelay引數用於控制網路延遲, 預設關閉 。關閉時候延遲較小但是增加了網路頻寬的消耗,開啟的時候則效果相反。

1.2主從複製拓撲

  • 一主一從結構

  • 最簡單的複製拓撲結構, 用於主節點出現宕機時從節點提供故障轉移支援 。 當應用寫命令併發量較高需要持久化時,可以只在從節點上開啟AOF, 這樣既保證資料安全性同時也避免了持久化對主節點的性能干擾。 但需要注意的是, 當主節點關閉持久化功能時,如果主節點離線要避免自動重啟操作。 因為主節點之前沒有開啟持久化功能自動重啟後資料集為空, 這時從節點如果繼續複製主節點會導致從節點資料也被清空。

  • 一主多從結構

  • 一主多從結構(又稱為星形拓撲結構) 使得應用端可以利用多個從節點實現讀寫分離 。 適用讀佔比較大的場景, 可以把讀命令傳送到從節點來分擔主節點壓力。 對於寫併發量較高的場景, 多個從節點會導致主節點寫命令的多次傳送從而過度消耗網路頻寬, 同時也加重了主節點的負載影響服務穩定性。

  • 樹狀主從結構

  • 樹狀拓撲結構使得從節點不但可以複製主節點資料,同時可以作為其他從節點的主節點繼續向下層複製。通過引入複製中間層,可以有效降低主節點負載和需要傳送給從節點的資料量。當主節點需要掛載多個從節點時為了避免對主節點的性能干擾,可以採用樹狀主從結構降低主節點壓力,適用寫併發量較高的場景,但是此種結構對運維來說比較複雜

1.3主從複製原理

1.3.1複製過程

  1. 儲存主節點(master) 資訊。執行slaveof後從節點只儲存主節點的地址資訊, 這時建立複製流程還沒有開始

  2. 從節點(slave) 內部通過每秒執行的定時任務維護複製相關邏輯,當定時任務發現存在新的主節點後, 會嘗試與該節點建立網路連線, 連線成功後會有如下日誌。連線失敗的話定時任務會無限重試直到連線成功或者執行slaveof no one取消複製。

    12399:S 15 Nov 11:52:44.777 * Connecting to MASTER 172.16.10.129:6381
    12399:S 15 Nov 11:52:44.777 * MASTER <-> SLAVE sync started
    
  3. 傳送連線建立成功後從節點發送ping請求進行首次通訊, ping請求的目的是檢測主從之間網路套接字是否可用,檢測主節點當前是否可接受處理命令。如果傳送ping命令後, 從節點沒有收到主節點的pong回覆或者超時, 比如網路超時或者主節點正在阻塞無法響應命令, 從節點會斷開復制連線, 下次定時任務會發起重連 。成功的話會列印如下日誌。

    12399:S 15 Nov 11:52:44.777 * Master replied to PING, replication can continue...
    
  4. 許可權驗證。 如果主節點設定了requirepass引數, 則需要密碼驗證,從節點必須配置masterauth引數保證與主節點相同的密碼才能通過驗證 。

  5. 同步資料集。 主從複製連線正常通訊後, 對於首次建立複製的場景, 主節點會把持有的資料全部發送給從節點, 這部分操作耗時最長的。

  6. 命令持續複製。 主節點把當前的資料同步給從節點後, 完成了複製的建立流程。 接下來主節點會持續地把寫命令傳送給從節點, 保證主從資料一致性。

1.3.2資料同步

  • Redis在2.8及以上版本使用psync命令完成主從資料同步, 同步過程分為: 全量複製和部分複製。

  • 全量複製: 一般用於初次複製場景, Redis早期支援的複製功能只有全量複製, 它會把主節點全部資料一次性發送給從節點, 當資料量較大時, 會對主從節點和網路造成很大的開銷。

  • 部分複製: 用於處理在主從複製中因網路閃斷等原因造成的資料丟失場景, 當從節點再次連上主節點後, 主節點會補發丟失資料給從節點。 因為補發的資料遠遠小於全量資料, 可以有效避免全量複製的過高開銷。

  • psync命令 ,從節點使用psync命令完成部分複製和全量複製功能, 命令格式:psync{runId}{offset}, runId: 從節點所複製主節點的執行id。offset: 當前從節點已複製的資料偏移量

  • 從節點(slave) 傳送psync命令給主節點, 引數runId是當前從節點儲存的主節點執行ID,引數offset是當前從節點儲存的複製偏移量, 如果是第一次參與複製則預設值為-1。
  • 主節點(master) 根據psync引數和自身資料情況決定響應結果:如果回覆+FULLRESYNC{runId}{offset}, 那麼從節點將觸發全量複製流程。如果回覆+CONTINUE, 從節點將觸發部分複製流程。如果回覆+ERR, 說明主節點版本低於Redis2.8, 無法識別psync命令,從節點將傳送舊版的sync命令觸發全量複製流程。

1.3.3全量複製

  • 流程分析

    1. 傳送psync命令進行資料同步, 由於是第一次進行復制, 從節點沒有複製偏移量和主節點的執行ID, 所以傳送psync-1。

    2. 主節點根據psync-1解析出當前為全量複製, 回覆+FULLRESYNC響應。

    3. 從節點接收主節點的響應資料儲存執行ID和偏移量offset。從節點列印如下日誌

      Partial resynchronization not possible (no cached master)
      Full resync from master: 92d1cb14ff7ba97816216f7beb839efe036775b2:216789
      
    4. 主節點執行bgsave儲存RDB檔案到本地 ,此時主節點日誌

      1500:M 15 Nov 11:52:44.777 * Starting BGSAVE for SYNC with target: disk
      1500:M 15 Nov 11:52:44.778 * Background saving started by pid 12417
      12417:C 15 Nov 11:52:44.842 * DB saved on disk
      12417:C 15 Nov 11:52:44.843 * RDB: 4 MB of memory used by copy-on-write
      1500:M 15 Nov 11:52:44.903 * Background saving terminated with success
      
    5. 主節點發送RDB檔案給從節點, 從節點把接收的RDB檔案儲存在本地並直接作為從節點的資料檔案 ,如果檔案過大,總時間超過repl-timeout所配置的值(預設60秒) , 從節點將放棄接受RDB檔案並清理已經下載的臨時檔案, 導致全量複製失敗 ,針對網路配置超時時間。在從節點日誌可以看到接受到的資料

      12399:S 15 Nov 11:52:44.903 * MASTER <-> SLAVE sync: receiving 205 bytes from master
      
    6. 對於從節點開始接收RDB快照到接收完成期間, 主節點仍然響應讀寫命令, 因此主節點會把這期間寫命令資料儲存在複製客戶端緩衝區內, 當從節點載入完RDB檔案後, 主節點再把緩衝區內的資料傳送給從節點, 保證主從之間資料一致性。 如果主節點建立和傳輸RDB的時間過長, 對於高流量寫入場景非常容易造成主節點複製客戶端緩衝區溢位。針對業務配置clientoutput-buffer-limit 快取的引數。對於主節點, 當傳送完所有的資料後就認為全量複製完成 。

    7. 從節點接收完主節點傳送來的全部資料後會清空自身舊資料,此時從節點日誌

      12399:S 15 Nov 11:52:44.903 * MASTER <-> SLAVE sync: Flushing old data
      
    8. 從節點清空資料後開始載入RDB檔案,此時從節點日誌

      12399:S 15 Nov 11:52:44.903 * MASTER <-> SLAVE sync: Loading DB in memory
      12399:S 15 Nov 11:52:44.903 * MASTER <-> SLAVE sync: Finished with success
      
    9. 從節點成功載入完RDB後, 如果當前節點開啟了AOF持久化功能,它會立刻做bgrewriteaof操作 。

  • 通過分析全量複製的所有流程, 全量複製是一個非常耗時費力的操作。 它的時間開銷主要包括:

    • 主節點bgsave時間。
    • RDB檔案網路傳輸時間。
    • 從節點清空資料時間。
    • 從節點載入RDB的時間。
    • 可能的AOF重寫時間

1.3.4部分複製

  • 部分複製主要是Redis針對全量複製的過高開銷做出的一種優化措施,使用psync{runId}{offset}命令實現。 當從節點(slave) 正在複製主節點(master) 時, 如果出現網路閃斷或者命令丟失等異常情況時, 從節點會向主節點要求補發丟失的命令資料, 如果主節點的複製積壓緩衝區記憶體在這部分資料則直接傳送給從節點, 這樣就可以保持主從節點複製的一致性

  • 流程說明
    1. 當主從節點之間網路出現中斷時, 如果超過repl-timeout時間, 主節點會認為從節點故障並中斷連線
    2. 主從連線中斷期間主節點依然響應命令, 但因複製連線中斷命令無法傳送給從節點, 不過主節點內部存在的複製積壓緩衝區, 依然可以儲存最近一段時間的寫命令資料, 預設最大快取1MB。
    3. 當主從節點網路恢復後, 從節點會再次連上主節點。
    4. 當主從連線恢復後, 由於從節點之前儲存了自身已複製的偏移量和主節點的執行ID。 因此會把它們當作psync引數傳送給主節點, 要求進行部分複製操作
    5. 主節點接到psync命令後首先核對引數runId是否與自身一致, 如果一致, 說明之前複製的是當前主節點; 之後根據引數offset在自身複製積壓緩衝區查詢, 如果偏移量之後的資料存在緩衝區中, 則對從節點發送CONTINUE響應, 表示可以進行部分複製
    6. 節點根據偏移量把複製積壓緩衝區裡的資料傳送給從節點, 保證主從複製進入正常狀態

1.3.5主從心跳機制

  • 從節點在建立複製後, 它們之間維護著長連線並彼此傳送心跳命令

  • 主從心跳判斷機制:
    • 主從節點彼此都有心跳檢測機制, 各自模擬成對方的客戶端進行通訊
    • 主節點預設每隔10秒對從節點發送ping命令, 判斷從節點的存活性連線狀態。 可通過引數repl-ping-slave-period控制傳送頻率。
    • 從節點在主執行緒中每隔1秒傳送replconf ack{offset}命令, 給主節點上報自身當前的複製偏移量。 replconf命令主要作用如下:
      • 實時監測主從節點網路狀態。
      • 上報自身複製偏移量, 檢查複製資料是否丟失, 如果從節點資料丟失, 再從主節點的複製緩衝區中拉取丟失資料。

1.3.6命令持續非同步複製

  • 主節點不但負責資料讀寫, 還負責把寫命令同步給從節點。 寫命令的傳送過程是非同步完成, 也就是說主節點自身處理完寫命令後直接返回給客戶端, 並不等待從節點複製完成 。

  • 由於主從複製過程是非同步的, 就會造成從節點的資料相對主節點存在延遲。 具體延遲多少位元組, 可以在執行info replication命令查

    slave0:ip=127.0.0.1,port=6381,state=online,offset=841,lag=1
    master_repl_offset:841
    
  • offset表示當前從節點的複製偏移量,master_repl_offset表示當前主節點的複製偏移量, 兩者的差值就是當前從節點複製延遲量

1.4會遇到的問題

1.讀寫分離

  • 對於讀佔比較高的場景, 可以通過把一部分讀流量分攤到從節點(slave) 來減輕主節點(master) 壓力, 同時需要注意永遠只對主節點執行寫操作
  • 當使用從節點響應讀請求時, 業務端可能會遇到如下問題:
    • 複製資料延遲。監控主從節點資料偏移量差值大小,超過一點數值報警
    • 讀到過期資料。優化記憶體管理的策略
    • 從節點故障 。針對從節點連線池做一個監控

2.主從配置不一致

  • 主從配置不一致是一個容易忽視的問題。 對於有些配置主從之間是可以不一致, 比如: 主節點關閉AOF在從節點開啟。 但對於記憶體相關的配置必須要一致, 比如maxmemory等引數。 當配置的maxmemory從節點小於主節點, 如果複製的資料量超過從節點maxmemory時, 它會根據maxmemory-policy策略進行記憶體溢位控制, 此時從節點資料已經丟失, 但主從複製流程依然正常進行, 複製偏移量也正常。 修復這類問題也只能手動進行全量複製。 當壓縮列表相關引數不一致時, 雖然主從節點儲存的資料一致但實際記憶體佔用情況差異會比較大。

3.規避全量複製

  • 第一次建立複製: 當對資料量較大且流量較高的主節點新增從節點時, 建議在低峰時進行操作, 或者儘量規避使用大資料量的Redis節點

  • 節點執行ID不匹配 :當主從複製關係建立後, 從節點會儲存主節點的執行ID, 如果此時主節點因故障重啟, 那麼它的執行ID會改變, 從節點發現主節點執行ID不匹配時, 會認為自己複製的是一個新的主節點從而進行全量複製。 對於這種情況應該從架構上規避, 比如提供故障轉移功能。 當主節點發生故障後, 手動提升從節點為主節點或者採用支援自動故障轉移的哨兵或叢集方案

  • 複製積壓緩衝區不足: 當主從節點網路中斷後, 從節點再次連上主節請求部分複製, 如果請求的偏移量不在
    主節點的積壓緩衝區內, 則無法提供給從節點資料, 因此部分複製會退化為全量複製。 針對這種情況需要根據網路中斷時長, 寫命令資料量分析出合理的積壓緩衝區大小。

4.規避複製風暴

  • 複製風暴是指大量從節點對同一主節點或者對同一臺機器的多個主節點短時間內發起全量複製的過程。 當主節點重啟恢復後, 從節點會發起全量複製流程, 主節點向多個從節點發送RDB快照, 可能使主節點的網路頻寬消耗嚴重, 造成主節點的延遲變大, 極端情況會發生主從節點連線斷開, 導致複製失敗。解決方案首先可以減少主節點(master) 掛載從節點(slave) 的數量,或者採用樹狀複製結構, 加入中間層從節點用來保護主節點。

2.哨兵模式Sentinel

  • Redis Sentinel是Redis的高可用實現方案, 在實際的生產環境中, 對提高整個系統的高可用性非常有幫助

2.1主從複製的問題及哨兵模式的高可用性

2.1.1主從複製的問題

  • 主從複製的作用體現在兩個方法,一個是主節點的備份,主節點出現不可達可頂替,二是可以擴充套件主節點的讀能力。但是主從複製也帶來了以下問題
  1. 一旦主節點出現故障, 需要手動將一個從節點晉升為主節點, 同時需要修改應用方的主節點地址, 還需要命令其他從節點去複製新的主節點, 整個過程都需要人工干預。(Redis的高可用問題 ,sentinel解決)
  2. 主節點的寫能力受到單機的限制。(Redis的分散式問題 ,cluster解決)
  3. 主節點的儲存能力受到單機的限制。 (Redis的分散式問題 ,cluster解決)

2.1.2 Redis Sentinel的高可用性

  • Redis Sentinel是一個分散式架構, 其中包含若干個Sentinel節點和Redis資料節點, 每個Sentinel節點會對資料節點和其餘Sentinel節點進行監控, 當它發現節點不可達時, 會對節點做下線標識。 如果被標識的是主節點, 它還會和其他Sentinel節點進行“協商”, 當大多數Sentinel節點都認為主節點不可達時, 它們會選舉出一個Sentinel節點來完成自動故障轉移的工作, 同時會將這個變化實時通知給Redis應用方。 整個過程完全是自動的, 不需要人工來介入,能有效地解決了Redis的高可用問題。

  • 下面以1個主節點、 2個從節點、 3個Sentinel節點組成的Redis Sentinel為例子進行說明, 拓撲結構如圖

  • 整個故障轉移的處理邏輯有下面4個步驟:
  1. 如下圖所示 主節點出現故障, 此時兩個從節點與主節點失去連線, 主從複製失敗

2) 如下圖所示, 每個Sentinel節點通過定期監控發現主節點出現了故障。

3) 如下圖所示, 多個Sentinel節點對主節點的故障達成一致, 選舉出sentinel-3節點作為領導者負責故障轉移。

4) 如下圖所示, Sentinel領導者節點執行了故障轉移, 整個過程 是自動化完成的

5)故障轉移後整個Redis Sentinel的拓撲結構如下所示

  • 通過上面介紹的Redis Sentinel邏輯架構以及故障轉移的處理, 可知Redis Sentinel具有以下幾個功能

    • 監控: Sentinel節點會定期檢測Redis資料節點、 其餘Sentinel節點是否可達。
    • 通知: Sentinel節點會將故障轉移的結果通知給應用方。
    • 主節點故障轉移: 實現從節點晉升為主節點並維護後續正確的主從關係。
    • 配置提供者: 在Redis Sentinel結構中, 客戶端在初始化的時候連線的是Sentinel節點集合, 從中獲取主節點資訊
  • Redis Sentinel包含了若個Sentinel節點, 這樣做也帶來了兩個好處

    • 對於節點的故障判斷是由多個Sentinel節點共同完成, 這樣可以有效地防止誤判。
    • Sentinel節點集合是由若干個Sentinel節點組成的, 這樣即使個別Sentinel節點不可用, 整個Sentinel節點集合依然是健壯的

2.2安裝和部署

角色 ip port
master 172.16.10.129 6381
slave-1 172.16.10.129 6382
slave-2 172.16.10.129 6383
sentinel-1 172.16.10.129 26381
sentinel-2 172.16.10.129 26382
sentinel-3 172.16.10.129 26383
  • 配置三個資料節點,資料節點的安裝和主從複製一樣。
  • 配置3個哨兵,每個哨兵的配置都一樣,在 Redis 安裝目錄下可以找到sentinel.conf檔案

  • 通過以下命令啟動Redis伺服器和哨兵。注意伺服器啟動的順序,首先是主機的Redis服務程序,然後啟動從機的服務程序,最後啟動3個哨兵的服務程序。

  • 可以通過以下命令檢視sentinel資訊。生產環境中建議Redis Sentinel的所有節點應該分佈在不同的物理機上。Redis Sentinel中的資料節點和普通的Redis資料節點在配置上沒有任何區別,只不過是添加了一些Sentinel節點對它們進行監控

    $ redis-cli -h 172.16.10.129 -p 26381 -a 123456 info Sentinel
    # Sentinel
    sentinel_masters:1
    sentinel_tilt:0
    sentinel_running_scripts:0
    sentinel_scripts_queue_length:0
    master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3
    

  • 監視主節點的配置

sentinel monitor <master-name> <ip> <port> <quorum>
  1. 代表要判定主節點最終不可達所需要的票數。 但實際上Sentinel節點會對所有節點進行監控, 但是在Sentinel節點的配置中沒有看到有關從節點和其餘Sentinel節點的配置, 那是因為Sentinel節點會從主節點中獲取有關從節點以及其餘Sentinel節點的相關資訊,
  2. 引數用於故障發現和判定, 例如將quorum配置為2, 代表至少有2個Sentinel節點認為主節點不可達, 那麼這個不可達的判定才是客觀的。對於設定的越小, 那麼達到下線的條件越寬鬆, 反之越嚴格。 一般建議將其設定為Sentinel節點的一半加1。
  3. 同時還與Sentinel節點的領導者選舉有關, 至少要有max(quorum, num(sentinels) /2+1) 個Sentinel節點參與選舉, 才能選出領導者Sentinel, 從而完成故障轉移。 例如有5個Sentinel節點, quorum=4, 那麼至少要有max(quorum, num(sentinels) /2+1) =4個線上Sentinel節點才可以
    進行領導者選舉。

2.3客戶端連線

  • 最瞭解主節點資訊的就是Sentinel節點集合, 而各個主節點可以通過進行標識的, 所以, 無論是哪種程式語言的客戶端, 如果需要正確地連線Redis Sentinel, 必須有Sentinel節點集合masterName兩個引數。

  • Java操作Redis Sentinel

    public static void testSentinel() {
            // 連線池配置
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10);
            jedisPoolConfig.setMaxIdle(5);
            jedisPoolConfig.setMinIdle(5);
            // 哨兵資訊
            Set<String> sentinels = new HashSet<String>(
                    Arrays.asList("172.16.10.129:26381", "172.16.10.129:26382", "172.16.10.129:26383"));
            // 建立連線池
            // mymaster是我們配置給哨兵的服務名稱
            // sentinels是哨兵資訊
            // jedisPoolConfig是連線池配置
            // 123456是連線Redis伺服器的密碼
            JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig,"123456");
            // 獲取客戶端
            Jedis jedis = pool.getResource();
            // 執行兩個命令
            jedis.set("mykey", "myvalue");
            String myvalue = jedis.get("mykey");
            // 列印資訊
            System.out.println(myvalue);
        }
    
  • 下面給出的程式碼就是JedisSentinelPool的初始化方法以及重要函式initSentinels

    public JedisSentinelPool(String masterName, Set<String> sentinels,
        final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
        final String password, final int database, final String clientName) {
      this.poolConfig = poolConfig;
      this.connectionTimeout = connectionTimeout;
      this.soTimeout = soTimeout;
      this.password = password;
      this.database = database;
      this.clientName = clientName;
    
      HostAndPort master = initSentinels(sentinels, masterName);
      initPool(master);
    }
    
    private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
        // 主節點
        HostAndPort master = null;
        // 遍歷所有sentinel節點
        for (String sentinel : sentinels) {
            // 連線sentinel節點
            HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
            Jedis jedis = new Jedis(hap.getHost(), hap.getPort());
            // 使用sentinel get-master-addr-by-name masterName獲取主節點資訊
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
            // 命令返回列表為空或者長度不為2, 繼續從下一個sentinel節點查詢
            if (masterAddr == null || masterAddr.size() != 2) {
                continue;
            }
            // 解析masterAddr獲取主節點資訊
            master = toHostAndPort(masterAddr);
            // 找到後直接跳出for迴圈
            break;
        }
        if (master == null) {
            // 直接丟擲異常, 
            throw new Exception();
        }
        // 為每個sentinel節點開啟主節點switch的監控執行緒
        for (String sentinel : sentinels) {
            final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
            MasterListener masterListener = new MasterListener(masterName, hap.getHost(), 
                hap.getPort());
            masterListener.start();
        }
        // 返回結果
        return master;
    }
    
  • 下面程式碼就是MasterListener的核心監聽程式碼, 程式碼中比較重要的部分就是訂閱Sentinel節點的+switch-master頻道, 它就是Redis Sentinel在結束對主節點故障轉移後會釋出切換主節點的訊息, Sentinel節點基本將故障轉移的各個階段發生的行為都通過這種釋出訂閱的形式對外提供 。

    Jedis sentinelJedis = new Jedis(sentinelHost, sentinelPort);
    // 客戶端訂閱Sentinel節點上"+switch-master"(切換主節點)頻道
    sentinelJedis.subscribe(new JedisPubSub() {
        @Override
        public void onMessage(String channel, String message) {
            String[] switchMasterMsg = message.split(" ");
            if (switchMasterMsg.length > 3) {
                // 判斷是否為當前masterName
                if (masterName.equals(switchMasterMsg[0])) {
                    // 發現當前masterName發生switch, 使用initPool重新初始化連線池
                    initPool(toHostAndPort(switchMasterMsg[3], switchMasterMsg[4]));
                }
            }
        }
    }, "+switch-master");
    

2.4哨兵模式的原理

2.4.1三個定時監控任務

1.每隔10秒, 每個Sentinel節點會向主節點和從節點發送info命令獲取最新的拓撲結構

  • 通過向主節點執行info命令, 獲取從節點的資訊, 這也是為什麼Sentinel節點不需要顯式配置監控從節點。
  • 當有新的從節點加入時都可以立刻感知出來。
  • 節點不可達或者故障轉移後, 可以通過info命令實時更新節點拓撲資訊

2.每隔2秒, 每個Sentinel節點會向Redis資料節點的__sentinel__: hello頻道上傳送該Sentinel節點對於主節點的判斷以及當前Sentinel節點的資訊, 同時每個Sentinel節點也會訂閱該頻道, 來了解其他Sentinel節點以及它們對主節點的判斷, 所以這個定時任務可以完成以下兩個工作:

  • 發現新的Sentinel節點: 通過訂閱主節點的__sentinel__: hello瞭解其他的Sentinel節點資訊, 如果是新加入的Sentinel節點, 將該Sentinel節點資訊儲存起來, 並與該Sentinel節點建立連線。
  • Sentinel節點之間交換主節點的狀態, 作為後面客觀下線以及領導者選舉的依據。

3.每隔1秒, 每個Sentinel節點會向主節點、 從節點、 其餘Sentinel節點發送一條ping命令做一次心跳檢測, 來確認這些節點當前是否可達。

  • 通過上面的三個定時任務, Sentinel節點對主節點、 從節點、 其餘Sentinel節點都建立起連線, 實現了對每個節點的監控, 這個定時任務是節點失敗判定的重要依據

2.4.2主觀下線和客觀下線

1.主觀下線 。

  • 上面介紹的第三個定時任務, 每個Sentinel節點會每隔1秒對主節點、 從節點、 其他Sentinel節點發送ping命令做心跳檢測, 當這些節點超過down-after-milliseconds沒有進行有效回覆, Sentinel節點就會對該節點做失敗判定, 這個行為叫做主觀下線。 從字面意思也可以很容易看出主觀下線是當前Sentinel節點的一家之言, 存在誤判的可能 。

2.客觀下線

  • 當Sentinel主觀下線的節點是主節點時, 該Sentinel節點會通過sentinel ismaster-down-by-addr命令向其他Sentinel節點詢問對主節點的判斷, 當超過個數, Sentinel節點認為主節點確實有問題, 這時該Sentinel節點會做出客觀下線的決定, 也就是大部分Sentinel節點都對主節點的下線做了同意的判定, 那麼這個判定就是客觀的, 如下圖所示。

2.4.3 領導者Sentinel節點選舉

  • 假如Sentinel節點對於主節點已經做了客觀下線, 那麼是不是就可以立即進行故障轉移了? 當然不是, 實際上故障轉移的工作只需要一個Sentinel節點來完成即可, 所以Sentinel節點之間會做一個領導者選舉的工作, 選出一個Sentinel節點作為領導者進行故障轉移的工作。 Redis使用了Raft演算法實現領導者選舉, 因為Raft演算法相對比較抽象和複雜, 這裡給出一個Redis Sentinel進行領導者選舉的大致思路:

    1. 每個線上的Sentinel節點都有資格成為領導者, 當它確認主節點客觀下線時候, 會向其他Sentinel節點發送sentinel is-master-down-by-addr命令,要求將自己設定為領導者。
    2. 收到命令的Sentinel節點, 如果沒有同意過其他Sentinel節點的sentinelis-master-down-by-addr命令, 將同意該請求, 否則拒絕。
    3. 如果該Sentinel節點發現自己的票數已經大於等於max(quorum,num(sentinels) /2+1) , 那麼它將成為領導者。
    4. 如果此過程沒有選舉出領導者, 將進入下一次選舉。
  • 選舉的過程非常快, 基本上誰先完成客觀下線, 誰就是領導者

2.5.3領導者Sentinel 故障轉移

  • 領導者選舉出的Sentinel節點負責故障轉移, 具體步驟如下:

1.在從節點列表中選出一個節點作為新的主節點, 選擇方法如下:

  • 過濾: “不健康”(主觀下線、 斷線) 、 5秒內沒有回覆過Sentinel節點ping響應、 與主節點失聯超過down-after-milliseconds*10秒。
  • 選擇slave-priority(從節點優先順序) 最高的從節點列表, 如果存在則返回, 不存在則繼續。
  • 選擇複製偏移量最大的從節點(複製的最完整) , 如果存在則返回, 不存在則繼續。
  • 選擇runid最小的從節點。

2.Sentinel領導者節點會對第一步選出來的從節點執行slaveof no one命令讓其成為主節點。

3.sentinel領導者節點會向剩餘的從節點發送命令, 讓它們成為新主節點的從節點, 複製規則和parallel-syncs 引數有關。

4.Sentinel節點集合會將原來的主節點更新為從節點, 並保持著對其關注, 當其恢復後命令它去複製新的主節點

2.5故障轉移日誌分析

序號 角色 IP port 程序號
1 master 172.16.10.129 6381 1500
2 slave-1 172.16.10.129 6382 12399
3 slave-2 172.16.10.129 6383 1510
4 sentinel-1 172.16.10.129 26381 1516
5 sentinel-2 172.16.10.129 26382 1521
6 sentinel-3 172.16.10.129 26383 1526
  • 以上為Redis Sentinel拓撲表,以下在操作中為方便用埠號代替端點名稱。
  • 使用kill -9 1500殺死掉6381主機程序,模擬主機宕機,時間為2018年11月16日10:39: 30。
  • 使用客戶端連線6382埠,使用info replication命令查詢,可知6382成為了master,且6383已經成為6382從節點。
129:6382:0>info replication
"# Replication
role:master
connected_slaves:1
slave0:ip=172.16.10.129,port=6383,state=online,offset=31857670,lag=0
master_replid:336ec72e013cec22ea7a9c5b08ea7e476abd8aae
master_replid2:91b8764503bafef32afb4eb4b67758cae97a3ec6
master_repl_offset:31857670
second_repl_offset:31801434
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:30809095
repl_backlog_histlen:1048576
"
  • 故障轉移日誌分析

(1)6381節點日誌

12417:C 15 Nov 11:52:44.842 * DB saved on disk
12417:C 15 Nov 11:52:44.843 * RDB: 4 MB of memory used by copy-on-write
1500:M 15 Nov 11:52:44.903 * Background saving terminated with success
1500:M 15 Nov 11:52:44.903 * Synchronization with slave 172.16.10.129:6382 succeeded
  • 由於為模擬宕機,6381節點在宕機時刻點沒有出現任何日誌,此處和shutdown不一樣,正常關機一般會觸發bgsave命令,儲存資料。

(2)6382節點日誌

#6382節點在10:39:30發現6381節點已經失聯
12399:S 16 Nov 10:39:30.412 # Connection with master lost.
12399:S 16 Nov 10:39:30.412 * Caching the disconnected master state.
12399:S 16 Nov 10:39:30.600 * Connecting to MASTER 172.16.10.129:6381
12399:S 16 Nov 10:39:30.600 * MASTER <-> SLAVE sync started
12399:S 16 Nov 10:39:30.600 # Error condition on socket for SYNC: Connection refused
12399:S 16 Nov 10:39:31.601 * Connecting to MASTER 172.16.10.129:638112399:S 16 Nov 10:39:30.412 # Connection with master lost.
12399:S 16 Nov 10:39:30.412 * Caching the disconnected master state.
12399:S 16 Nov 10:39:30.600 * Connecting to MASTER 172.16.10.129:6381
12399:S 16 Nov 10:39:30.600 * MASTER <-> SLAVE sync started
12399:S 16 Nov 10:39:30.600 # Error condition on socket for SYNC: Connection refused
12399:S 16 Nov 10:39:31.601 * Connecting to MASTER 172.16.10.129:6381
........(省略和之前的一樣)
12399:M 16 Nov 10:40:00.776 # Setting secondary replication ID to 91b8764503bafef32afb4eb4b67758cae97a3ec6, valid up to offset: 31801434. New replication ID is 336ec72e013cec22ea7a9c5b08ea7e476abd8aae


#10:40:00時,也就是30秒之後,6382接到Sentinel節點的命令:清理原來快取的主節點狀態,Sentinel節點將6382節點晉升為主節點,並重寫配置
12399:M 16 Nov 10:40:00.776 * Discarding previously cached master state.
12399:M 16 Nov 10:40:00.777 * MASTER MODE enabled (user request from 'id=11 addr=172.16.10.129:37340 fd=12 name=sentinel-18bd11bf-cmd age=82037 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
12399:M 16 Nov 10:40:00.778 # CONFIG REWRITE executed with success.

#6383節點發來了複製請求:
12399:M 16 Nov 10:40:01.748 * Slave 172.16.10.129:6383 asks for synchronization
12399:M 16 Nov 10:40:01.748 * Partial resynchronization request from 172.16.10.129:6383 accepted. Sending 869 bytes of backlog starting from offset 31801434.

(3)6383節點日誌

#同6381節點失聯
1510:S 16 Nov 10:39:30.412 # Connection with master lost.
1510:S 16 Nov 10:39:30.412 * Caching the disconnected master state.
1510:S 16 Nov 10:39:30.696 * Connecting to MASTER 172.16.10.129:6381
1510:S 16 Nov 10:39:30.696 * MASTER <-> SLAVE sync started
1510:S 16 Nov 10:39:30.696 # Error condition on socket for SYNC: Connection refused
1510:S 16 Nov 10:39:31.698 * Connecting to MASTER 172.16.10.129:6381
1510:S 16 Nov 10:39:31.698 * MASTER <-> SLAVE sync started
......(省略和之前的一樣)
1510:S 16 Nov 10:40:00.747 * Connecting to MASTER 172.16.10.129:6381
1510:S 16 Nov 10:40:00.747 * MASTER <-> SLAVE sync started
1510:S 16 Nov 10:40:00.747 # Error condition on socket for SYNC: Connection refused

#10:40:01秒時它接到Sentinel節點的命令,清理原來快取的主節點狀態,讓它去複製新的主節點(6382節點)
1510:S 16 Nov 10:40:01.347 * SLAVE OF 172.16.10.129:6382 enabled (user request from 'id=15 addr=172.16.10.129:49438 fd=10 name=sentinel-18bd11bf-cmd age=84053 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=141 qbuf-free=32627 obl=36 oll=0 omem=0 events=r cmd=exec')
1510:S 16 Nov 10:40:01.348 # CONFIG REWRITE executed with success.

#向新的主節點(6382節點)發起複製操作
1510:S 16 Nov 10:40:01.747 * Connecting to MASTER 172.16.10.129:6382
1510:S 16 Nov 10:40:01.748 * MASTER <-> SLAVE sync started
1510:S 16 Nov 10:40:01.748 * Non blocking connect for SYNC fired the event.
1510:S 16 Nov 10:40:01.748 * Master replied to PING, replication can continue...
1510:S 16 Nov 10:40:01.748 * Trying a partial resynchronization (request 91b8764503bafef32afb4eb4b67758cae97a3ec6:31801434).
1510:S 16 Nov 10:40:01.748 * Successful partial resynchronization with master.
1510:S 16 Nov 10:40:01.748 # Master replication ID changed to 336ec72e013cec22ea7a9c5b08ea7e476abd8aae
1510:S 16 Nov 10:40:01.748 * MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.
  • 在分析三個sentinel節點日誌之前先對比三個sentinel節點客觀下線的時間

    • sentinel-1 10:40:00.527 # +odown master mymaster 172.16.10.129 6381 #quorum 2/2
    • sentinel-2 10:40:00.628 # +odown master mymaster 172.16.10.129 6381 #quorum 3/2
    • sentinel-3 沒有發現它的節點客觀下線時間
  • 對比可知,sentinel-1最先完成客觀下線,最先向其他節點發起向我投票的命令,因此sentinel-1節點被選為領導者

(4)26381 sentinel-1節點日誌

#10:40:00對6381節點作了主觀下線(+sdown),注意這個時間正好是kill-9後的30秒,和down-after-milliseconds的配置是一致的。且最先達到了客觀下線的條件:
1516:X 16 Nov 10:40:00.450 # +sdown master mymaster 172.16.10.129 6381
1516:X 16 Nov 10:40:00.527 # +odown master mymaster 172.16.10.129 6381 #quorum 2/2
1516:X 16 Nov 10:40:00.528 # +new-epoch 4

#sentinel-1節點被選為領導者:
1516:X 16 Nov 10:40:00.528 # +try-failover master mymaster 172.16.10.129 6381
1516:X 16 Nov 10:40:00.562 # +vote-for-leader 18bd11bf3e8bc33b08b9beaddac3fc29e95a4138 4
1516:X 16 Nov 10:40:00.602 # 668aaa2b0503f7b687cee1ff75aca88d472e2644 voted for 18bd11bf3e8bc33b08b9beaddac3fc29e95a4138 4
1516:X 16 Nov 10:40:00.607 # 85a5f42150ddf1972e94919f72089e8fa0bb8cb7 voted for 18bd11bf3e8bc33b08b9beaddac3fc29e95a4138 4
1516:X 16 Nov 10:40:00.639 # +elected-leader master mymaster 172.16.10.129 6381

#尋找合適的從節點作為新的主節點:
1516:X 16 Nov 10:40:00.639 # +failover-state-select-slave master mymaster 172.16.10.129 6381

#選出了合適的從節點(6382節點):
1516:X 16 Nov 10:40:00.705 # +selected-slave slave 172.16.10.129:6382 172.16.10.129 6382 @ mymaster 172.16.10.129 6381

#命令6382節點執行slaveof no one,使其成為主節點:
1516:X 16 Nov 10:40:00.705 * +failover-state-send-slaveof-noone slave 172.16.10.129:6382 172.16.10.129 6382 @ mymaster 172.16.10.129 6381

#等待6382節點晉升為主節點:
1516:X 16 Nov 10:40:00.776 * +failover-state-wait-promotion slave 172.16.10.129:6382 172.16.10.129 6382 @ mymaster 172.16.10.129 6381

#確認6382節點已經晉升為主節點:
1516:X 16 Nov 10:40:01.338 # +promoted-slave slave 172.16.10.129:6382 172.16.10.129 6382 @ mymaster 172.16.10.129 6381

#故障轉移進入重新配置從節點階段:
1516:X 16 Nov 10:40:01.338 # +failover-state-reconf-slaves master mymaster 172.16.10.129 638