複製和故障轉移
Redis叢集中的節點分為主節點(master)和從節點(slave),其中主節點用於處理槽,而從節點則用於複製某個主節點,並在被複制 的主節點下線時,代替下線主節點繼續處理命令請求。
設定從節點:CLUSTER REPLICATE < node_id >可以讓接收命令的節點稱為node_id 所指定節點的從節點,並開始對主節點進行復制。
1)接收到該命令的節點首先會在自己的clusterState.nodes字典中找到node_id所對應節點的clusterNode結構,並將自己的clusterState.myself.slaveof指標指向這個結構,以此來記錄這個節點正在複製的主節點:
struct clusterNode{ //如果這個時一個從節點,那麼指向主節點 struct clusterNode *slaveof; }
2)節點修改自己的clusterState.myself.flags中的屬性,關閉原本的REDIS_NODE_MASTER標識,開啟REDIS_NODE_SLAVE標識,標識這個節點已經由原來的主節點變成了從節點。
3)節點會呼叫複製程式碼,根據clusterState.myself.slaveof指向clusterNode結構所儲存的IP地址和埠號,對節點進行復制。
一個節點稱為從節點,並開始複製某個主節點這一資訊會通過訊息傳送給叢集中的其他節點,最終叢集中的所有節點都會知道某個從節點正在複製某個主節點。
叢集中的所有節點都會在代表主節點的clusterNode結構的slaves屬性和numslaves屬性中記錄正在複製這個主節點的從節點名單:
struct clusterNode{ //正在複製這個主節點的從節點數量 int numslaves; //陣列,每個陣列項指向一個正在複製這個主節點的從節點的clusterNode struct clusterNode **slaves; }
叢集中的每個節點都會定期地向叢集中的其他節點傳送PING訊息,一次來檢測對方是否線上,如果接收PING訊息的節點沒有在規定的時間內,向傳送PING訊息的節點返回PONG訊息,那麼傳送PING訊息的節點就會將階段後PING訊息的節點標記為疑似下線(PFAIL)。
叢集中的各個節點會通過相互傳送訊息的方式來交換叢集中各個節點的狀態資訊:某個節點處於線上狀態、疑似下線、已下線狀態。
當一個主節點A通過罅隙得知主節點B認為主節點C進入疑似下線狀態時,主節點A會在自己的clusterState.nodes字典中找到主節點C所對應的clusterNode結構,並將主節點B的下線報告新增到clusterNode結構的fail_reports連結串列中
status clusterNode{ list *fali_reports;//連結串列,記錄所有其他節點對該節點的下線報告 };
下線報告結構:
struct c;isterNodeFailReport{ //報告目標節點已經下線的節點 struct clusterNode *node; //最後一個從node節點收到下線報告的時間(程式使用這個時間戳來檢查下線報告是否過期) mstime_t time; } typedef clusterNodeFailReport;
如果叢集裡半數以上負責處理槽的主節點都將某個主節點x報告未疑似下線,那麼這個主節點x將被標記未已下線,將主節點x標記為已下線的節點會向叢集廣播一條關於主節點x的FAIL罅隙,所有收到這條罅隙的節點都會立即將主節點x標記為已下線。
故障轉移的步驟:
1)複製下線主節點的所有從節點裡面,會有一個從節點被選中,
2)被選中的從節點會執行SLAVEOF no one命令,成為新的主節點。
3)新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽指派給自己。
4)新的主節點向叢集廣播一條PONG訊息,這條訊息讓其他叢集中的其他節點立即知道這個節點已經由從節點變為主節點,並且這個主節點已經接管了原本已下線節點負責處理的槽。
5)新的主節點開始接收和自己負責處理的槽有關的命令請求,故障轉移完成。
選舉新的主節點:
1)叢集的配置紀元是一個計數器。他的初始值為0;
2)當叢集中的某個節點開始一次故障轉移操作時,叢集配置紀元的值會被加1。
3)叢集裡面每個負責處理槽的主節點都有一次投票機會,而第一個向主節點要求投票的從節點將獲得主節點的投票。
4)當從節點發現自己正在複製的主節點進入下線狀態時,從節點會向叢集官博一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST訊息,要求所有收到這個訊息、並且具有投票權的主節點向這個從節點投票。
5)如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,表示這個主節點支援從節點成為新的主節點。
6)每個參與選舉的從節點都會收到CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,並根據自己收到了多少條這種訊息來統計自己獲得了多少主節點支援。
7)如果叢集中有N個具有投票權的主節點,那麼當一個從節點大於等於N/2+1張支援票時,這個從節點就當選成為新的主節點。
8)如果在一個配置紀元裡面沒有從節點收集到足夠多的支援票,那麼叢集進入下一個紀元,再次進行選舉,直到選出新的主節點為止。
訊息
叢集中各個節點通過傳送和接收訊息來進行通訊,我們稱傳送訊息的節點為傳送者,接收訊息的節點為接收者:
1)MEET訊息,當傳送者接到客戶端傳送的CLUSTER MEET命令時,傳送者會向接收者傳送MEET訊息,請求接收者加入到傳送者當前所處的叢集裡面。
2)PING訊息,叢集裡面的每個節點預設每隔一秒鐘就會從已知節點列表中隨機選出五個節點,然後對這五個節點中最長時間沒有傳送過PING訊息的節點傳送PING訊息,以此檢測被選中的節點是否線上。除此之外,如果節點A最後一次收到節點B傳送的PONG訊息的時間,距離當前時間已超過了節點A的cluster-node-timeout選項設定時長的一半,那麼節點A也會向節點B傳送PING訊息,這可以防止節點A因長時間沒有隨機選中節點B作為PING訊息的傳送物件而導致節點B的資訊更新滯後。
3)PONG訊息,當接收者收到傳送者發來的MEET訊息或者PING時,為了向傳送者確認這條MEET、PING訊息已到達,接收者會向傳送者返回一條PONG訊息。另外,一個節點也可以通過向叢集傳送叢集廣播自己的PONG訊息來讓叢集中的其他節點立即重新整理關於這個節點的認識。
4)FAIL訊息,當一個主節點A判斷另一個主節點B已經進入FAIL狀態時,節點A會會向叢集廣播一條關於節點B的FAIL訊息,所有接收到這條訊息的節點都會立即將節點B標記為已下線。
5)PUBLISH訊息,當節點接收到一個PUBLISH命令時,節點會執行這個命令,並向叢集廣播一條PUBLISH訊息,所有接收到這條PUBLISH訊息的節點都會執行相同的PUBLISH命令。
一條訊息由訊息頭(header)和訊息正文(data組成)
訊息頭:
typedef struct { //訊息的長度(訊息頭的長度和訊息正文的長度) uint32_t totlen; //訊息的型別 uint16_t type; //訊息正文包含的節點資訊數量 //只有傳送MEET、PING、PONG這三種Gossip協議訊息時使用 uint16_t count; //薩鬆這所處的配置紀元 uint64_t currentEpoch; //如果傳送者是一個主節點,那麼這裡面記錄的時傳送者的配置紀元 //如果傳送者時一個從節點,那麼這裡面記錄的時傳送者正在複製的主節點的配置紀元 uint64_t configEpoch; //傳送者的名稱(ID) char sender[REDIS_CLUSTER_NAMELEN]; //傳送者目前的槽指派資訊 unsigned char myslots[REDIS_CLUSTER_SLOTS/8]; //如果傳送者是一個從節點,記錄的是傳送者正在複製的主節點的名稱 //如果傳送者是一個主節點,那麼這裡記錄的是REDIS_NODE_NULL_NAME char slaveof[REDIS_CLUSTER_NAMELEN]; //傳送者的埠號 uint16_t port; //傳送者的標識值 uint16_t flags; //傳送者所處叢集的狀態 unsigned char state; //訊息正文 union clusterMsgData data; } clusterMsg;
clusterMsg.data 結構:
union clusterMsgData{ //MEET PING PONG 訊息正文 struct{ //每條MEET PING PONG訊息都包含兩個 clusterMsgDataGossip 結構 clusterMsgDataGossip gossip[1] } ping; //FAIL 訊息正文 struct{ clusterMsgDataFail about; } fali; //PUBLISH訊息正文 struct{ clusterMsgDataPublish msg; } publish; }
clusterMsgDataGossip結構記錄了選中節點的名字,傳送者與被選中節點最後一次傳送和接收PING訊息和PONG訊息的時間戳,被選中節點的IP地址和埠號,以及被選中節點的標識值:
typedef struct { //節點的名字 char nodename[REDIS_CLUSTER_NAMELEN]; //最後一次向該節點傳送PING訊息的時間戳 uint32_t ping_sent; //最後一次從該 節點接收到PONG訊息的時間戳 uint32_t pong_received; //節點的IP地址 char ip[16]; //節點的埠號 uint16_t port; //節點的標識值 uint16_t flags; } clusterMsgDataGossip;
每天學一點,總會有收穫。
說明:尊重作者智慧財產權,文中內容參考《Redis設計與實現》,僅在此做學習與大家分享。