天天看點

[redis 源碼走讀] sentinel 哨兵 - 主客觀下線

redis 哨兵叢集有 3 個角色:sentinel/master/slave,每個角色都可能出現故障,故障轉移主要針對 master,而且故障轉移是個複雜的工作流程。在分布式系統中,多個節點要保證資料一緻性,需要互相通信協調,要經曆幾個環節:

master 主觀下線 --> master 客觀下線 --> 投票選舉 leader --> leader 執行故障轉移。

本章重點走讀 redis 源碼,了解 sentinel 檢測 master 節點的主客觀下線流程。

🔥 文章來源 《[redis 源碼走讀] sentinel 哨兵 - 主客觀下線》

1. 故障轉移流程

  1. sentinel 時鐘定時檢查監控的各個 redis 執行個體角色,是否通信異常。
  2. 發現 master 主觀下線。
  3. 向其它 sentinel 節點詢問它們是否也檢測到該 master 主觀下線。
  4. sentinel 通過詢問,确認 master 客觀下線。
  5. 進入選舉環節,sentinel 向其它 sentinel 節點拉票,希望它們選自己為代表進行故障轉移。
  6. 少數服從多數,當超過法定 sentinel 個數選擇某個 sentinel 為代表。
  7. sentinel 代表執行故障轉移。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    ...
    /* 檢查 sentinel 是否處在異常狀态,例如本地時間忽然改變,因為心跳通信等,依賴時間。*/
    if (sentinel.tilt) {
        if (mstime() - sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING, "-tilt", NULL, "#tilt mode exited");
    }

    /* 檢查所有節點類型 sentinel/master/slave,是否主觀下線。*/
    sentinelCheckSubjectivelyDown(ri);
    ...
    if (ri->flags & SRI_MASTER) {
        /* 檢查 master 是否客觀下線。 */
        sentinelCheckObjectivelyDown(ri);
        /* 是否滿足故障轉移條件,開啟故障轉移。 */
        if (sentinelStartFailoverIfNeeded(ri))
            /* 滿足條件,進入故障轉移環節,馬上向其它 sentinel 節點選舉拉票。 */
            sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_ASK_FORCED);
        /* 通過狀态機,處理故障轉移對應各個環節。 */
        sentinelFailoverStateMachine(ri);
        /* 定時向其它 sentinel 節點詢問 master 主觀下線狀況或選舉拉票。 */
        sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_NO_FLAGS);
    }
}
           

2. 故障發現

[redis 源碼走讀] sentinel 哨兵 - 主客觀下線

2.1. 主觀下線

主要檢查節點間的 心跳 通信是否正常。

  • 檢測異步連結是否逾時,逾時則關閉連結。
  • 檢測心跳是否逾時,逾時則辨別主觀下線,否則恢複正常。
  • master 角色誤報,逾時辨別主觀下線。
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;

    /* 通過心跳通信間隔判斷掉線邏輯。 */
    if (ri->link->act_ping_time)
        elapsed = mstime() - ri->link->act_ping_time;
    else if (ri->link->disconnected)
        elapsed = mstime() - ri->link->last_avail_time;

    /* tcp 異步連結通信逾時關閉對應連結。 */
    ...

    /* 主觀下線
     * 1. 心跳通信逾時。
     * 2. 主服務節點卻上報從服務角色,異常情況逾時。 */
    if (elapsed > ri->down_after_period ||
        (ri->flags & SRI_MASTER &&
         ri->role_reported == SRI_SLAVE &&
         mstime() - ri->role_reported_time >
             (ri->down_after_period + SENTINEL_INFO_PERIOD * 2))) {
        /* Is subjectively down */
        if ((ri->flags & SRI_S_DOWN) == 0) {
            sentinelEvent(LL_WARNING, "+sdown", ri, "%@");
            ri->s_down_since_time = mstime();
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        /* 被辨別為主觀下線的節點,恢複正常,去掉主觀下線辨別。*/
        if (ri->flags & SRI_S_DOWN) {
            sentinelEvent(LL_WARNING, "-sdown", ri, "%@");
            ri->flags &= ~(SRI_S_DOWN | SRI_SCRIPT_KILL_SENT);
        }
    }
}
           

