來聊聊 Redis 哨兵如何主觀認定下線
上一篇我們將redis哨兵初始化分析完成,接下來我們就可以開始分析redis如何通過raft完成哨兵leader選舉,并完成主從節點故障轉移工作,因為篇幅原因,關于redis故障轉移的內容將分為兩個篇章,而這篇討論的是哨兵如何完成主觀下線的判定。
一、詳解哨兵的主觀認定下線的流程
1. 簡述raft協議
在正式開始后續的文章討論前,我們先來簡單介紹一下分布式共識raft協議,這個是分布式系統中保證高可用的選舉協議。該協議將所有分布式系統的節點分為3個角色:
- leader: 當前分布式集群中的主節點,即集群中的領導角色,負責承載當前系統中的核心業務。
- follower: 從節點,作為leader節點的跟隨節點。
- candidate:一旦leader發生故障被slave感知,那么這些節點會將自身角色轉為Canadian,并發起選舉,得票數最多的Canadian將轉為新的leader。
正常情況下,被選舉為leader的節點會向follower節點發送心跳,告知自己當前還未下線:
一旦follower感知到leader下線,就會將自己身份轉換為candidate,通過選舉競爭leader,每一個candidate都會給自己投一票然后向其他選舉節點獲取選票,在選舉計時時間以內,超過半數以上得票的candidate就會被選舉為新的leader節點,其余candidate收到此leader的心跳消息后身份就會轉為最新leader節點的follower:
2. redis中的raft協議與核心流程
與傳統raft協議實現有所不同,redis哨兵在未發生選舉時地位是對等并無leader和follower等概念,只有感知到監聽主節點下線時才會借助raft的協議觸發選舉,選舉出一個哨兵作為leader完成故障轉移之后,leader哨兵會再次回歸對等地位。
redis哨兵執行的生命周期還是交由時間事件定時執行,它的整體工作流程為:
- 檢查自己所監聽的master連接情況,檢查是否與監聽的master節點斷開連接,如果發現連接斷開則進行斷線重連。
- 再對master節點進行消息通信,這期間哨兵會發送ping與主節點保持通信,再發送info請求master最新信息。
- 一旦發現master長時間未與自己進行心跳,則主觀視為監聽節點下線,并通過頻道告知其他哨兵獲取其他哨兵對于主節點的結果判斷。
- 如果哨兵一致認定當前監聽節點下線,則會選舉出一個哨兵作為leader進行故障轉移,即在所有從節點中找到一個優先級最高的從節點作為新的master。
對此我我們給出程序執行的入口來查看這塊核心的主流程,可以看到serverCron定時執行的時間時間會每100ms執行一次哨兵的時間事件sentinelTimer,對此我們不妨步入sentinelTimer查看實現細節:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//......
//100ms一次,如果是哨兵模式則運行哨兵的時間事件
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}
//......
}
步入sentinelTimer,該函數會先判斷哨兵執行時間是否過長,如果發現時鐘回撥或者長時間才進行處理則觸發tilt模式,該模式下哨兵只會定期發送和接收消息,不做其他任務處理。
再調用sentinelHandleDictOfRedisInstances遍歷哨兵中的master開始開始進行我們上述所說的判斷與master連接狀態、進行通信和info消息獲取、主觀下線判斷、客觀下線判斷、故障轉移。
完成這些步驟之后,更新下一次的執行時間,可以看到redis對于這個時間設置做了一個巧妙的設計,我們都知道哨兵判定節點下線后就會發起選舉,為了避免哨兵集群所有節點同時發起選舉投票從而得到相同票數的情況而導致本輪選舉失敗而進行反復選舉的情況,redis會在哨兵本次時間事件執行完成之后,通過隨機種子調整哨兵時間下一次的執行時機,盡可能避免選舉時反復出現選票一致的情況:
對此我們也給出sentinelTimer的實現細節:
void sentinelTimer(void) {
// 前置檢查事件定期任務是否因為系統負載過大或者各種原因導致時鐘回撥,或者處理過長,進入tilt模式,該模式哨兵只會定期發送和接收命令
sentinelCheckTiltCondition();
//監聽的master節點作為參數傳入,進行逐個通信處理
sentinelHandleDictOfRedisInstances(sentinel.masters);
//......
//隨機調整執行頻率避免同時執行,確保提高選舉一次性成功的概率
server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
我們再次步入核心方法sentinelHandleDictOfRedisInstances它會遍歷每一個master節點,然后調用sentinelHandleRedisInstance處理每一個哨兵所監聽的master實例:
void sentinelHandleDictOfRedisInstances(dict *instances) {
//.......
//迭代出每一個master實例再對主節點進行處理
di = dictGetIterator(instances);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
//迭代并處理每一個master實例
sentinelHandleRedisInstance(ri);
//.......
}
//.......
}
步入sentinelHandleRedisInstance即可看到我們上文所說的而核心邏輯,它對于筆者上文的每一個流程都做了抽象,可以看到它會先嘗試和斷線的master建立連接,然后發送ping和info獲取master節點的確認和master實時消息,最后在檢查master是否超時未回復發起主觀下線,然后再發起客觀下線請求確認其他哨兵回復。 最后明確master節點確實下線之后再發起選舉,得出leader后由leader進行故障轉移,挑選出新的master承載核心業務。
//這個入參包含恰哨兵實例和當前主節點的從節點信息
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* ========== MONITORING HALF ============ */
/* Every kind of instance */
//1. 嘗試和斷連的實例重新建立連接
sentinelReconnectInstance(ri);
//2. 向實例發送ping和info等命令
sentinelSendPeriodicCommands(ri);
//......
/* Every kind of instance */
//3. 主觀判斷是否下線
sentinelCheckSubjectivelyDown(ri);
/* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
}
/* Only masters */
if (ri->flags & SRI_MASTER) {
//4. 檢查其當前是否客觀下線
sentinelCheckObjectivelyDown(ri);
//5. 判斷是否要進行故障切換,如果要啟動故障切換,則獲取其他哨兵對于該節點的判斷
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
//6. 執行故障切換
sentinelFailoverStateMachine(ri);
//7. 再次獲取哨兵實例對主節點狀態的判斷
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
3. 斷線重連檢查
基于上文我們了解哨兵時間事件執行的大體流程,接下來我們會針對每一個流程進行詳細的分析,首先我們先來了解一下對于斷線重連檢查方法,對于斷線重連檢查,redis哨兵通過兩個異步的連接進行處理,它通過cc這個異步連接和master建立通信完成PING和INFO的消息發送,再通過pc處理各種廣播消息:
我們都知道redis將哨兵中每一個維護的master封裝成sentinelRedisInstance ,這其中就有cc和pc兩個連接指針,用于和當前哨兵建立連接和通信:
typedef struct sentinelRedisInstance {
//......
//異步發送命令的連接
redisAsyncContext *cc; /* Hiredis context for commands. */
//pub/sub發送通道,用于處理頻道消息的收發
redisAsyncContext *pc;
//......
}
此時我們再來查看sentinelReconnectInstance方法內部,即非常直觀了解到其內部對于斷開或者為空的連接會調用redisAsyncConnectBind方法通過外部遍歷master傳入的master結構體信息發起異步連接重建:
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
if (!(ri->flags & SRI_DISCONNECTED)) return;
/* Commands connection. */
//如果命令指針cc為空,則進行一次異步重連
if (ri->cc == NULL) {
//基于外部遍歷傳入的master指針進行異步重連
ri->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
//如果連接失敗則調用sentinelKillLink銷毀該連接
if (ri->cc->err) {
sentinelEvent(REDIS_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
ri->cc->errstr);
sentinelKillLink(ri,ri->cc);
} else {
//......
}
}
//檢查發布訂閱pc,如果為空則將外部傳入的master信息通過異步的方式重新和頻道建立連接
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
ri->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
if (ri->pc->err) {
sentinelEvent(REDIS_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
ri->pc->errstr);
sentinelKillLink(ri,ri->pc);
} else {
//......
}
}
//......
}
4. 消息通信
完成連接重建之后,在所有連接正常的情況下,哨兵會檢查當前發送上次ping間隔是否超過指定間隔,如果是則通過cc指指針向master發送ping。 同理如果info消息超過發送間隔也會生成當前哨兵ip端口等基本信息通過cc通道發送給masrter:
對此我們給出命令定期發送函數sentinelSendPeriodicCommands的入口,可以看到它會依次檢查ping和hello消息的間隔邏輯,然后按需通過cc發送ping或者hello消息:
void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
//......
//和其他哨兵處理的邏輯
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
//......
} else if ((now - ri->last_pong_time) > ping_period) {//超過ping間隔發ping
sentinelSendPing(ri);
} else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {//超過pub間隔通過cc發送當前哨兵個人信息
sentinelSendHello(ri);
}
}
我們步入sentinelSendPing可以看到其內部邏輯比較簡單,通過cc發送ping然后更新上次發送ping的時間戳字段last_ping_time:
int sentinelSendPing(sentinelRedisInstance *ri) {
//通過cc異步命令接口發送ping
int retval = redisAsyncCommand(ri->cc,
sentinelPingReplyCallback, NULL, "PING");
//如果得到正常響應則更新last_ping_time
if (retval == REDIS_OK) {
ri->pending_commands++;
if (ri->last_ping_time == 0) ri->last_ping_time = mstime();
return 1;
} else {
return 0;
}
}
同理我們給出sentinelSendHello函數,可以看到其內部會組裝當前哨兵的ip和端口以及master的地址信息通過cc發送到__sentinel__:hello這個頻道中進行廣播:
int sentinelSendHello(sentinelRedisInstance *ri) {
//......
/* Format and send the Hello message. */
//將哨兵ip 端口以及master地址信息數據拼接到payload中
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
announce_ip, announce_port, server.runid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
//通過cc異步發送到__sentinel__:hello頻道中
retval = redisAsyncCommand(ri->cc,
sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
SENTINEL_HELLO_CHANNEL,payload);
if (retval != REDIS_OK) return REDIS_ERR;
ri->pending_commands++;
return REDIS_OK;
}
5. 判定主觀下線
然后就開始主觀下線的檢查,可以看到redis一旦發現master長時間未與當前哨兵進行通信,亦或者在很長一段時間都被報告為從節點,則將主觀判定其下線,再通過或預運算符將ri的flags標志位注明這個master已經主觀的被認定為下線。最后通過通過 +sdown這個channel 發送主觀下線的消息,讓他們各自檢查,從而開始后續客觀下線檢查及選舉和故障轉移等操作:
對應的我們也給出sentinelCheckSubjectivelyDown函數的實現,可以我們補充弄一下down_after_period 這個是就是決定Sentinel判斷實例進入主觀下線所需的時間長度,默認情況下是30000毫秒,如果需要修改我們可以在redis.conf中用down-after-milliseconds指定:
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
//......
//如果上一次到現在的間隔elapsed 大于down_after_period ,則當前哨兵會主觀認定其下線
if (elapsed > ri->down_after_period ||
//或者當前哨兵認定它是master而其他報告長時間的反饋都是從節點,則當前哨兵會主觀認定其下線
(ri->flags & SRI_MASTER &&
ri->role_reported == SRI_SLAVE &&
mstime() - ri->role_reported_time >
(ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
{
//通過 +sdown這個channel 發送主觀下線的消息
if ((ri->flags & SRI_S_DOWN) == 0) {
sentinelEvent(REDIS_WARNING,"+sdown",ri,"%@");
//設置當前監控的master實例為主觀下線
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN;
}
} else {
//......
}
}
二、小結
自此我們將redis哨兵主觀下線的核心流程分析完成,我們來簡單小結一下哨兵判斷主觀下線的流程:
- 哨兵實例隨機一個hz參數作為定時器執行間隔,即執行一個哨兵定時事件sentinelTimer,
- sentinelTimer會定期調用sentinelHandleDictOfRedisInstances遍歷檢查監控的master進行定時的交互。
- 哨兵實例定期發送ping和hello亦或者info請求給master。
- master超過down_after_period設置的時間沒有回應,或者其他角色長時間報告這個master已經是slave,則當前哨兵會主觀認定其下線,并將消息發送到+sdown中。
- 結束一次定時任務后,定時器sentinelTimer執行完后設置下一次隨機執行時間,保證在主觀與客觀認定master下線后通過隨機性提升選舉的效率。