redis 哨兵叢集有 3 個角色:sentinel/master/slave,每個角色都可能出現故障,故障轉移主要針對 master,而且故障轉移是個複雜的工作流程。在分布式系統中,多個節點要保證資料一緻性,需要互相通信協調,要經曆幾個環節:
master 主觀下線 --> master 客觀下線 --> 投票選舉 leader --> leader 執行故障轉移。
本章重點走讀 redis 源碼,了解 sentinel 檢測 master 節點的主客觀下線流程。
🔥 文章來源 《[redis 源碼走讀] sentinel 哨兵 - 主客觀下線》
1. 故障轉移流程
- sentinel 時鐘定時檢查監控的各個 redis 執行個體角色,是否通信異常。
- 發現 master 主觀下線。
- 向其它 sentinel 節點詢問它們是否也檢測到該 master 主觀下線。
- sentinel 通過詢問,确認 master 客觀下線。
- 進入選舉環節,sentinel 向其它 sentinel 節點拉票,希望它們選自己為代表進行故障轉移。
- 少數服從多數,當超過法定 sentinel 個數選擇某個 sentinel 為代表。
- 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. 故障發現

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
指令有兩個作用:
- 詢問其它 sentinel 節點,該 master 是否已經主觀下線。指令最後一個參數為 <*>。
- 确認 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(二)定期發送消息、檢測主觀下線
如果文章不錯,給個點贊呗 ~ 謝謝。👍