Redis 源碼分析之故障轉移

在 Redis cluster 中故障轉移是個很重要的功能,下面就從故障發現到故障轉移整個流程做一下詳細分析。

故障檢測

PFAIL 標記

集羣中每個節點都會定期向其他節點發送 PING 消息,以此來檢測對方是否在線,如果接收 PING 消息的節點 B 沒有在規定時間(cluster_node_timeout)內迴應節點 A PONG 消息,那麼節點 A 就會將節點 B 標記爲疑似下線(probable fail, PFAIL)。

void clusterCron(void) {
    // ...
    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        clusterNode *node = dictGetVal(de);
        now = mstime(); /* Use an updated time at every iteration. */
        // ...
        delay = now - node->ping_sent;
        if (delay > server.cluster_node_timeout) {
            /* Timeout reached. Set the node as possibly failing if it is
             * not already in this state. */
            if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
                node->flags |= CLUSTER_NODE_PFAIL;
                update_state = 1;
            }
        }
    }
    dictReleaseIterator(di);
    // ...
}

可以看到,在 clusterCron 函數中如果對節點 B 發出 PING 消息,在 server.cluster_node_timeout 時間內沒有收到其返回的 PONG 消息,如果節點 B 現在沒有被標記成 CLUSTER_NODE_PFAIL 狀態,那麼現在就做下這個標記。

可以根據 ping_sent 參數進行判斷的依據如下,

int clusterProcessPacket(clusterLink *link) {
    // ...
    if (link->node && type == CLUSTERMSG_TYPE_PONG) {
        link->node->pong_received = mstime();
        link->node->ping_sent = 0;
        // ...
    }
    // ...
}

當節點 A 接收到節點 B 的 PONG 消息時,會把 ping_sent 更新成 0,同時記下收到本次 PONG 消息的時間。

上面提到的 clusterNode 與 clusterLink 有如下關聯關係:

可以看出, clusterLink 就是爲了接收對端 gossip 消息而設置的。

另外,我們發現, 在上面的 clusterCron 函數中將節點標記成 PFAIL 時,會將 update_state 變量置爲 1,這會引發後面更改集羣狀態的邏輯。

if (update_state || server.cluster->state == CLUSTER_FAIL)
    clusterUpdateState();

集羣有兩個狀態,CLUSTER_OK 和 CLUSTER_FAIL,如果集羣目前狀態是 CLUSTER_FAIL,且設置了參數 cluster-require-full-coverage yes,那麼此時訪問集羣會返回錯誤,意思是可能有某些 slot 沒有被 server 接管。

clusterUpdateState 函數負責更新集羣狀態,該部分邏輯與本篇博文要講的主邏輯關係不大,所以放到了後面的補充章節中了。

FAIL 標記

主動標記 FAIL

被節點 A 標記成 FAIL/ PFAIL 的節點如何讓節點 C 知道呢?這主要是通過平常發送的 PING/PONG 消息實現的,在 3.x 的版本時,會盡最大努力把這樣的節點放到 gossip 消息的流言部分,到後面的 4.x 版本的代碼中每次的 PING/PONG 消息都會把 PFAIL 節點都帶上。

clusterProcessGossipSection 函數用來處理 gossip 消息的流言部分。

void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
    uint16_t count = ntohs(hdr->count);
    clusterMsgDataGossip *g = (clusterMsgDataGossip*) hdr->data.ping.gossip;
    clusterNode *sender = link->node ? link->node : clusterLookupNode(hdr->sender);
    while(count--) {
        // ...
        node = clusterLookupNode(g->nodename);
        if (node) { 
            if (sender && nodeIsMaster(sender) && node != myself) { 
                if (flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) { 
                    if (clusterNodeAddFailureReport(node,sender)) {
                        serverLog(LL_VERBOSE,
                           "Node %.40s reported node %.40s as not reachable.",
                            sender->name, node->name);
                    }
                    markNodeAsFailingIfNeeded(node);
                } else {
                // ...
                }
            }
        // ...
        }
    // ...
    }
    // ...
}

該函數依次處理 gossip 消息流言部分攜帶的各節點信息(總節點數的1/10)。當發現帶有 CLUSTER_NODE_FAIL 或者 CLUSTER_NODE_PFAIL 時會調用 clusterNodeAddFailureReport 函數。

int clusterNodeAddFailureReport(clusterNode *failing, clusterNode *sender) {
    list *l = failing->fail_reports;
    listNode *ln;
    listIter li;
    clusterNodeFailReport *fr;

    /* If a failure report from the same sender already exists, just update
     * the timestamp. */
    listRewind(l,&li);
    while ((ln = listNext(&li)) != NULL) {
        fr = ln->value;
        if (fr->node == sender) {
            fr->time = mstime();
            return 0;
        }
    }

    /* Otherwise create a new report. */
    fr = zmalloc(sizeof(*fr));
    fr->node = sender;
    fr->time = mstime();
    listAddNodeTail(l,fr);
    return 1;
}

每一個節點都有一個名爲 fail_reports 的 list 結構的變量,用來蒐集該異常節點獲得了集羣中哪些節點的 PFAIL 狀態投票。fail_reports 每個成員都是一個 clusterNodeFailReport 結構。

typedef struct clusterNodeFailReport {
    struct clusterNode *node;  /* Node reporting the failure condition. */
    mstime_t time;             /* Time of the last report from this node. */
} clusterNodeFailReport;

clusterNodeFailReport 中帶有時間戳,標記這個節點上一次被報上來處於異常狀態的時間。

