《Redis 設計與實現》讀書筆記(三)


多機數據庫實現

十五 、復制

從服務器通過命令 slaveof 127.0.0.1 6000 成為主服務器的從服務器。然后執行復制操作,保持自己的狀態和主服務器一樣

1.理論

同步

成為從服務器后的同步操作:

  1. 從服務器會發送SYNC命令給主服務器,
  2. 主機會執行bgsave命令,並記錄當前的偏移量。
  3. bgsave命令執行期間執行的寫命令,都會記錄到緩沖區
  4. bgsave命令執行成功后,主機發送RDB文件給從機
  5. 從機加載RDB文件
  6. 主機發送緩沖區的命令給從機
  7. 從機執行緩沖區命令

命令傳播

當主從機的狀態一致后
主機每次執行寫命令,都會通過命令傳播的方式,發送給從機
從機執行寫命令,這樣主從的狀態又會一致了。

2.8版本前的缺陷

如果主從之間網絡斷開,這樣主機的寫命令就不能通過命令傳播發給從機了,這時候主從就不一致了。
當主從重新連接上后,在2.8版本前的做法是重新執行一次同步操作。
如果主從斷開前執行了很多命令,斷開期間期間主機只執行了幾條寫命令,重新執行一次同步操作,效率會比較慢。更好的做法是只同步斷開期間執行的命令給從機就好了。所以為了優化這個缺陷,2.8后新出了PSYNC命令

新版命令

新版增加了PSYNC命令,這個命令支持完整重同步部分重同步
簡單來講就是重連后,主機會判斷當前能不能執行部分重同步,如果可以就做,如果不可以,就執行完整重同步。

其他知識

  • 復制偏移量(offset)。
    • 主機和從機都會保存復制偏移量,這個是當前執行過的所有命令的字節數。例如set key value命令的字節數是33。(這個不是簡單的把命令轉成字符串的,有一定的算法,算法應該和RDB文件的算法一樣的。總之就是把一條命令多個參數轉成一個字符串。例如 set test 3 命令就占44個字節)
    • 當執行一條新命令,例如偏移量是100,。主機執行完后,就會把自己的偏移量加100
    • 主機命令傳播給從機后,從機執行完,也把自己的偏移量加100
    • 這個的作用就是識別主從之間是否一致以及不一致的程度有多少
  • 復制積壓緩沖區 (repl_backlog)
    • 這個緩沖區和同步的時候的緩沖區不一樣
    • 主機每次執行寫命令,就把命令轉換成的字符串,存入這個緩沖區
    • 緩沖區采用固定長度,先進先出的隊列。
    • 默認緩沖區的大小是1M。通過info replication命令可以查看緩沖區信息,repl_backlog_size:1048576
    • 緩沖區每一個字節,都有自己的偏移量號碼對應上面的復制偏移量。
  • 服務器運行ID(run id)
    • 每個redis節點都有自己的運行ID。是40個隨機的十六進制字符組成。
    • 主從關系建立后,從機會記錄主機的ID
    • 每次從機執行PSYNC都要把主機的ID傳輸過去,如果主機ID變更,只能使用完整重同步。

info replication

127.0.0.1:6811> info replication
# Replication
role:master #當前節點的角色
connected_slaves:1  #從機數量
slave0:ip=127.0.0.1,port=6801,state=online,offset=2095671,lag=0 #從機1信息
master_repl_offset:2095671  #主機的offset
repl_backlog_active:1  #緩沖區是否可用
repl_backlog_size:1048576 #緩存的大小,默認是1M
repl_backlog_first_byte_offset:1047096 #緩沖區第一個字節的offset
repl_backlog_histlen:1048576 #

在主機執行這個命令,可以查看主從復制的情況,包括有多少個從機,偏移量,緩沖區大小等。

2.過程