2.2. 客觀下線

  • 詢問主觀下線。

當 sentinel 檢測到 master 主觀下線,它會詢問其它 sentinel(發送 IS-MASTER-DOWN-BY-ADDR 請求):是否也檢測到該 master 已經主觀下線了。

SENTINEL IS-MASTER-DOWN-BY-ADDR

指令有兩個作用:

  1. 詢問其它 sentinel 節點,該 master 是否已經主觀下線。指令最後一個參數為 <*>。
  2. 确認 master 客觀下線,目前 sentinel 向其它 sentinel 拉選票,讓其它 sentinel 選自己為 “代表”。指令最後一個參數為 <sentinel_runid>,sentinel 自己的 runid。

這裡是 sentinel 發現了 master 主觀下線,是以先進入詢問環節,再進行選舉拉票。

# is-master-down-by-addr 指令格式。
SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> <*>
           
/* If we think the master is down, we start sending
 * SENTINEL IS-MASTER-DOWN-BY-ADDR requests to other sentinels
 * in order to get the replies that allow to reach the quorum
 * needed to mark the master in ODOWN state and trigger a failover. */
#define SENTINEL_ASK_FORCED (1 << 0)

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetIterator(master->sentinels);
    while ((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        ...
        /* Only ask if master is down to other sentinels if:
         *
         * 1) We believe it is down, or there is a failover in progress.
         * 2) Sentinel is connected.
         * 3) We did not receive the info within SENTINEL_ASK_PERIOD ms. */
        if ((master->flags & SRI_S_DOWN) == 0) continue;
        if (ri->link->disconnected) continue;
        if (!(flags & SENTINEL_ASK_FORCED) &&
            mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
            continue;

        /* 當 sentinel 檢測到 master 主觀下線,那麼參數發送 "*",等待确認客觀下線,
         * 當确認客觀下線後,再進入選舉環節。sentinel 再向其它 sentinel 發送自己的 runid,去拉票。*/
        ll2string(port, sizeof(port), master->addr->port);
        retval = redisAsyncCommand(ri->link->cc,
                                   sentinelReceiveIsMasterDownReply, ri,
                                   "%s is-master-down-by-addr %s %s %llu %s",
                                   sentinelInstanceMapCommand(ri, "SENTINEL"),
                                   master->addr->ip, port,
                                   sentinel.current_epoch,
                                   (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ? sentinel.myid : "*");
        if (retval == C_OK) ri->link->pending_commands++;
    }
    dictReleaseIterator(di);
}
           
  • 其它 sentinel 接收指令。
void sentinelCommand(client *c) {
    ...
    else if (!strcasecmp(c->argv[1]->ptr, "is-master-down-by-addr")) {
        ...
        /* 其它 sentinel 接收到詢問指令,根據 ip 和 端口查找對應的 master。 */
        ri = getSentinelRedisInstanceByAddrAndRunID(
            sentinel.masters, c->argv[2]->ptr, port, NULL);

        /* 目前 sentinel 如果沒有處于異常保護狀态,而且也檢測到詢問的 master 已經主觀下線了。 */
        if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER))
            isdown = 1;

        /* 詢問 master 主觀下線指令參數是 *,選舉投票參數是請求的 sentinel 的 runid。*/
        if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr, "*")) {
            leader = sentinelVoteLeader(ri, (uint64_t)req_epoch, c->argv[5]->ptr, &leader_epoch);
        }

        /* 根據詢問主觀下線或投票選舉業務确定回複的内容參數。 */
        addReplyArrayLen(c, 3);
        addReply(c, isdown ? shared.cone : shared.czero);
        addReplyBulkCString(c, leader ? leader : "*");
        addReplyLongLong(c, (long long)leader_epoch);
        if (leader) sdsfree(leader);
    }
    ...
}
           
  • 目前 sentinel 接收指令回複。

目前 sentinel 接收到詢問的回複,如果确認該 master 已經主觀下線,那麼将其辨別為

SRI_MASTER_DOWN