每次調用 clusterNodeAddFailureReport 函數時,先會檢查sender 是否已經爲該異常節點投票過了,如果有,更新時間戳,如果沒有,把 sender 加入到投票節點中。

簡單點說就是,在 A 節點看來 B 節點是 PFAIL 狀態,在 gossip 通信中把它告訴了 C 節點,C 節點發現這個異常狀態的節點,檢查一下爲 B 節點投過票的節點中有沒有 A 節點,如果沒有就加進去。

然後下面就是判斷 PFAIL 狀態是不是要轉變成 FAIL 狀態的關鍵。

void markNodeAsFailingIfNeeded(clusterNode *node) {
    int failures;
    int needed_quorum = (server.cluster->size / 2) + 1;

    if (!nodeTimedOut(node)) return; /* We can reach it. */
    if (nodeFailed(node)) return; /* Already FAILing. */

    failures = clusterNodeFailureReportsCount(node);
    /* Also count myself as a voter if I'm a master. */
    if (nodeIsMaster(myself)) failures++;
    if (failures < needed_quorum) return; /* No weak agreement from masters. */

    serverLog(LL_NOTICE, "Marking node %.40s as failing (quorum reached).", node->name);

    /* Mark the node as failing. */
    node->flags &= ~CLUSTER_NODE_PFAIL;
    node->flags |= CLUSTER_NODE_FAIL;
    node->fail_time = mstime();

    /* Broadcast the failing node name to everybody, forcing all the other
     * reachable nodes to flag the node as FAIL. */
    if (nodeIsMaster(myself)) clusterSendFail(node->name); /* 廣播這個節點的 fail 消息 */
    clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
}

C 節點收到消息,檢查下 A 報過來的異常節點 B,在自己看來是否也是 PFAIL 狀態的,如果不是,那麼不理會 A 節點本次 report。如果在節點 C 看來,節點 B 已經被標記成 FAIL 了,那麼就不需要進行下面的判定了。

在函數 clusterNodeFailureReportsCount 中會判斷計算出把 B 節點標記成 PFAIL 狀態的節點的數量 sum,如果 sum 值小於集羣 size 的一半,爲防止誤判,忽略掉這條信息。在函數 clusterNodeFailureReportsCount 中會檢查關於 B 節點的 clusterNodeFailReport,清理掉那些過期的投票,過期時間爲 2 倍的 server.cluster_node_timeout

如果滿足條件,節點 C 將節點 B 的 PFAIL 狀態消除,標記成 FAIL,同時記下 fail_time,如果 C 節點是個 master,那麼將 B 節點 FAIL 的消息廣播出去,以便讓集羣中其他節點儘快知道。

void clusterSendFail(char *nodename) {
    unsigned char buf[sizeof(clusterMsg)];
    clusterMsg *hdr = (clusterMsg*) buf;
    clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAIL);
    memcpy(hdr->data.fail.about.nodename,nodename,CLUSTER_NAMELEN);
    clusterBroadcastMessage(buf,ntohl(hdr->totlen));
}

發送的 gossip 消息類型爲 CLUSTERMSG_TYPE_FAIL,廣播的節點排除自身和處於 HANDSHAKE 狀態節點。

Gossip 被動感知 FAIL

前面說過,gossip 消息的處理函數爲 clusterProcessPacket,下面看 CLUSTERMSG_TYPE_FAIL 類型的消息如何處理。

int clusterProcessPacket(clusterLink *link) {
    // ...
    uint16_t type = ntohs(hdr->type);
    // ...
    if (type == CLUSTERMSG_TYPE_FAIL) { // fail
        clusterNode *failing;
        if (sender) {
            failing = clusterLookupNode(hdr->data.fail.about.nodename);
            if (failing && !(failing->flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_MYSELF)))
            {
                serverLog(LL_NOTICE,
                    "FAIL message received from %.40s about %.40s",
                    hdr->sender, hdr->data.fail.about.nodename);
                failing->flags |= CLUSTER_NODE_FAIL;
                failing->fail_time = mstime();
                failing->flags &= ~CLUSTER_NODE_PFAIL;
                clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
                                     CLUSTER_TODO_UPDATE_STATE);
            }
        } else {
            serverLog(LL_NOTICE,
                "Ignoring FAIL message from unknown node %.40s about %.40s",
                hdr->sender, hdr->data.fail.about.nodename);
        }
    }
    // ...
}

集羣中另一個節點 D 收到節點 B 廣播過來的消息:B 節點 FAIL 了。如果 D 還沒有把 B 標記成 FAIL,那麼標記成 CLUSTER_NODE_FAIL,並取消 CLUSTER_NODE_PFAIL 標記;否則,忽略,因爲D已經知道 B 是 FAIL 節點了。

故障轉移

failover 分爲兩類,主動 failover(主動切主從)以及被動 failover(被動切主從),下面挨個進行分析。

被動 failover

先驗條件及初始化

void clusterCron(void) {
    // ...
    if (nodeIsSlave(myself)) {
        clusterHandleSlaveFailover();
        // ...
    }
    // ...
}

是否要做被動主從切換,在 clusterHandleSlaveFailover 函數中有如下的判斷邏輯,

if (nodeIsMaster(myself) ||
    myself->slaveof == NULL ||
    (!nodeFailed(myself->slaveof) && !manual_failover) ||
    myself->slaveof->numslots == 0)
{
    /* There are no reasons to failover, so we set the reason why we
     * are returning without failing over to NONE. */
    server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
    return;
}