PSYNC命令實現

  • PSYNC的調用方法有兩種

    1. 從機之前沒有成為別人的從機,也就是第一次成為從機。會發送PSYNC ? -1命令。這時候肯定會執行完整重同步
    2. 從機之前成為過別人的從機。會發送命令PSYNC runid是之前的主機的ID,offset是從機當前的offset。
      • 主機收到命令后會判斷runid是否和自己的一樣,如果不一樣,就執行完整重同步
      • 如果一樣,判斷offset是否小於自己的repl_backlog_first_byte_offset,也就是從機缺失的寫命令是否還在緩沖區內
        • 如果不在,就執行完整重同步
        • 如果再,就執行部分重同步
      • 所以,只有當runid沒有變更,而且offset小於repl_backlog_first_byte_offset,才會執行部分重同步,否則執行完成重同步
  • 如果可以執行部分重同步,主機會返回+CONTINUE命令,然后發送缺失的寫命令給從機

  • 如果需要執行完整重同步,主機會返回+FULLRESYNC命令,然后后面的步驟和同步一樣。

主從同步完整流程

  • slaveof命令
    • 執行完slaveof命令后,從機會把主機的ip和端口存在redisServer結構體里面,然后就返回ok了
    • 返回ok后才會執行同步操作,所以是異步的。
  • 從機與主機建立socket連接。這時候從機相當於主機的客戶端
  • 從機發送ping命令給主機,主機如果正常返回pong命令。如果主機超時不返回或者返回錯誤。從機斷開連接重試。
  • 身份驗證,如果需要從機需要發送auth 密碼命令
  • 從機發送端口信息給主機。也就是從機節點的端口。
  • 從機發送PSYNC命令
  • 主機判斷執行那種同步,不管是那種同步,主機都會成為從機的客戶端,也就是連接從機的端口。
    • 如果是完整重同步,主機記錄當前offset,執行bgsave,發送RDB文件給從機,發送offset后面的寫命令給從機。
    • 如果是部分重同步,主機發送從機的offset之后的寫命令給從機
  • 從機執行寫命令,主從狀態達到一致
  • 然后進入命令傳播階段,主機執行的所有寫命令,都發送給從機,從機執行后,主從狀態達到一致。

心跳

從機每隔一秒會向主機發送心跳命令 REPLCONF ACK <replication_offset>
心跳可以實現功能:

  • 檢測主從之間的網絡狀態

  • 輔助實現min-slaves

  • 檢測命令丟失

  • 檢測主從之間的網絡狀態

    • 如果主機超過1秒沒有收到從機的ack命令,就表名從機網絡出現了故障
    • info replication命令可以看到從機上一次ack距離現在的時間,就是lag參數,一般在0-1之間,超過就是有故障了
  • 輔助實現min-slaves

    • min-slaves選項是指在從機數小於min-slaves-to-write,而且全部從機的lag值大於min-slaves-max-lag秒時,主機拒絕執行寫命令。
    • 這個功能主要是防止主機的主從復制處於不安全狀態
  • 檢測命令丟失

    • 假如主機的寫命令沒有成功傳輸給從機,例如網絡丟失了。這時候從機的offset就會小於主機。通過心跳,主機會發現從機的offset不等於自己,就會補發對應的寫命令給從機。
      • 從機通過offset可以避免重復執行相同offset的命令
      • 命令補發這種情況較為容易觸發。
        • 例如主機剛執行一條新命令,也把命令傳播出去了,但是從機還沒有收到,然后心跳過來了,這時候從機offset肯定會小於主機offset。
        • 所以不知道Redis有沒有機制可以避免這種情況。例如兩次心跳都一樣而且offset小於自己,才觸發命令補發。

十六、哨兵

哨兵是Redis高可用的一種方案。Redis的架構是一主多從,然后有一個或者多個哨兵進程去監聽主服務器的情況。當哨兵認為主服務器已經下線,提升其中一個從服務器為主服務器,然后修改其他從服務器的復制配置。
哨兵的作用類似Mysql的MHA,只是哨兵支持多個,MHA只有一個manager。

1.初始化哨兵Sentinel

哨兵也是一個Redis進程,啟動方式是redis-sentinel /config.conf
哨兵進程只能執行哨兵相關的命令,不能執行其他的Redis命令。

