1. 程式人生 > >Redis原始碼閱讀(六)叢集-故障遷移(下)

Redis原始碼閱讀(六)叢集-故障遷移(下)

Redis原始碼閱讀(六)叢集-故障遷移(下)

  最近私人的事情比較多,沒有抽出時間來整理部落格。書接上文,上一篇裡總結了Redis故障遷移的幾個關鍵點,以及Redis中故障檢測的實現。本篇主要介紹叢集檢測到某主節點下線後,是如何選舉新的主節點的。注意到Redis叢集是無中心的,那麼使用分散式一致性的演算法來使叢集中各節點能對在新主節點的選舉上達成共識就是一個比較可行的方案。

  在工程上,Raft一致性演算法是比較易於實現和理解的分散式一致性演算法;Redis也是使用了Raft來做主節點選舉的。所以這裡先簡單介紹下Raft的原理:

一、Raft演算法

  Raft為解決一致性問題, 引入了Leader中心。簡單來說就是通過選舉出一個單Leader,讓Leader來接收來自客戶端的所有資料,Leader會將資料分發給所有節點,當Leader判斷出資料已經分發到系統中大半節點以後,即認為該資料已經可以在系統中持久化儲存,並通知客戶端該資料提交成功,後續系統中資料的一致性可以讓分散式系統內部通過資料同步的方式實現。(注意到這裡和Redis去中心化的思想不符,所以Redis只是利用了其中選舉的部分)

演算法主要部分如下:

  1. Leader選舉

  2. 客戶資料一致化處理(即論文中的log repliecation部分)

Leader選舉

  Raft給系統中的節點分配了三種狀態,分別是Follower, Candidates, Leader;選舉過程即是在分散式式系統中選出Leader狀態節點的過程。三種狀態的轉換關係如下所示:  

  當系統中沒有Leader時,所有節點初始狀態均為Follewer,每個節點在經歷time_out時間後,會自動轉變為Candidate,並嘗試發起投票以便成為新的Leader;為了保證系統中只存在一個Leader,當選新Leader的條件是Candidate收到了超過半數節點以上的投票(每個節點在每輪投票中只能投給唯一的節點,通常是投個第一個發來邀票請求的節點)

,達到該條件後,Candidate即變為Leader。注意到投票是有輪次的,只有收到當前輪次的投票才是有效票。在狀態機中,用term來表示投票的輪次。

根據上面介紹的流程,容易注意到為實現Leader的選舉,有幾個前提:1. 每個節點都需要知道系統中到底有哪些節點存在(為了能向每個節點邀票,同時要知道節點總數才能判斷最終是否收到了多數的投票); 2. 所有節點應該有統一的初始化term,且後續需要不斷的同步term

  第一點前提,只需要初始化階段給所有節點置統一的term即可;而第二點前提則要求各節點主動擁抱新term,無情拋棄老term;具體來說就是每個節點收到舊term的訊息,可以不處理訊息請求,並把自身的較高的term返回;而每個節點收到新的term之後,則要積極響應,並在響應結束之後更新自己的term。

  有一種可能,就是在選舉輪次中所有的節點都沒有收到多數節點的投票認可,那麼就需要發起新一輪的投票;為了減少新一輪投票依舊無法選出Leader的概率,每個Candidate節點的time_out時間是隨機的,這樣通常會有一個最先發出請求的節點佔得先機,能收穫大多數的選票。

客戶資料一致化處理

  由於Redis中並沒有用到Raft演算法的一致化資料處理,這裡不過多描述(該部分內容比較複雜,需要考慮的異常場景較多);詳細的介紹可以看這篇博文, https://www.cnblogs.com/mindwind/p/5231986.html;個人覺得這篇博文很直觀的介紹了分散式系統下在各種異常情況下,資料的一致性是如何保證的。