只有滿足如下條件的節點纔有資格做 failover:

  • slave 節點
  • master 不爲空
  • master 負責的 slot 數量不爲空
  • master 被標記成了 FAIL,或者這是一個主動 failover(manual_failover 爲真)

假設,現在 B 節點的 slave Bx 節點檢測到 B 節點掛掉了,通過了以上的條件測試,接下來就會進行 failover。

那麼下面 Bx 節點就開始在集羣中進行拉票,該邏輯也在 clusterHandleSlaveFailover 函數中。

mstime_t auth_age = mstime() - server.cluster->failover_auth_time; 
int needed_quorum = (server.cluster->size / 2) + 1;
mstime_t auth_timeout, auth_retry_time;

auth_timeout = server.cluster_node_timeout*2;
if (auth_timeout < 2000) auth_timeout =2000 ;
auth_retry_time = auth_timeout*2; 

cluster 的 failover_auth_time 屬性,表示 slave 節點開始進行故障轉移的時刻。集羣初始化時該屬性置爲 0,一旦滿足 failover 的條件後,該屬性就置爲未來的某個時間點(不是立馬執行),在該時間點,slave 節點纔開始進行拉票。

auth_age 變量表示從發起 failover 流程開始到現在,已經過去了多長時間。

needed_quorum 變量表示當前 slave 節點必須至少獲得多少選票,才能成爲新的 master。

auth_timeout 變量表示當前 slave 發起投票後,等待迴應的超時時間,至少爲 2s。如果超過該時間還沒有獲得足夠的選票,那麼表示本次 failover 失敗。

auth_retry_time 變量用來判斷是否可以開始發起下一次 failover 的時間間隔。

if (server.repl_state == REPL_STATE_CONNECTED) {
    data_age = (mstime_t)(server.unixtime - server.master->lastinteraction) * 1000;
} else {
    data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}
if (data_age > server.cluster_node_timeout)
    data_age -= server.cluster_node_timeout;

data_age 變量表示距離上一次與我的 master 節點交互過去了多長時間。經過 cluster_node_timeout 時間還沒有收到 PONG 消息纔會將節點標記爲 PFAIL 狀態。實際上 data_age 表示在 master 節點下線之前,當前 slave 節點有多長時間沒有與其交互過了。

data_age 主要用於判斷當前 slave 節點的數據新鮮度;如果 data_age 超過了一定時間,表示當前 slave 節點的數據已經太老了,不能替換掉下線 master 節點,因此在不是手動強制故障轉移的情況下,直接返回。

制定 failover 時間

void clusterHandleSlaveFailover(void) {
    // ...
    if (auth_age > auth_retry_time) {
        server.cluster->failover_auth_time = mstime() +
            500 + /* Fixed delay of 500 milliseconds, let FAIL msg propagate. */
            random() % 500; /* Random delay between 0 and 500 milliseconds. */
        server.cluster->failover_auth_count = 0;
        server.cluster->failover_auth_sent = 0;
        server.cluster->failover_auth_rank = clusterGetSlaveRank();
        /* We add another delay that is proportional to the slave rank.
         * Specifically 1 second * rank. This way slaves that have a probably
         * less updated replication offset, are penalized.
         * */
        server.cluster->failover_auth_time +=
            server.cluster->failover_auth_rank * 1000;
        if (server.cluster->mf_end) {
            server.cluster->failover_auth_time = mstime();
            server.cluster->failover_auth_rank = 0;
        }
        // ...
        clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_SLAVES);
        return;
    }
    // ...
}

滿足條件(auth_age > auth_retry_time)後,發起故障轉移流程。

首先設置故障轉移發起時刻,即設置 failover_auth_time 時間。

mstime() + 500 + random()%500 + rank*1000

固定延時 500ms 是爲了讓 master fail 的消息能夠廣泛傳播到集羣,這樣集羣中的其他節點纔可能投票。

隨機延時是爲了避免多個你 slave 節點同時發起 failover 流程。

rank 表示 slave 節點的排名,計算方式如下,

int clusterGetSlaveRank(void) {
    long long myoffset;
    int j, rank = 0;
    clusterNode *master;

    serverAssert(nodeIsSlave(myself));
    master = myself->slaveof;
    if (master == NULL) return 0; /* Never called by slaves without master. */

    myoffset = replicationGetSlaveOffset();
    for (j = 0; j < master->numslaves; j++)
        if (master->slaves[j] != myself &&
            master->slaves[j]->repl_offset > myoffset) rank++;
    return rank;
}

可以看出,排名主要是根據複製數據量來定,複製數據量越多,排名越靠前(rank 值越小)。這樣做是爲了做 failover 時儘量選擇一個複製數據量較多的 slave,以盡最大努力保留數據。在沒有開始拉選票之前,每隔一段時間(每次調用clusterHandleSlaveFailover函數,也就是每次 cron 的時間)就會調用一次 clusterGetSlaveRank 函數,以更新當前 slave 節點的排名。
注意,如果是 mf,那麼 failover_auth_time 和 failover_auth_rank 都置爲 0,表示該 slave 節點現在就可以執行故障轉移。
最後向該 master 的所有 slave 廣播 PONG 消息,主要是爲了更新複製偏移量,以便其他 slave 計算出 failover 時間點。
這時,函數返回,就此開始了一輪新的故障轉移,當已經處在某一輪故障轉移時,執行接下來的邏輯。

slave 拉選票