數據結構

  • sentinelState 哨兵狀態結構

    • uint64_t current_epoch 當前紀元,用於選取領頭羊哨兵
    • dict *masters 監視的主服務器信息,一個哨兵集群可以監視多個主服務器。key是主服務器的名字,例如127.0.0.1::6479 value是sentinelRedisInstance結構
    • tilt 是否進入tilt模式
  • sentinelRedisInstance 哨兵實例結構

    • flags 標志值,表示實例當前的狀態,可取值:主服務器,從服務器,主觀下線,客觀下線
    • char *name 名字 例如127.0.0.1:6379
    • char *runid 運行ID
    • uint64_t config_epoch 配置紀元
    • *addr 地址包括ip和端口
    • down_after_period 實例無響應多久判斷為主觀下線
    • quorum 判斷為客觀下線所需的投票數
    • dict slaves 這個主服務器下面的所有從服務器,結構和masters結構一樣。
    • dict sentinels 監視這個主服務器的其他哨兵,不包含哨兵自己

哨兵配置

port 6711


#監聽的存儲redis,TestMaster1是redis名稱,127.0.0.1是ip,6702 是端口,1是升級為Master的權重


sentinel monitor mymaster 127.0.0.1 6721 1
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
daemonize yes
#指定工作目錄
dir "/data/redis_demo"


logfile "/data/redis_demo/log/sentinel.log"
#redis主節點密碼
sentinel auth-pass mymaster 123456
  • mymaster是主服務器的名字
  • 后面是ip和端口 1是quorum
  • down-after-milliseconds 實例無響應多久判斷為主觀下線

哨兵啟動后,初始化后,就會和主服務器建立連接,有兩個:

  • 命令連接。也就是哨兵充當主服務器的客戶端。用於向客戶端發送PING,發送訂閱等命令
  • 訂閱連接,會訂閱頻道__sentinel__:hello,用於接收訂閱消息。

2.獲取主從服務器信息

建立連接后,哨兵會每10秒向主服務器發送INFO命令。INFO命令會返回主服務器的所有從服務器信息。這樣哨兵就能知道主服務器有多少從服務器了。
然后會新建或者更新主服務器的slaves結構
slaves結構的key是從服務器的ip和端口,例如127.0.0.1:7000,value是sentinelRedisInstance數據結構。

當有新的從服務器,哨兵會像和主服務器建立的連接一樣,和從服務器也建立兩個連接。

然后會每10向從服務器發送INFO命令。

3.獲取其他哨兵信息

哨兵會每個2秒向主和從服務器發送訂閱消息,頻道是__sentinel__:hello,消息是:PUBLISH __sentinel__:hello "s_ip,s_port,s_runid,s_epoch,m_name,m_ip,m_port,m_epoch"

  • s開頭的是哨兵自己的信息
  • m開頭的是主服務器的信息

假如哨兵1發送了這個消息,因為其他哨兵,例如2和3,都會訂閱這個頻道,所以它們也能收到這個消息,哨兵1自己也會收到。
所以當它們收到這個信息后:

  • 如果run_id是自己,不處理
  • 如果run_id不是自己,
    • 更新或者新建其他哨兵的數據結構。更新master的sentinels結構,key是哨兵2和3的ip端口,例如127.0.0.1::8000 value是sentinelRedisInstance結構。
    • 和其他哨兵建立連接,只會建立命令連接,不會建立訂閱連接。

4.判斷主觀下線

哨兵會每1秒向其他哨兵和主從服務器發送PING命令。其他服務器會返回:

  • PONG,LOADING MASTERDOWN3種回復
  • 除此之外的其他回復或者超時不回復稱為無效回復。

當在down-after-milliseconds時間內,例如是5s,對方連續返回無效回復,例如是5次PING都返回無效回復,哨兵就會把這個服務標記為主觀下線,就是把flags值修改為SRI_S_DOWN。

5.判斷客觀下線

