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