首先對於一些不合理的 failover 要過濾掉。

/* Return ASAP if we can't still start the election.
 */
if (mstime() < server.cluster->failover_auth_time) {
    clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_DELAY);
    return;
}

/* Return ASAP if the election is too old to be valid.
 * failover 超時
 */
if (auth_age > auth_timeout) {
    clusterLogCantFailover(CLUSTER_CANT_FAILOVER_EXPIRED);
    return;
}

然後開始拉選票。

if (server.cluster->failover_auth_sent == 0) {
    server.cluster->currentEpoch++; // 增加當前節點的currentEpoch的值,表示要開始新一輪選舉了
    server.cluster->failover_auth_epoch = server.cluster->currentEpoch;
    serverLog(LL_WARNING,"Starting a failover election for epoch %llu.",
              (unsigned long long) server.cluster->currentEpoch);

    /* 向所有節點發送 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,開始拉票*/
    clusterRequestFailoverAuth();
    server.cluster->failover_auth_sent = 1; // 表示已經發起了故障轉移流程
    clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
                         CLUSTER_TODO_UPDATE_STATE|
                         CLUSTER_TODO_FSYNC_CONFIG);
    return; /* Wait for replies. */
}

如果 failover_auth_sent 爲 0,表示沒有發起過投票,那麼將 currentEpoch 加 1,記錄 failover_auth_epoch 爲 currentEpoch,函數 clusterRequestFailoverAuth 用來發起投票,failover_auth_sent 置 1,表示該 slave 已經發起過投票了。

void clusterRequestFailoverAuth(void) {
    unsigned char buf[sizeof(clusterMsg)];
    clusterMsg *hdr = (clusterMsg*) buf;
    uint32_t totlen;

    clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST);
    /* If this is a manual failover, set the CLUSTERMSG_FLAG0_FORCEACK bit
     * in the header to communicate the nodes receiving the message that
     * they should authorized the failover even if the master is working. */
    if (server.cluster->mf_end) hdr->mflags[0] |= CLUSTERMSG_FLAG0_FORCEACK;
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    hdr->totlen = htonl(totlen);
    clusterBroadcastMessage(buf,totlen);
}

clusterRequestFailoverAuth 函數向集羣廣播 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 類型的 gossip 信息,這類型的信息就是向集羣中的 master 節點索要本輪選舉中的選票。另外,如果是 mf,那麼會在 gossip hdr 中帶上 CLUSTERMSG_FLAG0_FORCEACK 信息。

其他 master 投票

else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST) {
    if (!sender) return 1;  /* We don't know that node. */
    clusterSendFailoverAuthIfNeeded(sender,hdr);
}

clusterProcessPacket 函數中處理 gossip 消息,當接收到 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 類型的消息時,調用 clusterSendFailoverAuthIfNeeded 函數處理,在滿足條件的基礎上,給 sender 投票。

注:以下若不進行特殊說明,都是 clusterSendFailoverAuthIfNeeded 函數處理邏輯。

篩掉沒資格投票的節點
 if (nodeIsSlave(myself) || myself->numslots == 0) return;

slave 節點或者不負責 slot 的 master 節點

篩掉不需要投票的 sender
uint64_t requestCurrentEpoch = ntohu64(request->currentEpoch);
if (requestCurrentEpoch < server.cluster->currentEpoch) {
    serverLog(LL_WARNING,
              "Failover auth denied to %.40s: reqEpoch (%llu) < curEpoch(%llu)",
              node->name,
              (unsigned long long) requestCurrentEpoch,
              (unsigned long long) server.cluster->currentEpoch);
    return;
}

sender 節點集羣信息過舊。
正常來說,如果 receiver 在接收到 sender 的 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息之前接收了 PING/PONG 消息,會更新自己的 currentEpoch,這時 currentEpoch 會增加,因爲 sender 發起選舉之前,會先增加自身的currentEpoch;否則的話,receiver 的 currentEpoch 應該小於 sender。因此 sender 的 currentEpoch 應該 >= receiver 的。有可能 sender 是個長時間下線的節點剛剛上線,這樣的節點不能給他投票,因爲它的集羣信息過舊。

if (server.cluster->lastVoteEpoch == server.cluster->currentEpoch) {
    serverLog(LL_WARNING,
              "Failover auth denied to %.40s: already voted for epoch %llu",
              node->name,
              (unsigned long long) server.cluster->currentEpoch);
    return;
}

receiver 節點在本輪選舉中已經投過票了,避免兩個 slave 節點同時贏得本界選舉。
lastVoteEpoch 記錄了在本輪投票中 receiver 投過票的 sender 的 currentEpoch。各 slave 節點獨立發起選舉,currentEpoch 是相同的,都在原來的基礎上加 1。

clusterNode *master = node->slaveof;
if (nodeIsMaster(node) || master == NULL || (!nodeFailed(master) && !force_ack))
{
    if (nodeIsMaster(node)) {
        serverLog(LL_WARNING,
                  "Failover auth denied to %.40s: it is a master node",
                  node->name);
    } else if (master == NULL) { 
        serverLog(LL_WARNING,
                  "Failover auth denied to %.40s: I don't know its master",
                  node->name);
    } else if (!nodeFailed(master)) { 
        serverLog(LL_WARNING,
                  "Failover auth denied to %.40s: its master is up",
                  node->name);
    }
    return;
}

sender 是個 master。
sender 是個沒有 master 的 slave。
sender 的 master 沒有 fail,且不是個 mf。