當哨兵判斷一個主服務器主觀下線后(從服務器不會觸發),會向其他哨兵發送命令:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

分別為主服務器的ip 端口,自己的配置紀元,runid=*號。

其他哨兵接收這個命令后,會返回

  • down_state 下線狀態1=下線,0=未下線
  • leader_runid 選舉的leader的run_id,
  • leader_epoch 選舉的leader的配置紀元

上面的 current_epoch run_id leader_runid leader_epoch都是用於選舉領頭羊哨兵的,在判斷客觀下線中沒有用。

所以總的來說,哨兵判斷一個主服務器下線后,會詢問其他哨兵,是否也把這個服務器標記為下線,如果有大於等於quorum參數的哨兵投票說主服務器已下線,哨兵會把主服務器標記為客觀下線,也就是把flags標記為SRI_O_DOWN

6.選舉領頭羊leader

當一個哨兵把主服務器標記為客觀下線后,就會進入選領頭羊leader環節,在多個哨兵中選擇一個領頭羊哨兵,來執行故障轉移操作。

  • 哨兵1判斷主服務器為客觀下線后,向所有其他哨兵發送上面的is-master-down-by-addr命令,current_epoch設置為自己的配置紀元,runid是自己的runid
  • 哨兵2收到這條命令后,如果在自己的配置紀元沒有選過領頭羊,就會返回leader_runid=哨兵1的runid,leader_epoch=哨兵1的配置紀元。如果已經選過領頭羊,就會返回選中的領頭羊信息
  • 如果超過總哨兵的半數都投票給哨兵1,哨兵1就會成為領頭羊

配置紀元問題:

  • 全部哨兵的配置紀元是否需要相同,如果相同,怎么同步?
  • 如果不相同,怎么判斷這個配置單元中有沒有選過其他人

解答

  • 在哨兵A認識其他哨兵的時候,會傳送自己的配置紀元給對方
  • 一開始所有的哨兵的配置紀元都是0
  • 當哨兵看到對方的配置紀元比自己大,就會更新自己的配置紀元為對方的配置紀元
  • 這樣當所有哨兵都認識后,所有哨兵的配置紀元都會統一,也就是所有哨兵中最大的那個,例如A是1,B是2,C是3,最后ABC的配置紀元都會設置為3
  • 當哨兵A發起投票的時候,它會先把自己的配置紀元+1,例如變為4,然后要求BC投票。然后計時(例如等待5s)。
  • 當B收到A的投票要求,如果B的配置紀元比自己的大(例如B現在是3),就會認為4是沒有投票的配置紀元,就把票投給A,然后設置自己的配置紀元為4.
  • 當B收到C的投票要求,發現自己的配置紀元等於C的配置紀元(例如都是4),因為在配置紀元=4時,B已經把票投給A了。所以B不能投票給C,它會返回A的runid和A的配置紀元
  • A計時結束后(也就是5s后),如果A只收到B的票,但是沒有收到C的票(可能C把票投給B了),所以成為領頭羊失敗。這時A會把配置紀元再+1=5,然后再次要求BC投票,然后再計時

異常情況

  • 如果A的配置紀元是5,C是4,B是3
  • C先發起投票請求,B會投票給C,但是A不會,因為C的紀元比自己小
  • A發起投票請求,B會投票給A,C也會投票給A
  • 所以最終A和C都認為自己成為了領頭羊。
  • 可能的解決方法:
    • 方法1:
      • C收到A的返回中會標明A投票給了A,紀元是5
      • C發現A的紀元比自己的紀元大,所以應該停止成為領頭羊
    • 方法2:
      • C和A成為領頭羊后,向所有節點群發自己成為領頭羊的消息,以及自己的紀元
      • 當C發現A成為領頭羊,而且紀元比自己大,就自動放棄領頭羊

Raft算法視頻
配置紀元
Raft算法

7.故障轉移

