多機數據庫實現
十五 、復制
從服務器通過命令 slaveof 127.0.0.1 6000 成為主服務器的從服務器。然后執行復制操作,保持自己的狀態和主服務器一樣
1.理論
同步
成為從服務器后的同步操作:
- 從服務器會發送SYNC命令給主服務器,
- 主機會執行bgsave命令,並記錄當前的偏移量。
- bgsave命令執行期間執行的寫命令,都會記錄到緩沖區
- bgsave命令執行成功后,主機發送RDB文件給從機
- 從機加載RDB文件
- 主機發送緩沖區的命令給從機
- 從機執行緩沖區命令
命令傳播
當主從機的狀態一致后
主機每次執行寫命令,都會通過命令傳播的方式,發送給從機
從機執行寫命令,這樣主從的狀態又會一致了。
2.8版本前的缺陷
如果主從之間網絡斷開,這樣主機的寫命令就不能通過命令傳播發給從機了,這時候主從就不一致了。
當主從重新連接上后,在2.8版本前的做法是重新執行一次同步操作。
如果主從斷開前執行了很多命令,斷開期間期間主機只執行了幾條寫命令,重新執行一次同步操作,效率會比較慢。更好的做法是只同步斷開期間執行的命令給從機就好了。所以為了優化這個缺陷,2.8后新出了PSYNC命令
新版命令
新版增加了PSYNC命令,這個命令支持完整重同步
和部分重同步
簡單來講就是重連后,主機會判斷當前能不能執行部分重同步,如果可以就做,如果不可以,就執行完整重同步。
其他知識
- 復制偏移量(offset)。
- 主機和從機都會保存復制偏移量,這個是當前執行過的所有命令的字節數。例如set key value命令的字節數是33。(這個不是簡單的把命令轉成字符串的,有一定的算法,算法應該和RDB文件的算法一樣的。總之就是把一條命令多個參數轉成一個字符串。例如
set test 3
命令就占44
個字節) - 當執行一條新命令,例如偏移量是100,。主機執行完后,就會把自己的偏移量加100
- 主機命令傳播給從機后,從機執行完,也把自己的偏移量加100
- 這個的作用就是識別主從之間是否一致以及不一致的程度有多少
- 主機和從機都會保存復制偏移量,這個是當前執行過的所有命令的字節數。例如set key value命令的字節數是33。(這個不是簡單的把命令轉成字符串的,有一定的算法,算法應該和RDB文件的算法一樣的。總之就是把一條命令多個參數轉成一個字符串。例如
- 復制積壓緩沖區 (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的調用方法有兩種
- 從機之前沒有成為別人的從機,也就是第一次成為從機。會發送PSYNC ? -1命令。這時候肯定會執行完整重同步
- 從機之前成為過別人的從機。會發送命令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小於自己,才觸發命令補發。
- 假如主機的寫命令沒有成功傳輸給從機,例如網絡丟失了。這時候從機的offset就會小於主機。通過心跳,主機會發現從機的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成為領頭羊,而且紀元比自己大,就自動放棄領頭羊
- 方法1:
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負責的
步驟是
- 對目標節點發送
cluster setslot <slot> IMPORTING <source_id>
命令,讓目標節點做好導入槽的准備 - 對源節點發送
cluster setslot <slot> MIGRATING <target_id>
命令,讓源節點做好導出槽的准備。 - 對源節點發送
cluster getkeysinslot <slot> <count>
命令,獲取count個屬於槽slot的key - 對源節點發送
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對象
- 當目標節點接收IMPORTING命令后,會查看clusterState對象的
- MIGRATING命令
- 當元節點接收MIGRATING命令后,會查看clusterState對象的
clusterNode *migrating_slots_to[16384]
變量的對應的slot是否執行NULL,如果否,證明節點正在導出這個slot。如果是,將slot執行target_id對應的clusterNode對象
- 當元節點接收MIGRATING命令后,會查看clusterState對象的
客戶端請求
因為遷移的過程,機器是一直上線的,所以就會存在問題:遷移過程中,如果客戶端操作遷移中的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錯誤
- 判斷key對應的槽i是否在遷移。查看migrating_slots_to[i]是否指向clusterNOde對象。
- 客戶端收到ASK命令后,連接到對應的節點
- 執行命令REDIS_ASKING打開標識
- 執行命令
- 目標節點收到命令后
- 查看slot是否由自己負責
- 如果是,執行命令
- 如果否,查看slot是否正在導入查看importing_slots_from[i]是否指向clusterNOde對象。
- 如果是,判斷客戶端是否帶ASKING標識。
- 如果是,執行命令
- 如果否,返回MOVED命令
- 如果否,返回MOVED命令
- 如果是,判斷客戶端是否帶ASKING標識。
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 正文。是個聯合對象。消息不同,這里的數據結構不一樣。
消息正文
- MEET PING PONG消息的實現
- 正文是兩個clusterMsgDataGossip結構的實例
- 因為MEET PING PONG3種消息的正文結構一樣,所以通過消息頭的type來判斷是哪種消息
- 發送者會從自己已知節點里面隨機找兩個節點(可以是主或者從)。然后把兩個節點的信息保存到兩個clusterMsgDataGossip結構里面,有數據
- char nodeName[REDIS_CLUSTER_NAMELEN] 節點名稱
- uint32_t ping_sent 最后一次向該節點發送PING的時間戳
- uint32_t pong_received 最后一次從該節點接受PONG消息的時間戳
- char ip[16] IP
- uint16_t port 該節點端口
- uint16_t flags 節點的標識值
- 接受者收到這三種消息后,會查看里面的兩個Gossip結構,也就是兩個其他節點的信息
- 如果接受者第一次接觸節點,就會向這個節點握手
- 如果接受者已接觸這個節點,就會更新節點信息
- FAIL消息的實現
- 消息使用clusterMsgDataFail結構,只有一個變量
char nodename[REDIS_CLUSTER_NAMELEN]
- 當接受者收到這個消息,就會標識這個節點為下線狀態
- 消息使用clusterMsgDataFail結構,只有一個變量
- PUBLISH消息的實現
- PUBLISH命令有兩個參數,channel和msg,例如publish "channel1" "msg1"
- 消息使用clusterMsgDataPublish
- uint32_t channel_len channel的長度
- uint32_t message_len 消息的長度
- unsigned char bulk_data[8] 消息內容,不一定是8字節
- 例如上面的例子: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被標記為下線
- 那節點B的從節點,會有一個成為主節點,例如節點D
- 節點D會執行slaveof no one命令,成為新的主節點
- 節點D撤銷所有對節點B的槽指派,並將這些槽都指派給自己
- 節點D向集群廣播一條PONG消息。讓其他節點知道自己成為了主節點並接管了節點B的所有槽指派
- 節點D開始接受和處理客戶端的命令請求,轉移完成
選舉新節點
- 配置紀元是一個自增變量,初始值是0
- 當集群某個節點開始一次故障轉移時,配置紀元的值會加一
- 在一個配置紀元中,主節點只有一次投票機會。它會把票投給第一個要求它投票的節點
- 當從節點知道自己的主節點已下線后,會廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求其他主節點為它投票
- 如果主節點有投票權(它正在負責處理槽),並且沒有投過票給其他節點,那它會給第一個要求投票的節點返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
- 每個參選的從節點都會受到ACK消息,如果自己收到的ACK消息大於可投票的節點的半數,這個節點就會成為新的主節點。
- 如果在一個配置紀元里面,沒有從節點收到足夠多的票數(例如3個主節點,掛了一個,剩下2個,2個從節點各自收到一個投票)。那集群就會進入一個新的配置紀元。再次進行選舉。
- 有點不太明白。怎么進入新的紀元?誰來決定是否進入新的紀元?
- 選舉算法和哨兵的類似,也是Raft算法