if (mstime() - node->slaveof->voted_time < server.cluster_node_timeout * 2)
{
    serverLog(LL_WARNING,
              "Failover auth denied to %.40s: "
              "can't vote about this master before %lld milliseconds",
              node->name,
              (long long) ((server.cluster_node_timeout*2) - (mstime() - node->slaveof->voted_time)));
    return;
}

兩次投票時間間隔不能少於 2 倍 的 cluster_node_timeout
這個裕量時間,使得獲得贏得選舉的 slave 將新的主從關係周知集羣其他節點,避免其他 slave 發起新一輪的投票。

uint64_t requestConfigEpoch = ntohu64(request->configEpoch);
unsigned char *claimed_slots = request->myslots;
for (j = 0; j < CLUSTER_SLOTS; j++) {
    if (bitmapTestBit(claimed_slots, j) == 0) continue;
    if (server.cluster->slots[j] == NULL ||
        server.cluster->slots[j]->configEpoch <= requestConfigEpoch)
    {
        continue;
    }
    /* If we reached this point we found a slot that in our current slots
         * is served by a master with a greater configEpoch than the one claimed
         * by the slave requesting our vote. Refuse to vote for this slave. */
    serverLog(LL_WARNING,
              "Failover auth denied to %.40s: "
              "slot %d epoch (%llu) > reqEpoch (%llu)",
              node->name, j,
              (unsigned long long) server.cluster->slots[j]->configEpoch,
              (unsigned long long) requestConfigEpoch);
    return;
}

sender 節點聲稱要接管的 slots,在 receiver 節點看來其中有個別 slot 原來負責節點的 configEpoch 要比 sender 的大,這說明 sender 看到的集羣消息太舊了,這可能是一個長時間下線又重新上線的節點。

在本輪選舉投票
clusterSendFailoverAuth(node);
server.cluster->lastVoteEpoch = server.cluster->currentEpoch;
node->slaveof->voted_time = mstime(); // 更新投票時間

clusterSendFailoverAuth 函數中發送 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 類型的 gossip 消息,這就算在本輪選舉中投票了,並記錄本輪投票的 epoch以及投票時間。

slave 統計選票

slave 接收到 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 類型的 gossip 消息,就算統計到一票。

else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK) { // slave 統計票數
    if (!sender) return 1;  /* We don't know that node. */
    /* We consider this vote only if the sender is a master serving
         * a non zero number of slots, and its currentEpoch is greater or
         * equal to epoch where this node started the election. */
    if (nodeIsMaster(sender) && sender->numslots > 0 &&
        senderCurrentEpoch >= server.cluster->failover_auth_epoch)
    {
        server.cluster->failover_auth_count++;
        /* Maybe we reached a quorum here, set a flag to make sure
             * we check ASAP. */
        clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER);
    }
}

sender 是個負責 slot 的 master 並且滿足 currentEpoch 的要求,那麼這張選票有效。出現 senderCurrentEpoch < server.cluster->failover_auth_epoch 的情況時有可能的,如果這張選票是上一輪選舉的獲得選票,就不能作數。
failover_auth_count 變量中記錄了 slave 在本輪選舉中獲得選票數目。

slave 做主從切換

void clusterHandleSlaveFailover(void) {
    // ...
    int needed_quorum = (server.cluster->size / 2) + 1; 
    if (server.cluster->failover_auth_count >= needed_quorum) {
        /* We have the quorum, we can finally failover the master. */
        serverLog(LL_WARNING,
                  "Failover election won: I'm the new master.");

        /* Update my configEpoch to the epoch of the election. */
        if (myself->configEpoch < server.cluster->failover_auth_epoch) {
            myself->configEpoch = server.cluster->failover_auth_epoch;
            serverLog(LL_WARNING,
                      "configEpoch set to %llu after successful failover",
                      (unsigned long long) myself->configEpoch);
        }

        /* Take responsability for the cluster slots. */
        clusterFailoverReplaceYourMaster();
    } else {
        clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_VOTES);
    }
}

slave 節點獲得足夠多選票後, 成爲新的 master 節點。
更新自己的 configEpoch 爲選舉協商的 failover_auth_epoch,這是本節點就獲得了最新當前集羣最大的 configEpoch,表明它看到的集羣信息現在是最新的。
最後調用 clusterFailoverReplaceYourMaster 函數取代下線主節點,成爲新的主節點,並向其他節點廣播這種變化。

void clusterFailoverReplaceYourMaster(void) {
    int j;
    clusterNode *oldmaster = myself->slaveof;

    if (nodeIsMaster(myself) || oldmaster == NULL) return;

    /* 1) Turn this node into a master. */
    /* 把 myself 標記爲 master,並從原 master 裏刪掉,更新原 master 的涉及 slave 的參數,
     * 如果 slave 數量爲0,去掉它的 CLUSTER_NODE_MIGRATE_TO 標記
     */
    clusterSetNodeAsMaster(myself);

    /* 取消主從複製過程,將當前節點升級爲主節點 *、
    replicationUnsetMaster();

    /* 2) Claim all the slots assigned to our master.
     * 接手老的 master 節點負責的槽位
     */
    for (j = 0; j < CLUSTER_SLOTS; j++) {
        if (clusterNodeGetSlotBit(oldmaster,j)) {
            clusterDelSlot(j);
            clusterAddSlot(myself,j);
        }
    }

    /* 3) Update state and save config. */
    clusterUpdateState();
    clusterSaveConfigOrDie(1);

    /* 4) Pong all the other nodes so that they can update the state
     *    accordingly and detect that we switched to master role. */
    clusterBroadcastPong(CLUSTER_BROADCAST_ALL);

    /* 5) If there was a manual failover in progress, clear the state. */
    resetManualFailover();
}