成為領頭羊leader的哨兵將執行主服務器的故障轉移工作

  • 從從服務器中選一個成為主服務器
    • 優先選擇近期ping后有回應的服務器
    • 優先選擇數據較新的從節點
  • 對新的主服務器執行slaveof no one命令 讓它成為主服務器
  • 每2秒對新服務器執行INFO命令,查看role是否從slave更新為master
  • 如果成為master,對其他從服務器執行slaveof操作,讓它們從新的主服務器復制數據
  • 把舊的主服務器記錄下來,等下次它上線,執行slaveof命令,讓它從新的主服務器復制

十七、集群

集群通過分片(sharding)來進行數據共享,並提供復制和故障轉移功能。

1.節點

一個集群由多個節點組成,一開始這些節點是互相不能感知的。
我們需要通過命令cluster meet <ip> <port>,讓節點加入集群。例如在A節點執行meet命令,ip和port是B節點的,這樣A和B節點就相互感知了。
通過 cluster nodes命令可以查看當前集群的情況。

127.0.0.1:6812> cluster nodes
1fdfb5833caf8e9cf3b7f1233ce3969e0a324db7 127.0.0.1:6804 master - 0 1572954527331 12 connected 0-1104 5461-5779 11423-12004
72234454d061c86c630e8eb7995e2480fe340b95 127.0.0.1:6803 master - 0 1572954527331 8 connected 12005-16383
  • 分別是 節點ID,IP 端口,角色

啟動

節點需要配置 cluster-enabled yes 才會開啟集群模式。
集群模式的節點啟動后,其他都和單機節點一樣的,只會在serverCron函數中增加一個clusterCron函數的調用

集群數據結構

集群增加了3種數據結構

  • clusterNode 集群節點信息,有字段
    • mstime_t ctime 創建時間
    • char name 節點名,也叫節點ID
    • int flags 存儲節點的角色(master還是slave)和集群狀態(在線或者下線)
    • uint_64_t configEpoch 配置紀元 用於故障轉移
    • char ip 節點IP地址
    • int port 節點端口
    • clusterLink link 和其他節點的連接
  • clusterLink 和其他節點的連接,和redisClient結構很像,有字段:
    • mstime_t ctime 創建時間
    • int fd 套接字描述符
    • sds sndbuf 待發送緩沖區
    • sds rcvbuf 已接收緩沖區
    • clusterNode node 這個連接對應的節點信息
  • clusterState 集群狀態,有字段:
    • clusterNode myself指向自己的clusterNode結構
    • uint64_t currentEpoch 配置紀元,用於故障轉移
    • int state 集群狀態 上線還是下線
    • int size 集群中至少處理着一個槽的節點數量。
    • dict *nodes 集群中所有的節點,key是節點名,value是clusterNode對象,也包括節點自己的node實例。

cluster meet命令

  • 客戶端向節點A發送Meet命令
  • 節點A創建節點B的ClusterNode對象
  • A節點發送Meet命令給節點B
  • 節點B創建節點A的ClusterNode對象
  • 返回Pong命令
  • 節點A收到Pong命令
  • 節點A返回Ping命令給節點B
  • 節點B收到Ping命令
  • 握手完成

然后節點A和B通過Gossip協議,然自己一直的節點認識彼此。

2.槽指派

Redis集群有16384個槽。數據庫中每個鍵都對應這些槽中的一個。每個節點處理0-16384個槽。
只有當全部槽都有節點處理,集群才會進入上線狀態。

槽指派命令

cluster addslots 0 1 2

這里把槽 0 1 2 3個槽指派給當前連接的節點。

槽的數據結構

槽的信息存儲在clusterNode結構的unsigned char slots[16384/8]。這是一個二進制字符串列表,只有0 1。 如果是1表示這個下標的槽由當前節點處理。還要個numslots記錄處理的槽的總數。
在clusterState結構有個 clusterNode *slots[16384]變量用來存儲每個槽對應的節點對象。
這樣就能實現通過O1復雜度可以

  • 查找自己是否負責某個槽
  • 某個槽是哪個節點在處理,還是沒有節點在處理

執行cluster addslots命令后,當前節點會把自己負責的槽都同步給其他節點。