二、Redis選舉新主節點的實現

  Redis選舉新的主節點,有一個很重要的概念需要先理解下,就是epoch——紀元,類似於Raft中term的意義。Redis的資料結構中記錄了兩個epoch,分別是currentepoch和configepoch;這兩個紀元的作用是不同的。

  currentepoch: 這個變數是記錄整個叢集投票輪次的,初始時,叢集中所有節點的currentepoch都是0;後面雖然沒個發起投票的節點都會自增該值,但也會同時將該值作為投票輪次資訊發給其他節點,整個叢集最終還是會具有相同的currentepoch值;

  configepoch:  這個變數是記錄節點自身的特徵值,主要用於判斷槽的歸屬節點是否需要更新;考慮如下的場景,A節點負責槽1,2,3且configepoch=n,A節點掉線後從屬節點B接替了A的工作成為新的主節點,那麼B此時就負責1,2,3槽,B的configepoch=n1(n1一定大於n,因為每當有從節點通過故障遷移接替主節點工作時,該從節點的configepoch就會變更為整個叢集中最大的configepoch);當A節點恢復工作後,還不知道自己已經被替代了,還向其他節點宣稱自己是1,2,3槽的負責節點。其他節點已將1,2,3槽的負責節點改為B了,當其他節點收到A恢復之後的心跳包,會比較1,2,3槽所屬節點B的configepoch(=n1)與A的configepoch(=n)哪個更大,發現n1更大就不會變更自己的記錄,反過來還要通知A它所負責的槽已經被接管了。

介紹下Redis選舉主節點的流程:

  1. 從節點檢測到自己從屬的主節點下線,開始發起一次選舉;發起方式是向所有主節點廣播發送一條投票請求,希望其他主節點同意自己為新的主節點;(帶上自己記錄的currentepoch,即Raft演算法中的term),原始碼中如下:

  1 /*
  2 從節點的故障轉移,是在函式clusterHandleSlaveFailover中處理的,該函式在叢集定時器函式clusterCron中呼叫。本函式
  3 用於處理從節點進行故障轉移的整個流程,包括:判斷是否可以發起選舉;判斷選舉是否超時;判斷自己是否拉
  4 到了足夠的選票;使自己升級為新的主節點這些所有流程。
  5 */
  6  //slave呼叫
  7 void clusterHandleSlaveFailover(void) { //clusterBeforeSleep對CLUSTER_TODO_HANDLE_FAILOVER狀態的處理,或者clusterCron中實時處理
  8     //也就是當前從節點與主節點已經斷鏈了多長時間,從通過ping pong超時,檢測到本slave的master掉線了,從這時候開始算
  9     mstime_t data_age;
 10     //該變量表示距離發起故障轉移流程,已經過去了多少時間;
 11     mstime_t auth_age = mstime() - server.cluster->failover_auth_time;
 12     //該變量表示當前從節點必須至少獲得多少選票,才能成為新的主節點
 13     int needed_quorum = (server.cluster->size / 2) + 1;
 14     //表示是否是管理員手動觸發的故障轉移流程;
 15     int manual_failover = server.cluster->mf_end != 0 &&
 16                           server.cluster->mf_can_start; //說明向從傳送了cluster failover force要求該從進行強制故障轉移
 17     int j;
 18     //該變量表示故障轉移流程(發起投票,等待迴應)的超時時間,超過該時間後還沒有獲得足夠的選票,則表示本次故障轉移失敗;
 19     mstime_t auth_timeout, 
 20     //該變量表示判斷是否可以開始下一次故障轉移流程的時間,只有距離上一次發起故障轉移時,已經超過auth_retry_time之後,
 21     //才表示可以開始下一次故障轉移了(auth_age > auth_retry_time);
 22              auth_retry_time;
 23 
 24     server.cluster->todo_before_sleep &= ~CLUSTER_TODO_HANDLE_FAILOVER;
 25 
 26     /* Compute the failover timeout (the max time we have to send votes
 27      * and wait for replies), and the failover retry time (the time to wait
 28      * before waiting again.
 29      *
 30      * Timeout is MIN(NODE_TIMEOUT*2,2000) milliseconds.
 31      * Retry is two times the Timeout.
 32      */
 33     auth_timeout = server.cluster_node_timeout*2;
 34     if (auth_timeout < 2000) auth_timeout = 2000;
 35     auth_retry_time = auth_timeout*2;
 36 
 37     /* Pre conditions to run the function, that must be met both in case
 38      * of an automatic or manual failover:
 39      * 1) We are a slave.
 40      * 2) Our master is flagged as FAIL, or this is a manual failover.
 41      * 3) It is serving slots. */
 42     /*
 43     當前節點是主節點;當前節點是從節點但是沒有主節點;當前節點的主節點不處於下線狀態並且不是手動強制進行故障轉移;
 44     當前節點的主節點沒有負責的槽位。滿足以上任一條件,則不能進行故障轉移,直接返回即可;
 45     */
 46     if (nodeIsMaster(myself) ||
 47         myself->slaveof == NULL ||
 48         (!nodeFailed(myself->slaveof) && !manual_failover) ||
 49         myself->slaveof->numslots == 0) {
 50         //真正把slaveof置為NULL在後面真正備選舉為主的時候設定,見後面的replicationUnsetMaster
 51         /* There are no reasons to failover, so we set the reason why we
 52          * are returning without failing over to NONE. */
 53         server.cluster->cant_failover_reason = REDIS_CLUSTER_CANT_FAILOVER_NONE;
 54         return;
 55     }; 
 56 
 57     //slave從節點進行後續處理,並且和主伺服器斷開了連線
 58 
 59     /* Set data_age to the number of seconds we are disconnected from
 60      * the master. */
 61     //將data_age設定為從節點與主節點的斷開秒數
 62     if (server.repl_state == REDIS_REPL_CONNECTED) { //如果主從之間是因為網路不通引起的,read判斷不出epoll err事件,則狀態為這個
 63         data_age = (mstime_t)(server.unixtime - server.master->lastinteraction) 
 64                    * 1000; //也就是當前從節點與主節點最後一次通訊過了多久了
 65     } else { 
 66     //這裡一般都是直接kill主master程序,從epoll err感知到了,會在replicationHandleMasterDisconnection把狀態置為REDIS_REPL_CONNECT
 67         //本從節點和主節點斷開了多久,
 68         data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000; 
 69     }
 70 
 71     /* Remove the node timeout from the data age as it is fine that we are
 72      * disconnected from our master at least for the time it was down to be
 73      * flagged as FAIL, that's the baseline. */
 74     // node timeout 的時間不計入斷線時間之內 如果data_age大於server.cluster_node_timeout,則從data_age中
 75     //減去server.cluster_node_timeout,因為經過server.cluster_node_timeout時間沒有收到主節點的PING回覆,才會將其標記為PFAIL
 76     if (data_age > server.cluster_node_timeout)
 77         data_age -= server.cluster_node_timeout; //從通過ping pong超時,檢測到本slave的master掉線了,從這時候開始算
 78 
 79     /* Check if our data is recent enough. For now we just use a fixed
 80      * constant of ten times the node timeout since the cluster should
 81      * react much faster to a master down.
 82      *
 83      * Check bypassed for manual failovers. */
 84     // 檢查這個從節點的資料是否較新:   
 85     // 目前的檢測辦法是斷線時間不能超過 node timeout 的十倍
 86     /* data_age主要用於判斷當前從節點的資料新鮮度;如果data_age超過了一定時間,表示當前從節點的資料已經太老了,
 87     不能替換掉下線主節點,因此在不是手動強制故障轉移的情況下,直接返回;*/
 88     if (data_age >
 89         ((mstime_t)server.repl_ping_slave_period * 1000) +
 90         (server.cluster_node_timeout * REDIS_CLUSTER_SLAVE_VALIDITY_MULT))
 91     {
 92         if (!manual_failover) {
 93             clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_DATA_AGE);
 94             return;
 95         }
 96     }
 97 
 98     /* If the previous failover attempt timedout and the retry time has
 99      * elapsed, we can setup a new one. */