進行必要的 flag 設置和 slots 交接,向集羣廣播 PONG 消息,並進行善後處理。

集羣其他節點感知主從變化

if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG || type == CLUSTERMSG_TYPE_MEET) {
    // ...
    /* Check for role switch: slave -> master or master -> slave. */
    if (sender) {
        if (!memcmp(hdr->slaveof, CLUSTER_NODE_NULL_NAME, sizeof(hdr->slaveof)))
        {
            /* Node is a master. set master flag for sender */
            clusterSetNodeAsMaster(sender);
        }
        // ...
    }
    clusterNode *sender_master = NULL; /* Sender or its master if slave. */
    int dirty_slots = 0; /* Sender claimed slots don't match my view? */

    if (sender) {
        sender_master = nodeIsMaster(sender) ? sender : sender->slaveof;
        if (sender_master) {
            dirty_slots = memcmp(sender_master->slots, hdr->myslots, sizeof(hdr->myslots)) != 0;
        }
    }

    if (sender && nodeIsMaster(sender) && dirty_slots) 
        clusterUpdateSlotsConfigWith(sender,senderConfigEpoch,hdr->myslots);
    // ...
}

集羣中其他節點接收到 PONG 消息後,對 sender 進行正確的 role 標記,以某節點 D 爲例。
對於剛剛做完故障轉移的 slave,也即現在 master,在節點 D 看來它負責的 slot 是空的,所以 dirty_slots 爲 1。
之後調用 clusterUpdateSlotsConfigWith 函數處理 slots 的 dirty diff 信息。
至此 failover 的邏輯就已經基本完成。

主動 failover

除了上面的發現故障後集羣自動 failover,也可以進行主動的主從切換。

slave 節點接受 cluster failover 命令

主動 failover 是通過 redis 命令實現的,命令格式爲 CLUSTER FAILOVER [FORCE|TAKEOVER],該命令使用詳情可以參考這篇文檔