當機器所有槽都有節點處理,機器就會進入上線狀態

集群中執行命令

  • 客戶端發送命令給其中一個節點
  • 計算這個key對應的槽,使用CRC16 校驗和算法
  • 槽是否有當前節點處理。檢查clusterState.slots[i]是否指向clusterState.self,如果是就是自己處理。
    • 是,執行命令
    • 否,查看槽在哪個節點負責,返回MOVED錯誤給客戶端MOVED 10000 127.0.0.1:6801分別是槽號,處理該槽的IP和port
  • 客戶端收到MOVED錯誤,連接到對應的節點,重試

通過命令cluster keyslot test 可以查看test這個key屬於哪個槽.
如果使用-c集群模式啟動客戶端,MOVED命令會被隱藏。否則會拋出。

數據庫實現

集群模式,的數據庫實現和單機模式差不多,不同點:

  • 集群模式只有一個數據庫,就是0
  • clusterState對象有個變量是 zskiplist *slots_to_keys是個跳躍表對象,保存當前數據庫的所有key,以及key的slot,slot是分數的形式。
    • 保存這個信息的好處是
      • 可以快速執行 cluster getkeysinslot <slot> <count> 用於返回指定槽的N個key。這個命令主要用於重新分片

4.重新分片

重新分片就是把N個槽從節點A遷移到節點B。重新分片過程中,集群是一直在線狀態的。

重新分片工作一般是使用管理軟件redis-trib負責的
步驟是

  1. 對目標節點發送cluster setslot <slot> IMPORTING <source_id>命令,讓目標節點做好導入槽的准備
  2. 對源節點發送cluster setslot <slot> MIGRATING <target_id>命令,讓源節點做好導出槽的准備。
  3. 對源節點發送cluster getkeysinslot <slot> <count>命令,獲取count個屬於槽slot的key
  4. 對源節點發送 migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,將對應的key遷移到目標節點。一條命令只能遷移一個key。

數據結構

  • IMPORTING命令
    • 當目標節點接收IMPORTING命令后,會查看clusterState對象的clusterNode *importing_slots_from[16384]變量對應的slot是否指向NULL,如果否,證明節點正在導入這個slot。如果是,將slot執行source_id對應的clusterNode對象
  • MIGRATING命令
    • 當元節點接收MIGRATING命令后,會查看clusterState對象的clusterNode *migrating_slots_to[16384]變量的對應的slot是否執行NULL,如果否,證明節點正在導出這個slot。如果是,將slot執行target_id對應的clusterNode對象

客戶端請求

因為遷移的過程,機器是一直上線的,所以就會存在問題:遷移過程中,如果客戶端操作遷移中的key,怎么辦。解決方法就是引入ASK錯誤。

在遷移的過程中,遷移的slot依然由源節點負責,所以對這個slot的key的操作依然是對源節點發送命令的。

  • 客戶端發送命令給源節點
  • 源節點查看key是否在數據庫中。
  • 如果是,執行命令
  • 如果否
    • 判斷key對應的槽i是否在遷移。查看migrating_slots_to[i]是否指向clusterNOde對象。
      • 如果是,有可能在目標節點,返回ASK錯誤ASK 10000 127.0.0.1:6801分別是槽號,處理該槽的IP和port
      • 如果否,返回key不存在
  • 客戶端收到ASK命令后,連接到對應的節點
  • 執行命令REDIS_ASKING打開標識
  • 執行命令
  • 目標節點收到命令后
  • 查看slot是否由自己負責
    • 如果是,執行命令
    • 如果否,查看slot是否正在導入查看importing_slots_from[i]是否指向clusterNOde對象。
      • 如果是,判斷客戶端是否帶ASKING標識。
        • 如果是,執行命令
        • 如果否,返回MOVED命令
      • 如果否,返回MOVED命令

ASK命令

  • ASK命令和MOVED命令一樣,也可能被隱藏。
  • 客戶端只有打開REDIS_ASKING標識,才能執行命令
  • 打開REDIS_ASKING表示只會對下一條命令生效
  • 下一條該slot的命令,還是會發給源節點