100 
101     /*
102     例如叢集有7個master,其中redis1下面有2個slave,突然redis1掉了,則slave1和slave2競爭要求其他6個master進行投票,如果這6個
103     master投票給slave1和slave2的票數都是3,也就是3個master投給了slave1,另外3個master投給了slave2,那麼兩個slave都得不到超過一半
104     的票數,則只有靠這裡的超時來進行重新投票了。不過一半這種情況很少發生,因為發起投票的時間是隨機的,因此一半一個slave的投票報文auth req會比
105     另一個slave的投票報文先發出。越先發出越容易得到投票
106     */ 
107     /*
108     如果auth_age大於auth_retry_time,表示可以開始進行下一次故障轉移了。如果之前沒有進行過故障轉移,則auth_age等
109     於mstime,肯定大於auth_retry_time;如果之前進行過故障轉移,則只有距離上一次發起故障轉移時,已經超過
110     auth_retry_time之後,才表示可以開始下一次故障轉移。
111     */
112     if (auth_age > auth_retry_time) {  
113     //每次超時從新發送auth req要求其他主master投票,都會先走這個if,然後下次呼叫該函式才會走if後面的流程
114         server.cluster->failover_auth_time = mstime() +
115             500 + /* Fixed delay of 500 milliseconds, let FAIL msg propagate. */
116             random() % 500; /* Random delay between 0 and 500 milliseconds. */ //等到這個時間到才進行故障轉移
117         server.cluster->failover_auth_count = 0;
118         server.cluster->failover_auth_sent = 0;
119         server.cluster->failover_auth_rank = clusterGetSlaveRank();//本節點按照在master中的repl_offset來獲取排名
120         /* We add another delay that is proportional to the slave rank.
121          * Specifically 1 second * rank. This way slaves that have a probably
122          * less updated replication offset, are penalized. */
123         server.cluster->failover_auth_time +=
124             server.cluster->failover_auth_rank * 1000;
125             
126         /* However if this is a manual failover, no delay is needed. */
127 
128         /*
129         注意如果是管理員發起的手動強制執行故障轉移,則設定server.cluster->failover_auth_time為當前時間,表示會
130         立即開始故障轉移流程;最後,呼叫clusterBroadcastPong,向該下線主節點的所有從節點發送PONG包,包頭部分帶
131         有當前從節點的複製資料量,因此其他從節點收到之後,可以更新自己的排名;最後直接返回;
132         */
133         if (server.cluster->mf_end) {
134             server.cluster->failover_auth_time = mstime();
135             server.cluster->failover_auth_rank = 0;
136         }
137         redisLog(REDIS_WARNING,
138             "Start of election delayed for %lld milliseconds "
139             "(rank #%d, offset %lld).",
140             server.cluster->failover_auth_time - mstime(),
141             server.cluster->failover_auth_rank,
142             replicationGetSlaveOffset());
143         /* Now that we have a scheduled election, broadcast our offset
144          * to all the other slaves so that they'll updated their offsets
145          * if our offset is better. */
146         /*
147         呼叫clusterBroadcastPong,向該下線主節點的所有從節點發送PONG包,包頭部分帶
148         有當前從節點的複製資料量,因此其他從節點收到之後,可以更新自己的排名;最後直接返回;
149         */
150         clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_SLAVES);
151         return;
152     }
153 
154     /* 進行故障轉移 */
155 
156     /* It is possible that we received more updated offsets from other
157      * slaves for the same master since we computed our election delay.
158      * Update the delay if our rank changed.
159      *
160      * Not performed if this is a manual failover. */
161     /*
162     如果還沒有開始故障轉移,則呼叫clusterGetSlaveRank,取得當前從節點的最新排名。因為在開始故障轉移之前,
163     可能會收到其他從節點發來的心跳包,因而可以根據心跳包中的複製偏移量更新本節點的排名,獲得新排名newrank,
164     如果newrank比之前的排名靠後,則需要增加故障轉移開始時間的延遲,然後將newrank記錄到server.cluster->failover_auth_rank中;
165     */
166     if (server.cluster->failover_auth_sent == 0 &&
167         server.cluster->mf_end == 0) //還沒有進行過故障莊毅
168     {
169         int newrank = clusterGetSlaveRank();
170         if (newrank > server.cluster->failover_auth_rank) {
171             long long added_delay =
172                 (newrank - server.cluster->failover_auth_rank) * 1000;
173             server.cluster->failover_auth_time += added_delay;
174             server.cluster->failover_auth_rank = newrank;
175             redisLog(REDIS_WARNING,
176                 "Slave rank updated to #%d, added %lld milliseconds of delay.",
177                 newrank, added_delay);
178         }
179     }
180 
181     /* Return ASAP if we can't still start the election. */
182      // 如果執行故障轉移的時間未到,先返回
183     if (mstime() < server.cluster->failover_auth_time) {
184         clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_DELAY);
185         return;
186     }
187 
188     /* Return ASAP if the election is too old to be valid. */
189     // 如果距離應該執行故障轉移的時間已經過了很久   
190     // 那麼不應該再執行故障轉移了(因為可能已經沒有需要了)
191     // 直接返回
192     if (auth_age > auth_timeout) {// 如果auth_age大於auth_timeout,說明之前的故障轉移超時了,因此直接返回;
193         clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_EXPIRED);
194         return;
195     }
196     
197 
198     /* Ask for votes if needed. */
199    // 向其他節點發送故障轉移請求
200     if (server.cluster->failover_auth_sent == 0) {
201 
202          // 增加配置紀元
203         server.cluster->currentEpoch++;
204 
205          // 記錄發起故障轉移的配置紀元
206         server.cluster->failover_auth_epoch = server.cluster->currentEpoch;
207 
208         redisLog(REDIS_WARNING,"Starting a failover election for epoch %llu.",
209             (unsigned long long) server.cluster->currentEpoch);
210 
211         //向其他所有節點發送資訊,看它們是否支援由本節點來對下線主節點進行故障轉移
212         clusterRequestFailoverAuth();
213 
214          // 開啟標識,表示已傳送資訊
215         server.cluster->failover_auth_sent = 1;
216 
217         // TODO:       
218         // 在進入下個事件迴圈之前,執行:      
219         // 1)儲存配置檔案      
220         // 2)更新節點狀態        
221         // 3)同步配置
222         clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
223                              CLUSTER_TODO_UPDATE_STATE|
224                              CLUSTER_TODO_FSYNC_CONFIG);
225         return; /* Wait for replies. */
226     }
227 
228     /* Check if we reached the quorum. */
229    // 如果當前節點獲得了足夠多的投票,那麼對下線主節點進行故障轉移
230     if (server.cluster->failover_auth_count >= needed_quorum) {
231         // 舊主節點
232         clusterNode *oldmaster = myself->slaveof; //在後面clusterSetNodeAsMaster中把slaveof置為NULL
233 
234         redisLog(REDIS_WARNING,
235             "Failover election won: I'm the new master.");
236         redisLog(REDIS_WARNING,
237                 "configEpoch set to %llu after successful failover",
238                 (unsigned long long) myself->configEpoch);
239 
240         /* We have the quorum, perform all the steps to correctly promote
241          * this slave to a master.
242          *
243          * 1) Turn this node into a master. 
244          *    將當前節點的身份由從節點改為主節點
245          */
246         clusterSetNodeAsMaster(myself);
247         // 讓從節點取消複製,成為新的主節點
248         replicationUnsetMaster();
249 
250         /* 2) Claim all the slots assigned to our master. */
251        // 接收所有主節點負責處理的槽  輪訓16384個槽位,當前節點接手老的主節點負責的槽位;
252         for (j = 0; j < REDIS_CLUSTER_SLOTS; j++) {
253             if (clusterNodeGetSlotBit(oldmaster,j)) {
254                  // 將槽設定為未分配的               
255                  clusterDelSlot(j);            
256                  // 將槽的負責人設定為當前節點
257                 clusterAddSlot(myself,j);
258             }
259         }
260 
261         /* 3) Update my configEpoch to the epoch of the election. */
262         // 更新叢集配置紀元  本節點此時的配置epoch就是叢集中最大的configEpoch
263         myself->configEpoch = server.cluster->failover_auth_epoch;
264 
265         /* 4) Update state and save config. */
266         // 更新節點狀態       
267         clusterUpdateState();     
268         // 並儲存配置檔案
269         clusterSaveConfigOrDie(1);
270 
271         //如果一個主master下面有2個savle,如果master掛了,通過選舉slave1被選為新的主,則slave2通過這裡來觸發重新連線到新主,即slave1,見clusterUpdateSlotsConfigWith
272         /* 5) Pong all the other nodes so that they can update the state
273          *    accordingly and detect that we switched to master role. */
274         // 向所有節點發送 PONG 資訊      
275         // 讓它們可以知道當前節點已經升級為主節點了      
276         clusterBroadcastPong(CLUSTER_BROADCAST_ALL);  //真正觸發其他本來屬於同一個master的slave節點,連線到這個新選舉出的master,是在clusterUpdateSlotsConfigWith   
277         /* 6) If there was a manual failover in progress, clear the state. */      
278 
279         // 如果有手動故障轉移正在執行,那麼清理和它有關的狀態
280         resetManualFailover();
281     } else {
282         //說明沒有獲得足夠的票數,列印:Waiting for votes, but majority still not reached.
283         clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_VOTES); //例如6個主節點,現在只有1個主節點投票auth ack過來了,則會列印這個
284     }
285 }

  2. 主節點接收所有從節點發來的投票請求,但會比較該請求附帶的配置紀元是否為新的(大於主節點自己記錄的currentepoch),只有帶著大於等於的訊息才是有效請求;主節點只會投票給第一個發來有效投票請求的從節點。

  1 void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) {//node為sender
  2 //主節點對slave傳送過來的CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST進行投票
  3 /*
  4 叢集中所有節點收到用於拉票的CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST包後,只有負責一定槽位的主節點能投票,其他沒資格的節點直接忽略掉該包。
  5 */
  6     // 請求節點的主節點    
  7     clusterNode *master = node->slaveof;   
  8 
  9     // 請求節點的當前配置紀元   
 10     uint64_t requestCurrentEpoch = ntohu64(request->currentEpoch);    
 11 
 12     // 請求節點想要獲得投票的紀元  
 13     uint64_t requestConfigEpoch = ntohu64(request->configEpoch);   
 14     // 請求節點的槽佈局
 15 
 16     unsigned char *claimed_slots = request->myslots;
 17     int force_ack = request->mflags[0] & CLUSTERMSG_FLAG0_FORCEACK;
 18     int j;
 19 
 20     /* IF we are not a master serving at least 1 slot, we don't have the
 21      * right to vote, as the cluster size in Redis Cluster is the number
 22      * of masters serving at least one slot, and quorum is the cluster
 23      * size + 1 */
 24 
 25     // 如果節點為從節點,或者是一個沒有處理任何槽的主節點, 
 26     // 那麼它沒有投票權
 27     if (nodeIsSlave(myself) || myself->numslots == 0) return; //從節點和不負責槽位處理的直接返回,不參與投票
 28 
 29     /* Request epoch must be >= our currentEpoch. */
 30      // 請求的配置紀元必須大於等於當前節點的配置紀元
 31      /*
 32      如果傳送者的currentEpoch小於當前節點的currentEpoch,則拒絕為其投票。因為傳送者的狀態與當前叢集狀態不一致,
 33      可能是長時間下線的節點剛剛上線,這種情況下,直接返回即可;
 34      */
 35     if (requestCurrentEpoch < server.cluster->currentEpoch) {
 36         redisLog(REDIS_WARNING,
 37             "Failover auth denied to %.40s: reqEpoch (%llu) < curEpoch(%llu)",
 38             node->name,
 39             (unsigned long long) requestCurrentEpoch,
 40             (unsigned long long) server.cluster->currentEpoch);
 41         return;
 42     }
 43 
 44     /* I already voted for this epoch? Return ASAP. */
 45     // 已經投過票了
 46     /*
 47     如果當前節點lastVoteEpoch,與當前節點的currentEpoch相等,說明本界選舉中,當前節點已經投過票了,不
 48     在重複投票,直接返回(因此,如果有兩個從節點同時發起拉票,則當前節點先收到哪個節點的包,就只給那個
 49     節點投票。注意,即使這兩個從節點分屬不同主節點,也只能有一個從節點獲得選票);
 50     */
 51     if (server.cluster->lastVoteEpoch == server.cluster->currentEpoch) {
 52         redisLog(REDIS_WARNING,
 53                 "Failover auth denied to %.40s: already voted for epoch %llu",
 54                 node->name,
 55                 (unsigned long long) server.cluster->currentEpoch);
 56         return;
 57     }
 58     
 59     /* Node must be a slave and its master down.
 60      * The master can be non failing if the request is flagged
 61      * with CLUSTERMSG_FLAG0_FORCEACK (manual failover). */
 62     /*
 63     如果傳送節點是主節點;或者傳送節點雖然是從節點,但是找不到其主節點;或者傳送節點的主節點並未下線
 64     並且這不是手動強制開始的故障轉移流程,則根據不同的條件,記錄日誌後直接返回;
 65     */
 66     if (nodeIsMaster(node) || master == NULL ||
 67         (!nodeFailed(master) && !force_ack)) { //如果從接收到cluster failover,然後發起auth req要求投票,則master受到後,就是該master線上也需要進行投票
 68         if (nodeIsMaster(node)) { //auth  request必須由slave發起
 69             redisLog(REDIS_WARNING,
 70                     "Failover auth denied to %.40s: it is a master node",
 71                     node->name);
 72         } else if (master == NULL) {
 73         //slave認為自己的master下線了,但是本節點不知道他的master是那個,也就不知道是為那個master的slave投票,
 74         //因為我們要記錄是對那個master的從節點投票的,看if後面的流程
 75             redisLog(REDIS_WARNING,
 76                     "Failover auth denied to %.40s: I don't know its master",
 77                     node->name);
 78         } else if (!nodeFailed(master)) { //從這裡也可以看出,必須叢集中有一個主節點判斷出某個節點fail了,才會處理slave傳送過來的auth req
 79         //slave認為自己的master下線了,於是傳送過來auth request,本主節點收到該資訊後,發現
 80         //該slave對應的master是正常的,因此給出列印,不投票
 81             redisLog(REDIS_WARNING,
 82                     "Failover auth denied to %.40s: its master is up",
 83                     node->name);
 84         }
 85         return;
 86     }
 87     /* We did not voted for a slave about this master for two
 88      * times the node timeout. This is not strictly needed for correctness
 89      * of the algorithm but makes the base case more linear. */
 90      /*
 91      針對同一個下線主節點,在2*server.cluster_node_timeout時間內,只會投一次票,這並非必須的限制條
 92      件(因為之前的lastVoteEpoch判斷,已經可以避免兩個從節點同時贏得本界選舉了),但是這可以使得獲
 93      勝從節點有時間將其成為新主節點的訊息通知給其他從節點,從而避免另一個從節點發起新一輪選舉又進
 94      行一次沒必要的故障轉移;
 95      */
 96      // 如果之前一段時間已經對請求節點進行過投票,那麼不進行投票
 97     if (mstime() - node->slaveof->voted_time < server.cluster_node_timeout * 2)
 98     {
 99         redisLog(REDIS_WARNING,
100                 "Failover auth denied to %.40s: "
101                 "can't vote about this master before %lld milliseconds",
102                 node->name,
103                 (long long) ((server.cluster_node_timeout*2)-
104                              (mstime() - node->slaveof->voted_time)));
105         return;
106     }
107 
108     /* The slave requesting the vote must have a configEpoch for the claimed
109      * slots that is >= the one of the masters currently serving the same
110      * slots in the current configuration. */
111     /*
112         判斷髮送節點,對其宣稱要負責的槽位,是否比之前負責這些槽位的節點,具有相等或更新的配置紀元configEpoch:
113     該槽位當前的負責節點的configEpoch,是否比傳送節點的configEpoch要大,若是,說明發送節點的配置資訊不是最新的,
114     可能是一個長時間下線的節點又重新上線了,這種情況下,不能給他投票,因此直接返回;
115     */
116     for (j = 0; j < REDIS_CLUSTER_SLOTS; j++) {
117 
118          // 跳過未指派節點
119         if (bitmapTestBit(claimed_slots, j) == 0) continue;
120 
121         // 查詢是否有某個槽的配置紀元大於節點請求的紀元
122         if (server.cluster->slots[j] == NULL || server.cluster->slots[j]->configEpoch <= requestConfigEpoch) 
123         //這裡就是configEpoch真正發揮作用的地方
124         {
125             continue;
126         }
127 
128         // 如果有的話,說明節點請求的紀元已經過期,沒有必要進行投票
129         /* If we reached this point we found a slot that in our current slots
130          * is served by a master with a greater configEpoch than the one claimed
131          * by the slave requesting our vote. Refuse to vote for this slave. */
132         redisLog(REDIS_WARNING,
133                 "Failover auth denied to %.40s: "
134                 "slot %d epoch (%llu) > reqEpoch (%llu)",
135                 node->name, j,
136                 (unsigned long long) server.cluster->slots[j]->configEpoch,
137                 (unsigned long long) requestConfigEpoch);
138         return;
139     }
140 
141     /* We can vote for this slave. */
142     // 為節點投票    
143     clusterSendFailoverAuth(node);
144     // 更新時間值
145     server.cluster->lastVoteEpoch = server.cluster->currentEpoch;
146     node->slaveof->voted_time = mstime();
147 
148     redisLog(REDIS_WARNING, "Failover auth granted to %.40s for epoch %llu",
149         node->name, (unsigned long long) server.cluster->currentEpoch);
150 }

  3. 如果有從節點收到了系統中超過半數主節點的投票,則變為新的主節點,並向所有節點廣播自己當選的資訊;

  4,.如果從節點在超時時間內沒有收到多數主節點的投票,也沒有收到其他從節點升級為新主節點的廣播,就會再次發起投票,重複第3步。

  新的主節點向所有其他節點廣播之後,其他的節點會更新自己的配置,將新的主節點和其負責的槽對應起來。再有和相關槽對應的命令發到叢集就會被轉發給這個新的主節點;至此故障遷移結束。我們可以考慮下,如果同時有半數以上的伺服器掉線了,是否會導致叢集徹底失效呢?