/* Receive the SENTINEL is-master-down-by-addr reply, see the
 * sentinelAskMasterStateToOtherSentinels() function for more information. */
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
    ...
    if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&
        r->element[0]->type == REDIS_REPLY_INTEGER &&
        r->element[1]->type == REDIS_REPLY_STRING &&
        r->element[2]->type == REDIS_REPLY_INTEGER) {
        ri->last_master_down_reply_time = mstime();
        if (r->element[0]->integer == 1) {
            /* ri sentinel 回複,也檢測到該 master 節點已經主觀下線。 */
            ri->flags |= SRI_MASTER_DOWN;
        } else {
            ri->flags &= ~SRI_MASTER_DOWN;
        }
        ...
    }
}
           
  • 确認客觀下線

當 >= 法定個數(quorum)的 sentinel 節點确認該 master 主觀下線,那麼辨別目前主觀下線的 master 被辨別為客觀下線。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;

    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        quorum = 1; /* the current sentinel. *
        /* Count all the other sentinels. */
        di = dictGetIterator(master->sentinels);
        while ((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            /* 該 ri 檢測到 master 主觀掉線。 */
            if (ri->flags & SRI_MASTER_DOWN) {
                quorum++;
            }
        }
        dictReleaseIterator(di);
        /* 是否滿足目前 sentinel 配置的法定個數:quorum。 */
        if (quorum >= master->quorum) odown = 1;
    }

    /* Set the flag accordingly to the outcome. */
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            sentinelEvent(LL_WARNING, "+odown", master, "%@ #quorum %d/%d",
                          quorum, master->quorum);
            master->flags |= SRI_O_DOWN;
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
            sentinelEvent(LL_WARNING, "-odown", master, "%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}
           

3. 開啟故障轉移

當 sentinel 檢測到某個 master 客觀下線,可以進入開啟故障轉移流程了。

/* 定時檢查 master 故障情況情況。 */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    ...
    if (ri->flags & SRI_MASTER) {
        /* 檢查 master 是否客觀下線。 */
        sentinelCheckObjectivelyDown(ri);
        /* 是否滿足故障轉移條件,開啟故障轉移。 */
        if (sentinelStartFailoverIfNeeded(ri))
            /* 滿足條件,進入故障轉移環節,馬上向其它 sentinel 節點選舉拉票。 */
            sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_ASK_FORCED);
        /* 通過狀态機,處理故障轉移對應各個環節。 */
        sentinelFailoverStateMachine(ri);
        /* 定時向其它 sentinel 節點詢問 master 主觀下線狀況或選舉拉票。 */
        sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_NO_FLAGS);
    }
}

/* 是否滿足故障轉移條件,開啟故障轉移。 */
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
    /* master 客觀下線。 */
    if (!(master->flags & SRI_O_DOWN)) return 0;

    /* 目前 master 沒有處在故障轉移過程中。 */
    if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

    /* 兩次故障轉移,需要有一定的時間間隔。
     * 1. 目前 sentinel 滿足了故障轉移條件。
     * 2. 目前 sentinel 接收到其它 sentinel 的拉票,也設定了 failover_start_time,說明
     *    其它 sentinel 先開啟了故障轉移,為了避免沖突,需要等待一段時間。*/
    if (mstime() - master->failover_start_time < master->failover_timeout * 2) {
        ...
        return 0;
    }

    sentinelStartFailover(master);
    return 1;
}

/* 開啟故障轉移,進入投票環節。 */
void sentinelStartFailover(sentinelRedisInstance *master) {
    ...
    /* 目前 master 開啟故障轉移。 */
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
    /* 目前 master 故障轉移正在進行中。 */
    master->flags |= SRI_FAILOVER_IN_PROGRESS;
    /* 開始一輪選舉,選舉紀元(計數器 + 1)。*/
    master->failover_epoch = ++sentinel.current_epoch;
    ...
    /* 記錄故障轉移開啟時間。 */
    master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}
           

4. 參考

  • raft 論文翻譯
  • raft 算法官網
  • raft 算法原理
  • Redis Sentinel 高可用原理
  • Redis源碼解析:21sentinel(二)定期發送消息、檢測主觀下線

如果文章不錯,給個點贊呗 ~ 謝謝。👍

繼續閱讀