5.復制和故障轉移

集群里面有

  • 主節點,負責處理槽
  • 從節點,從主節點復制數據,但是不處理槽
    如果主節點故障,集群會自動把其中一個主節點的從節點提升為新主節點。之前復制舊主節點的從節點會重新復制新主節點

消息

集群的節點通過消息來進行交流。
發送消息的節點成為發送者
接收消息的節點成為接收者
消息有5種:

  • MEET消息。執行cluster meet命令后,發送的消息
  • PING消息。 集群內每個節點每隔一秒鍾,就會從集群里面隨機選出最多5個節點,然后選出最長時間沒有發送PING消息的節點,來發送PING消息。(也就是每一秒只會給一個節點發送PING消息)
    • 如果節點A最后一次接受節點B的PONG消息的時間距離現在超過了cluster-node-timeout配置的一半。節點A也會想節點B發送PING消息。
  • PONG消息。當接受者收到MEET消息或者PING消息,為了向發送者確認已收到這條消息,接受者會向發送者發送PONG消息。
    • 另外,節點可以通過向集群廣播PONG消息來讓別的節點刷新對該節點的認識
  • FAIL 消息 當一個主節點A判斷另一個節點B已經進入FAIL狀態時,就會廣播FAIL消息。接收到這個消息的節點,會立刻把節點B標志為下線
  • PUBLISH消息。當一個節點收到PUBLISH命令時,會執行這個命令,並向集群廣播PUBLISH消息
    MEET PING PONG3中消息稱為Gossip協議消息。
    一條消息由消息頭和正文組成。

消息頭
消息頭是一個結構,里面包含正文和其他屬性

  • uint32_t totlen。消息的長度,包含消息頭和正文
  • uint64_t type。消息類型
  • uint6_t count 消息正文包含的節點信息數量。只有在MEET PING PONG三種消息使用
  • uint64_t currentEpoch 發送者所處的配置紀元
  • uint64_t configEpoch 如果發送者是主節點,記錄主節點的配置紀元。如果是從節點,記錄正在復制的主節點的配置紀元
  • char sender[REDIS_CLUSTER_NAMELEN]。發送者名稱,也就是node id
  • unsigned char myslots[REDIS_CLUSTER_SLOTS/8] 。發送者目前的槽指派信息
  • char slaveof[REDIS_CLUSTER_NAMELEN] 如果是從節點,記錄主節點的名稱。
  • uint16_t port 發送者端口
  • uint16_t flags 發送者標識值
  • char state 集群狀態
  • union clusterMsgData data 正文。是個聯合對象。消息不同,這里的數據結構不一樣。

消息正文

  1. MEET PING PONG消息的實現
    1. 正文是兩個clusterMsgDataGossip結構的實例
    2. 因為MEET PING PONG3種消息的正文結構一樣,所以通過消息頭的type來判斷是哪種消息
    3. 發送者會從自己已知節點里面隨機找兩個節點(可以是主或者從)。然后把兩個節點的信息保存到兩個clusterMsgDataGossip結構里面,有數據
      1. char nodeName[REDIS_CLUSTER_NAMELEN] 節點名稱
      2. uint32_t ping_sent 最后一次向該節點發送PING的時間戳
      3. uint32_t pong_received 最后一次從該節點接受PONG消息的時間戳
      4. char ip[16] IP
      5. uint16_t port 該節點端口
      6. uint16_t flags 節點的標識值
    4. 接受者收到這三種消息后,會查看里面的兩個Gossip結構,也就是兩個其他節點的信息
      1. 如果接受者第一次接觸節點,就會向這個節點握手
      2. 如果接受者已接觸這個節點,就會更新節點信息
  2. FAIL消息的實現
    1. 消息使用clusterMsgDataFail結構,只有一個變量char nodename[REDIS_CLUSTER_NAMELEN]
    2. 當接受者收到這個消息,就會標識這個節點為下線狀態
  3. PUBLISH消息的實現
    1. PUBLISH命令有兩個參數,channel和msg,例如publish "channel1" "msg1"
    2. 消息使用clusterMsgDataPublish
      1. uint32_t channel_len channel的長度
      2. uint32_t message_len 消息的長度
      3. unsigned char bulk_data[8] 消息內容,不一定是8字節
      4. 例如上面的例子:bulk_data存儲的是channel1msg1。channel_len =8 message_len =4