#define CLUSTER_MF_TIMEOUT 5000 
else if (!strcasecmp(c->argv[1]->ptr,"failover") && (c->argc == 2 || c->argc == 3)){
    /* CLUSTER FAILOVER [FORCE|TAKEOVER] */
    int force = 0, takeover = 0;

    if (c->argc == 3) {
        /* 不與 master 溝通,主節點也不會阻塞其客戶端,需要經過選舉 */
        if (!strcasecmp(c->argv[2]->ptr,"force")) {         
            force = 1;
        /* 不與 master 溝通,不經過選舉 */
        } else if (!strcasecmp(c->argv[2]->ptr,"takeover")) {
            takeover = 1;
            force = 1; /* Takeover also implies force. */
        /* 與 master 溝通,需要經過選舉 */
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    // ...
    server.cluster->mf_end = mstime() + CLUSTER_MF_TIMEOUT; // mf 的超時時間爲 5s
}

cluster failover 命令有三種不同的選項,各有不同的含義,如上面註釋所說。takeover 變量標記是否要經過選舉, force 變量標記是否需要與 master 溝通。

另外,mf 過程有一個過期時間,目前定義爲 5s,同時, mf_end 也表示現在正在做 mf。
不同的選項有不同的處理方式,如下,

if (takeover) {
    // takeover 不會做任何初始化校驗。
    // 不經過其他節點選舉協商,直接將該節點的 current epoch 加 1,然後廣播這個新的配置
    serverLog(LL_WARNING,"Taking over the master (user request).");
    clusterBumpConfigEpochWithoutConsensus();
    clusterFailoverReplaceYourMaster();
} else if (force) {
    /* If this is a forced failover, we don't need to talk with our
     * master to agree about the offset. We just failover taking over
     * it without coordination. */
    serverLog(LL_WARNING,"Forced failover user request accepted.");
    server.cluster->mf_can_start = 1;// 可以直接開始選舉過程
} else {
    serverLog(LL_WARNING,"Manual failover user request accepted.");
    clusterSendMFStart(myself->slaveof); // 發送帶有 CLUSTERMSG_TYPE_MFSTART 標記的 gossip 包(只有消息頭)給我的 master
}

takeover 方式最爲粗暴,slave 節點不發起選舉,而是直接將自己升級爲master,接手原主節點的槽位,增加自己的 configEpoch 後更新配置。clusterFailoverReplaceYourMaster 的邏輯在前面講過,只有在本輪選舉中獲得足夠多的選票纔會調用該函數。

force 方式表示可以直接開始選舉過程,選舉過程也在前面說過了。

現在來看看默認方式,處理邏輯爲 clusterSendMFStart 函數。該函數主要邏輯就是發送向要做 failover 的 slave 的 master 發送 CLUSTERMSG_TYPE_MFSTART 類型的 gossip 消息。

master 節點做 mf 準備

else if (type == CLUSTERMSG_TYPE_MFSTART) {
    /* This message is acceptable only if I'm a master and the sender
     * is one of my slaves. */
    if (!sender || sender->slaveof != myself) return 1;
    /* Manual failover requested from slaves.
     * Initialize the state accordingly.
     * master 收到消息,重置 mf 狀態
     */
    resetManualFailover();
    server.cluster->mf_end = mstime() + CLUSTER_MF_TIMEOUT;
    server.cluster->mf_slave = sender;
    pauseClients(mstime()+(CLUSTER_MF_TIMEOUT*2)); // 阻塞客戶端 10s
    serverLog(LL_WARNING,"Manual failover requested by slave %.40s.",
              sender->name);
}

resetManualFailover 函數中重置與 mf 相關的參數,表示這是一次新的 mf。

設置 mf_end,將它的 master 指向 sender(就是那個搞事情的 slave),同時阻塞 client 10s 鍾。

隨後,標記在做 mf 的 master 發送 PING 信息時 hdr 會帶上 CLUSTERMSG_FLAG0_PAUSED 標記。

void clusterBuildMessageHdr(clusterMsg *hdr, int type) {
    // ...
      /* Set the message flags. */
    if (nodeIsMaster(myself) && server.cluster->mf_end)
        hdr->mflags[0] |= CLUSTERMSG_FLAG0_PAUSED;
    // ...
}

mflags 記錄與 mf 相關的 flag。

slave 處理

獲得 master 的 repl offset

slave 節點處理帶有 CLUSTERMSG_FLAG0_PAUSED 標記的 gossip 消息。

int clusterProcessPacket(clusterLink *link) {
    // ...
    sender = clusterLookupNode(hdr->sender);
    if (sender && !nodeInHandshake(sender)) { 
        // ...
        if (server.cluster->mf_end && // 處於 mf 狀態
            nodeIsSlave(myself) &&   // 我是 slave
            myself->slaveof == sender && // 我的 master 是 sender
            hdr->mflags[0] & CLUSTERMSG_FLAG0_PAUSED &&
            server.cluster->mf_master_offset == 0) // 還沒有正式開始時,mf_master_offset 設置爲 0
        {
            server.cluster->mf_master_offset = sender->repl_offset; // 從 sender 獲得 repl_offset
            serverLog(LL_WARNING,
                      "Received replication offset for paused "
                      "master manual failover: %lld",
                      server.cluster->mf_master_offset);
        }
    }
    // ...
}

對於那個發起 failover 的 slave,記下其 master 的 repl_offset,如果之前還沒有記錄下的話。

向 maser 追平 repl offset
void clusterCron(void) {
    // ...
    if (nodeIsSlave(myself)) {
        clusterHandleManualFailover();
        // ...
    }
    // ...
}

void clusterHandleManualFailover(void) {
    /* Return ASAP if no manual failover is in progress. */
    if (server.cluster->mf_end == 0) return;

    /* If mf_can_start is non-zero, the failover was already triggered so the
     * next steps are performed by clusterHandleSlaveFailover(). */
    if (server.cluster->mf_can_start) return;

    if (server.cluster->mf_master_offset == 0) return; /* Wait for offset... */

    if (server.cluster->mf_master_offset == replicationGetSlaveOffset()) {
        /* Our replication offset matches the master replication offset
         * announced after clients were paused. We can start the failover. */
        server.cluster->mf_can_start = 1;
        serverLog(LL_WARNING,
                  "All master replication stream processed, "
                  "manual failover can start.");
    }
}

clusterCron 函數裏有 clusterHandleManualFailover 的邏輯。

mf_end 爲 0,說明此時沒有 mf 發生。

mf_can_start 非 0 值,表示現在可以此 slave 可以發起選舉了。

mf_master_offset 爲 0,說明現在還沒有獲得 master 的複製偏移量,需要等一會兒。當 mf_master_offset 值等於 replicationGetSlaveOffset 函數的返回值時,把 mf_can_start 置爲 1。另外,應該記得,使用帶有 force 選項的 CLUSTER FAILOVER 命令,直接就會把 mf_can_start 置爲 1,而 replicationGetSlaveOffset 函數的作用就是檢查當前的主從複製偏移量,也就是說主從複製偏移量一定要達到 mf_master_offset 時,slave 纔會發起選舉,即默認選項有一個追平 repl offset 的過程。

其他一些選舉什麼的流程跟被動 failover 沒有區別。

過期清理 mf

主從節點在週期性的clusterCron 中都有一個檢查本次 mf 是否過期的函數。

void manualFailoverCheckTimeout(void) {
    if (server.cluster->mf_end && server.cluster->mf_end < mstime()) {
        serverLog(LL_WARNING,"Manual failover timed out.");
        resetManualFailover();
    }
}

void resetManualFailover(void) {
    if (server.cluster->mf_end && clientsArePaused()) {
        server.clients_pause_end_time = 0;
        clientsArePaused(); /* Just use the side effect of the function. */
    }
    server.cluster->mf_end = 0; /* No manual failover in progress. */
    server.cluster->mf_can_start = 0;
    server.cluster->mf_slave = NULL;
    server.cluster->mf_master_offset = 0;
}

如果過期沒有做 mf ,那麼就會重置它的相關參數。

附錄

epoch 概念

在 Redis cluster 裏 epoch 是個非常重要的概念,類似於 raft 算法中的 term 概念。Redis cluster 裏主要是兩種:currentEpoch 和 configEpoch。

currentEpoch

這是一個集羣狀態相關的概念,可以當做記錄集羣狀態變更的遞增版本號。每個集羣節點,都會通過server.cluster->currentEpoch 記錄當前的 currentEpoch。

集羣節點創建時,不管是主節點還是從節點,都置currentEpoch 爲 0。當前節點接收到來自其他節點的包時,如果發送者的currentEpoch(消息頭部會包含發送者的currentEpoch)大於當前節點的currentEpoch,那麼當前節點會更新 currentEpoch 爲發送者的 currentEpoch。因此,集羣中所有節點的currentEpoch最終會達成一致,相當於對集羣狀態的認知達成了一致。

currentEpoch 作用在於,集羣狀態發生改變時,某節點會先增加自身 currentEpoch 的值,然後向集羣中其他節點徵求同意,以便執行某些動作。目前,僅用於 slave 節點的故障轉移流程,在上面分析中也看到了,在發起選舉之前,slave 會增加自己的 currentEpoch,並且得到的 currentEpoch 表示這一輪選舉的 voteEpoch,當獲得了足夠多的選票後纔會執行故障轉移。

configEpoch

這是一個集羣節點配置相關的概念,每個集羣節點都有自己獨一無二的 configepoch。所謂的節點配置,實際上是指節點所負責的 slot 信息。

configEpoch 主要用於解決不同的節點就 slot 歸屬認知發生衝突的情況。公說公有理婆說婆有理,到底聽誰的,configEpoch 越大,看到的集羣節點配置信息越新,就越有話語權。對於衝突的情況,後面會有博客進行詳細分析。

以下幾種情況 configEpoch 會更新:

  1. 新節點加入;
  2. 槽節點映射衝突檢測;(slot 歸屬變更)
  3. 從節點投票選舉衝突檢測。(主從切換)

遞增 node epoch 稱爲 bump epoch。

關於 configEpoch 有三個原則:

  1. 如果 epoch 不變, 集羣就不應該有變更(包括選舉和遷移槽位)。
  2. 每個節點的 node epoch 都是獨一無二的。
  3. 擁有越高 epoch 的節點, 集羣信息越新。

clusterUpdateState 函數邏輯

#define CLUSTER_MAX_REJOIN_DELAY 5000
#define CLUSTER_MIN_REJOIN_DELAY 500
#define CLUSTER_WRITABLE_DELAY 2000
void clusterUpdateState(void) {
    // ...
    static mstime_t among_minority_time;
    static mstime_t first_call_time = 0;
    server.cluster->todo_before_sleep &= ~CLUSTER_TODO_UPDATE_STATE;
    
    /* 時間從第一次調用該函數算起,是爲了跳過 DB load 時間。
     * cluster 啓動時,狀態爲 CLUSTER_FAIL,
     * 這裏要等待一定的時間(2s)讓 cluster 變爲 CLUSTER_OK 狀態。
     */
    if (first_call_time == 0) first_call_time = mstime();
    if (nodeIsMaster(myself) &&
        server.cluster->state == CLUSTER_FAIL &&
        mstime() - first_call_time < CLUSTER_WRITABLE_DELAY) return;
    
    /* 先假設集羣狀態爲 CLUSTER_OK,
     * 然後遍歷 16384 個 slot,如果發現有 slot 被有被接管,
     * 或者接管某 slot 的 node 是 fail 狀態,那麼把集羣設置爲 CLUSTER_FAIL,退出循環
     */
    new_state = CLUSTER_OK;
    if (server.cluster_require_full_coverage) {
        for (j = 0; j < CLUSTER_SLOTS; j++) {
            if (server.cluster->slots[j] == NULL ||
                server.cluster->slots[j]->flags & (CLUSTER_NODE_FAIL))
            {
                new_state = CLUSTER_FAIL;
                break;
            }
        }
    }
    {
       /* 計算 cluster size,計數的是那些至少負責一個 slot 的 node
        * 計算 reachable_masters,計數基於 cluster size,
        * 加入篩選條件(不帶有 CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL) 標記
        */
        dictIterator *di;
        dictEntry *de;
        server.cluster->size = 0;
        di = dictGetSafeIterator(server.cluster->nodes);
        while((de = dictNext(di)) != NULL) {
            clusterNode *node = dictGetVal(de);

            if (nodeIsMaster(node) && node->numslots) {
                server.cluster->size++;
                if ((node->flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) == 0)
                    reachable_masters++;
            }
        }
        dictReleaseIterator(di);
    }
    {
        /* 如果 reachable_masters 不到 cluster size 一半(a minority partition),
         * 就將集羣標記爲 CLUSTER_FAIL 
         */
        int needed_quorum = (server.cluster->size / 2) + 1;
        if (reachable_masters < needed_quorum) {
            new_state = CLUSTER_FAIL;
            among_minority_time = mstime();
        }
    }
    
    if (new_state != server.cluster->state) {
        mstime_t rejoin_delay = server.cluster_node_timeout;

        if (rejoin_delay > CLUSTER_MAX_REJOIN_DELAY)
            rejoin_delay = CLUSTER_MAX_REJOIN_DELAY;
        if (rejoin_delay < CLUSTER_MIN_REJOIN_DELAY)
            rejoin_delay = CLUSTER_MIN_REJOIN_DELAY;
        /* 處於 minority partition 的時間沒有超過 cluster_node_timeout,
         * 那麼此次不更新集羣狀態。
         */
        if (new_state == CLUSTER_OK &&
            nodeIsMaster(myself) &&
            mstime() - among_minority_time < rejoin_delay)
        {
            return;
        }

        /* Change the state and log the event. */
        serverLog(LL_WARNING,"Cluster state changed: %s",
            new_state == CLUSTER_OK ? "ok" : "fail");
        server.cluster->state = new_state;
    }

4. 參考

Redis源碼解析:27集羣(三)主從複製、故障轉移

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章