設置從節點

cluster replicate <node_id>

通過這個命令,可以讓接收命令的節點成為node_id的從節點。
接收命令的節點會:

  • 修改clusterState.myself.slaveof的屬性,執行node_id對應的clusterNode對象
  • 修改clusterState.myself.flags的屬性,關閉REDIS_NODE_MASTER標志,打開REDIS_NODE_SLAVE標志
  • 調用復制代碼,從主節點復制數據。復制的邏輯和單機復制是一樣的,所以相當於執行命令slaveof <master_ip> <master_port>
  • 把消息發送給集群所有節點,讓所有節點都知道該節點成為node_id的主節點
  • 其他節點收到消息后
    • 修改主節點對應的clusterNode結構的slaves,這是一個custerNode列表,把從節點加入到列表后面
    • 修改主節點對應的clusterNode結構的numslaves,int類型,加一

故障檢測

集群內每個節點都會定期向其他節點發送PING消息,目標節點收到ping消息后,返回PONG消息。如果目標節點超時沒有返回,發送節點會在該節點的clusterNode結構里面修改flags屬性,打開REDIS_NODE_PFAIL標識,標識位疑似下線狀態。

例如節點A標記節點B為疑似下線。然后通過PING PONG命令,節點A會把這個信息同步給集群其他節點。
當節點C收到節點A認為節點B疑似下線。節點C會在節點B的clusterNode結構的fail_reports鏈表里面添加一個clusterNodeFailReport結構,有變量:

  • clusterNode *node 執行報告節點B疑似下線的節點。這里是節點A
  • time 收到下線報告的時間。

當集群里面半數以上負責槽的主節點都將某個節點標記為疑似下線,那么這個節點會被標記為下線,標記的節點會向集群廣播FAIL消息,通知其他節點。

例如這里的節點C,它收到了A節點的報告,同時如果他自己PING節點B也是失敗,而且集群里面只有ABC3個負責槽的主節點,那么節點C就會標記節點B位下線,並廣播FAIL消息。

故障轉移

當集群中其中一個主節點,例如節點B被標記為下線

  1. 那節點B的從節點,會有一個成為主節點,例如節點D
  2. 節點D會執行slaveof no one命令,成為新的主節點
  3. 節點D撤銷所有對節點B的槽指派,並將這些槽都指派給自己
  4. 節點D向集群廣播一條PONG消息。讓其他節點知道自己成為了主節點並接管了節點B的所有槽指派
  5. 節點D開始接受和處理客戶端的命令請求,轉移完成

選舉新節點

  1. 配置紀元是一個自增變量,初始值是0
  2. 當集群某個節點開始一次故障轉移時,配置紀元的值會加一
  3. 在一個配置紀元中,主節點只有一次投票機會。它會把票投給第一個要求它投票的節點
  4. 當從節點知道自己的主節點已下線后,會廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求其他主節點為它投票
  5. 如果主節點有投票權(它正在負責處理槽),並且沒有投過票給其他節點,那它會給第一個要求投票的節點返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
  6. 每個參選的從節點都會受到ACK消息,如果自己收到的ACK消息大於可投票的節點的半數,這個節點就會成為新的主節點。
  7. 如果在一個配置紀元里面,沒有從節點收到足夠多的票數(例如3個主節點,掛了一個,剩下2個,2個從節點各自收到一個投票)。那集群就會進入一個新的配置紀元。再次進行選舉。
    1. 有點不太明白。怎么進入新的紀元?誰來決定是否進入新的紀元?
    2. 選舉算法和哨兵的類似,也是Raft算法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM