Redis 哨兵的服務框架
哨兵也是 Redis 服務器,只是它與我們平時提到的 Redis 服務器職能不同,哨兵負責監視普通的 Redis 服務器,提高一個服務器集群的健壯和可靠性。哨兵和普通的 Redis 服務器所用的是同一套服務器框架,這包括:網絡框架,底層數據結構,訂閱發布機制等。
從主函數開始,來看看哨兵服務器是怎么誕生,它在什么時候和普通的 Redis 服務器分道揚鑣:
int main(int argc, char **argv) { // 隨機種子,一般rand() 產生隨機數的函數會用到 srand(time(NULL)^getpid()); gettimeofday(&tv,NULL); dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid()); // 通過命令行參數確認是否啟動哨兵模式 server.sentinel_mode = checkForSentinelMode(argc,argv); // 初始化服務器配置,主要是填充redisServer 結構體中的各種參數 initServerConfig(); // 將服務器配置為哨兵模式,與普通的redis 服務器不同 /* We need to init sentinel right now as parsing the configuration file * in sentinel mode will have the effect of populating the sentinel * data structures with master nodes to monitor. */ if (server.sentinel_mode) { // initSentinelConfig() 只指定哨兵服務器的端口 initSentinelConfig(); initSentinel(); } ...... // 普通redis 服務器模式 if (!server.sentinel_mode) { ...... // 哨兵服務器模式 } else { // 檢測哨兵模式是否正常配置 sentinelIsRunning(); } ...... // 進入事件循環 aeMain(server.el); // 去除事件循環系統 aeDeleteEventLoop(server.el); return 0; }
在上面,通過判斷命令行參數來判斷 Redis 服務器是否啟用哨兵模式,會設置服務器參數結構體中的redisServer.sentinel_mode 的值。在上面的主函數調用了一個很關鍵的函數:initSentinel(),它完成了哨兵服務器特有的初始化程序,包括填充哨兵服務器特有的命令表,struct sentinel 結構體。
// 哨兵服務器特有的初始化程序 /* Perform the Sentinel mode initialization. */ void initSentinel(void) { int j; // 如果 redis 服務器是哨兵模式,則清空命令列表。哨兵會有一套專門的命令列表, // 這與普通的 redis 服務器不同 /* Remove usual Redis commands from the command table, then just add * the SENTINEL command. */ dictEmpty(server.commands,NULL); // 將sentinelcmds 命令列表中的命令填充到server.commands for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) { int retval; struct redisCommand *cmd = sentinelcmds+j; retval = dictAdd(server.commands, sdsnew(cmd->name), cmd); redisAssert(retval == DICT_OK); } /* Initialize various data structures. */ // sentinel.current_epoch 用以指定版本 sentinel.current_epoch = 0; // 哨兵監視的 redis 服務器哈希表 sentinel.masters = dictCreate(&instancesDictType,NULL); // sentinel.tilt 用以處理系統時間出錯的情況 sentinel.tilt = 0; // TILT 模式開始的時間 sentinel.tilt_start_time = 0; // sentinel.previous_time 是哨兵服務器上一次執行定時程序的時間 sentinel.previous_time = mstime(); // 哨兵服務器當前正在執行的腳本數量 sentinel.running_scripts = 0; // 腳本隊列 sentinel.scripts_queue = listCreate(); }
我們查看 struct redisCommand sentinelcmds 這個全局變量就會發現,它里面只有七個命令,難道哨兵僅僅提供了這種服務?為了能讓哨兵自動管理普通的 Redis 服務器,哨兵還添加了一個定時程序,我們從 serverCron() 定時程序中就會發現,哨兵的定時程序被調用執行了,這里包含了哨兵的主要工作:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ...... run_with_period(100) { if (server.sentinel_mode) sentinelTimer(); } }
定時程序
定時程序是哨兵服務器的重要角色,所做的工作主要包括:監視普通的 Redis 服務器(包括主機和從機),執行故障修復,執行腳本命令。
// 哨兵定時程序 void sentinelTimer(void) { // 檢測是否需要啟動sentinel TILT 模式 sentinelCheckTiltCondition(); // 對哈希表中的每個服務器實例執行調度任務,這個函數很重要 sentinelHandleDictOfRedisInstances(sentinel.masters); // 執行腳本命令,如果正在執行腳本的數量沒有超出限定 sentinelRunPendingScripts(); // 清理已經執行完腳本的進程,如果執行成功從腳本隊列中刪除腳本 sentinelCollectTerminatedScripts(); // 停止執行時間超時的腳本進程 sentinelKillTimedoutScripts(); // 為了防止多個哨兵同時選舉,故意錯開定時程序執行的時間。通過調整周期可以 // 調整哨兵定時程序執行的時間,即默認值REDIS_DEFAULT_HZ 加上一個任意值 server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ; }
哨兵與 Redis 服務器的互聯
每個哨兵都有一個 struct sentinel 結構體,里面維護了多個主機的連接,與每個主機連接的相關信息都存儲在 struct sentinelRedisInstance。透過這兩個結構體,很快就可以描繪出,一個哨兵服務器所維護的機器的信息:
typedef struct sentinelRedisInstance { ...... /* Master specific. */ // 其他正在監視此主機的哨兵 dict *sentinels; /* Other sentinels monitoring the same master. */ // 次主機的從機列表 dict *slaves; /* Slaves for this master instance. */ ...... // 如果是從機,master 則指向它的主機 struct sentinelRedisInstance *master; /* Master instance if it's slave. */ ...... } sentinelRedisInstance;
哨兵服務器所能描述的 Redis 信息:
可見,哨兵服務器連接(監視)了多台主機,多台從機和多台哨兵服務器。有這樣大概的脈絡,我們繼續往下看就會更有線索。
哨兵要監視 Redis 服務器,就必須連接 Redis 服務器。啟動哨兵的時候需要指定一個配置文件,程序初始化的時候會讀取這個配置文件,獲取被監視 Redis 服務器的 IP 地址和端口等信息。
或者
如果想要監視一個 Redis 服務器,可以在配置文件中寫入:
sentinel monitor <master-name> <ip> <redis-port> <quorum>
其中,master-name 是主機名,ip redis-port 分別是 IP 地址和端口,quorum 是哨兵用來判斷某個 Redis 服務器是否下線的參數,之后會講到。sentinelHandleConfiguration() 函數中,完成了對配置文件的解析和處理過程。
// 哨兵配置文件解析和處理
char *sentinelHandleConfiguration(char **argv, int argc) {
sentinelRedisInstance *ri;
if (!strcasecmp(argv[0],"monitor") && argc == 5) {
/* monitor <name> <host> <port> <quorum> */
int quorum = atoi(argv[4]);
// quorum >= 0
if (quorum <= 0) return "Quorum must be 1 or greater.";
if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
atoi(argv[3]),quorum,NULL) == NULL)
{
switch(errno) {
case EBUSY: return "Duplicated master name.";
case ENOENT: return "Can't resolve master instance hostname.";
case EINVAL: return "Invalid port number";
}
}
......
}
可以看到里面主要調用了 createSentinelRedisInstance() 函數。createSentinelRedisInstance() 函數的主要工作是初始化 sentinelRedisInstance 結構體。在這里,哨兵並沒有選擇立即去連接這指定的 Redis 服務器,而是將 sentinelRedisInstance.flag 標記 SRI_DISCONNECT,而將連接的工作丟到定時程序中去,可以聯想到,定時程序中肯定有一個檢測 sentinelRedisInstance.flag 的函數,如果發現連接是斷開的,會發起連接。這個策略和我們之前的講到的主從連接時候的策略是一樣的,是 Redis 的慣用手法。因為哨兵要和 Redis 服務器保持連接,所以必然會定時檢測和 Redis 服務器的連接狀態。
在定時程序的調用鏈中,確實發現了哨兵主動連接 Redis 服務器的過程:
sentinelTimer()->sentinelHandleRedisInstance()->sentinelReconnectInstance()
。
sentinelReconnectInstance() 負責連接被標記為 SRI_DISCONNECT 的 Redis 服務器。它對一個 Redis 服務器發起了兩個連接:
- 普通連接(sentinelRedisInstance.cc,Commands connection)
- 訂閱發布專用連接(sentinelRedisInstance.pc,publish connection)。為什么需要分這兩個連接呢?因為對於一個客戶端連接來說,redis 服務器要么專門處理普通的命令,要么專門處理訂閱發布命令,這在之前訂閱發布篇幅中專門有提及這個細節。
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
if (!(ri->flags & SRI_DISCONNECTED)) return;
/* Commands connection. */
if (ri->cc == NULL) {
ri->cc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
// 連接出錯
if (ri->cc->err) {
// 錯誤處理
} else {
// 此連接被綁定到redis 服務器的事件中心
......
}
}
// 此哨兵會訂閱所有主從機的hello 訂閱頻道,每個哨兵都會定期將自己監視的
// 服務器和自己的信息發送到主從服務器的hello 頻道,從而此哨兵就能發現其
// 他服務器,並且也能將自己的監測的數據散播到其他服務器。這就是redis 所
// 謂的auto discover.
/* Pub / Sub */
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
ri->pc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
// 連接出錯
if (ri->pc->err) {
// 錯誤處理
} else {
// 此連接被綁定到redis 服務器的事件中心
......
// 訂閱了ri 上的__sentinel__:hello 頻道
/* Now we subscribe to the Sentinels "Hello" channel. */
retval = redisAsyncCommand(ri->pc,
sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
SENTINEL_HELLO_CHANNEL);
......
}
}
Redis 在定時程序中會嘗試對所有的 master 作重連接。這里會有一個疑問,之前有提到從機(slave),哨兵又是在什么時候連接了從機和哨兵呢?
HELLO 命令
我們從上面 sentinelReconnectInstance() 的源碼得知,哨兵對於一個 Redis 服務器管理了兩個連接:普通命令連接和訂閱發布專用連接。其中,哨兵在初始化訂閱發布連接的時候,做了兩個工作:一是,向 Redis 服務器發送 SUBSCRIBE SENTINEL_HELLO_CHANNEL命令;二是,注冊了回調函數 sentinelReceiveHelloMessages()。稍稍理解大概可以畫出下面的數據流向圖:
從源碼來看,哨兵 A 向 master 1 的 HELLO 頻道發布的數據有:哨兵 A 的 IP 地址,端口,runid,當前配置版本,以及 master 1 的 IP,端口,當前配置版本。從上圖可以看出,其他所有監視同一 Redis 服務器的哨兵都能收到一份 HELLO 數據,這是訂閱發布相關的內容。
在定時程序的調用鏈:sentinelTimer()->sentinelHandleRedisInstance()->sentinelPingInstance() 中,哨兵會向 Redis 服務器的 hello 頻道發布數據。在 sentinel.c 文件中找到向 hello 頻道發布數據的函數:
int sentinelSendHello(sentinelRedisInstance *ri) { // ri 可以是一個主機,從機。 // 只是用主機和從機作為一個中轉,主從機收到publish 命令后會將數據傳輸給 // 訂閱了hello 頻道的哨兵。這里可能會有疑問,為什么不直接發給哨兵??? char ip[REDIS_IP_STR_LEN]; char payload[REDIS_IP_STR_LEN+1024]; int retval; sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master; sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master); /* Try to obtain our own IP address. */ if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR; if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR; // 格式化需要發送的數據,包括: // 哨兵IP 地址,端口,runnid,當前配置版本, // 主機IP 地址,端口,當前配置的版本 /* Format and send the Hello message. */ snprintf(payload,sizeof(payload), "%s,%d,%s,%llu," /* Info about this sentinel. */ "%s,%s,%d,%llu", /* Info about current master. */ ip, server.port, server.runid, (unsigned long long) sentinel.current_epoch, /* --- */ master->name,master_addr->ip,master_addr->port, (unsigned long long) master->config_epoch); 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; }
redisAsync 系列的函數底層也是 當 Redis 服務器收到來自哨兵的數據時候,會向所有訂閱 hello 頻道的哨兵發布數據,由此剛才注冊的回調函數sentinelReceiveHelloMessages() 就被調用了。回調函數 sentinelReceiveHelloMessages() 做了兩件事情:
- 發現其他監視同一 Redis 服務器的哨兵
- 更新配置版本,當其他哨兵傳遞的配置版本更高的時候,會更新 Redis 主服務器配置(IP 地址和端口)
總結一下這里的工作原理,哨兵會向 hello 頻道發送包括:哨兵自己的IP 地址和端口,runid,當前的配置版本;其所監視主機的 IP 地址,端口,當前的配置版本。【這里要說清楚,什么是 runid 和配置版本】雖然未知的信息很多,但我們可以得知,當一個哨兵新加入到一個 Redis 集群中時,就能通過 hello 頻道,發現其他更多的哨兵,而它自己也能夠被其他的哨兵發現。這是 Redis 所謂 auto discover 的一部分。
INFO 命令
同樣,在定時程序的調用鏈:sentinelTimer()->sentinelHandleRedisInstance()->sentinelPingInstance() 中,哨兵向與 Redis 服務器的命令連接通道上,發送了一個INFO 命令(字符串);並注冊了回調函數sentinelInfoReplyCallback()。Redis 服務器需要對 INFO 命令作出相應,能在 redis.c 主文件中找到 INFO 命令的處理函數:當 Redis 服務器收到INFO命令時候,會向該哨兵回傳數據,包括:
關於該 Redis 服務器的細節信息,rRedis 軟件版本,與其所連接的客戶端信息,內存占用情況,數據落地(持久化)情況,各種各樣的狀態,主從復制信息,所有從機的信息,CPU 使用情況,存儲的鍵值對數量等。
由此得到最值得關注的信息,所有從機的信息都在這個時候曝光給了哨兵,哨兵由此就可以監視此從機了。
Redis 服務器收集了這些信息回傳給了哨兵,剛才所說哨兵的回調函數 sentinelInfoReplyCallback()會被調用,它的主要工作就是着手監視未被監視的從機;完成一些故障修復(failover)的工作。連同上面的一節,就是Redis 的 auto discover 的全貌了。
心跳
心跳是一種判斷兩台機器連接是否正常非常常用的手段,接收方在收到心跳包之后,會更新收到心跳的時間,在某個時間點如果檢測到心跳包過久未收到(即超時),這證明網絡環境不好,或者對方很忙,也為接收方接下來的行動提供指導:接收方可以等待心跳正常的時候再發送數據。在哨兵的定時程序中,哨兵會向所有的服務器,包括哨兵服務器,發送 PING 心跳,而哨兵收到來自 Redis 服務器的回應后,也會更新相應的時間點或者執行其他操作。
在線狀態監測
哨兵有兩種判斷用戶在線的方法,主觀和客觀方法,即 Check Subjectively Down 和 Check Objective Down。主觀是說,Redis 服務器的在線判斷依據是某個哨兵自己的信息;客觀是說,Redis 服務器的在線判斷依據是由其他監視此 Redis 服務器的哨兵的信息。
哨兵憑借的自己的信息判斷 Redis 服務器是否下線的方法,稱為主觀方法,即通過判斷前面有提到的 PING 心跳等其他通信時間是否超時來判斷主機是否下線。主觀的信息有可能是錯的。
哨兵不僅僅憑借自己的信息,還依據其他哨兵提供的信息判斷 Redis 服務器是否下線的方法稱為客觀方法,即通過所有其他哨兵報告的主機在線狀態來判定某主機是否下線。前面提到,INFO 命令可以從其他哨兵服務器上獲取信息,而這里面的信息就包含了他們共同關注主機的在線狀態。客觀判斷方法是基於主觀判斷方法的,即如果一個 Redis 服務器被客觀判定為下線,那么其早已被主觀判斷為下線了。因此客觀判斷的在線狀態較有說服力,譬如在故障修復中就用到客觀判斷的結果。
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { dictIterator *di; dictEntry *de; int quorum = 0, odown = 0; // 足夠多的哨兵報告主機下線了,則設置Objectively down 標記 if (master->flags & SRI_S_DOWN) { // 此哨兵本身認為redis 服務器下線了 /* 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); if (ri->flags & SRI_MASTER_DOWN) quorum++; } dictReleaseIterator(di); // 足夠多的哨兵報告主機下線了,設置標記 if (quorum >= master->quorum) odown = 1; } /* Set the flag accordingly to the outcome. */ if (odown) { // 寫日志,設置SRI_O_DOWN if ((master->flags & SRI_O_DOWN) == 0) { sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d", quorum, master->quorum); master->flags |= SRI_O_DOWN; master->o_down_since_time = mstime(); } } else { // 寫日志,取消SRI_O_DOWN if (master->flags & SRI_O_DOWN) { sentinelEvent(REDIS_WARNING,"-odown",master,"%@"); master->flags &= ~SRI_O_DOWN; } } }
故障修復
一個 Redis 集群難免遇到主機宕機斷電的時候,哨兵如果檢測主機被大多數的哨兵判定為下線,就很可能會執行故障修復,重新選出一個主機。一般在 Redis 服務器集群中,只有主機同時肩負讀請求和寫請求的兩個功能,而從機只負責讀請求,從機的數據更新都是由之前所提到的主從復制上獲取的。因此,當出現意外情況的時候,很有必要新選出一個新的主機。
一般在 Redis 服務器集群中,只有主機同時肩負讀請求和寫請求的兩個功能,而從機只負責讀請求依然是在定時程序的調用鏈中, 我們能找到故障修復(failover) 誕生的地方:
sentinelTimer()->sentinelHandleRedisInstance()->sentinelStartFailoverIfNeeded()
。
sentinelStartFailoverIfNeeded() 函數判斷是否有必要進行故障修復,這里有三個條件:
- Redis 主機必須已經被客觀判定為下線了
- 針對 Redis 主機的故障修復尚未開始
- 限定時間內,不能多次執行故障修復
三個條件都得到滿足,故障修復就開始了。
繼續往下走:sentinelTimer()->sentinelHandleRedisInstance()->sentinelStartFailoverIfNeeded()->sentinelStartFailover()。sentinelStartFailover()
設置了一些故障修復相關的標記等數據。故障修復分成了幾個步驟完成,每個步驟對應一個狀態。
故障修復狀態圖
哨兵專門有一個故障修復狀態機,
// 故障修復狀態機,依據被標記的狀態執行相應的動作 void sentinelFailoverStateMachine(sentinelRedisInstance *ri) { redisAssert(ri->flags & SRI_MASTER); if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return; switch(ri->failover_state) { case SENTINEL_FAILOVER_STATE_WAIT_START: sentinelFailoverWaitStart(ri); break; case SENTINEL_FAILOVER_STATE_SELECT_SLAVE: sentinelFailoverSelectSlave(ri); break; case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE: sentinelFailoverSendSlaveOfNoOne(ri); break; case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION: sentinelFailoverWaitPromotion(ri); break; case SENTINEL_FAILOVER_STATE_RECONF_SLAVES: sentinelFailoverReconfNextSlave(ri); break; } }
WAIT_START
在哨兵服務器群中,有首領(leader)的概念,這個首領可以是系統管理員根據具體情況指定的,也可以是眾多的哨兵中按一定的條件選出的。在 WAIT_STATE 中執行故障修復的哨兵首先確定自己是不是首領,如果不是故障修復會被拖延,到下一個定時程序再次檢測自己是否為首領,超過一定時間會強制停止故障修復。
怎么樣才可以當選一個首領呢?每一個哨兵都會有一個當前的配置版本號 current_-epoch,此版本號會經由hello,is-master-down 命令交換,以便將自身的版本號告知其他所有監視同一 Redis 服務器的哨兵。
每一個哨兵手里都會有一票投給其中一個配置版本最高的哨兵,它的投票信息將會通過 is-master-down 命令交換。is-master-down 命令在故障修復的時候會被強制觸發,收到它的哨兵將會進行投票並返回自己的投票結果,哨兵會將它保存在對應的 sentinelRedisInstance 中。如此一來,執行故障修復的哨兵就能得到其他哨兵的投票結果,它就能確定自己是不是哨兵了。
struct sentinelState { // 哨兵的配置版本 uint64_t current_epoch; ...... } sentinel; typedef struct sentinelRedisInstance { ...... // 故障修復相關的參數 /* Failover */ // 所選首領的runid。runid 其實就是一個redis 服務器唯一標識 char *leader; /* If this is a master instance, this is the runid of the Sentinel that should perform the failover. If this is a Sentinel, this is the runid of the Sentinel that this Sentinel voted as leader. */ // 所選首領的配置版本 uint64_t leader_epoch; /* Epoch of the 'leader' field. */ ...... } sentinelRedisInstance;
因此, 只要某哨兵的配置版本足夠高, 它就有機會當選為首領。在
sentinelTimer()-»sentinelHandleDictOfRedisInstances()-»sentinelHandleRedisInstance()- »sentinelFailoverStateMachine()-»sentinelFailoverWaitStart()-»sentinelFailoverWaitStart()
你可以看到詳細的投票過程。
總結了一下選舉首領的過程:
- 遍歷哨兵表中的所有哨兵,統計每個哨兵的得票情況,注意,得票哨兵的版本號必須和執行故障修復哨兵的配置版本號相同,這樣做是為了確認執行故障修復版本號已經將自己的版本告訴了其他的哨兵。【這里在畫圖的時候可以說明白,其實低版本號的哨兵是沒有機會進行故障修復的】
- 計算得票最多的哨兵
- 執行故障修復的哨兵自己給得票數最高的哨兵投一票,如果沒有投票結果,則給自己投一票。當然投票的前提還是配置版本號要比自己的高。
- 再次計算得票最多的哨兵
- 滿足兩個條件:得票最多的哨兵的票數必須超過選舉數的一半以上;得票最多的哨兵的票數必須超過主機的法定人數(quorum)。
是一個比較曲折的過程。最終,如果確定當前執行故障修復的哨兵是首領,它則可以進入下一個狀態:SELECT_SLAVE。
SELECT_SLAVE
SELECT_SLAVE 的意圖很明確,因為當前的主機(master)已經掛了,需要重新指定一個主機,候選的服務器就是當前掛掉主機的所有從機(slave)。
在
sentinelTimer()-»sentinelHandleDictOfRedisInstances()-»sentinelHandleRedisInstance()-»sentinelFailoverStateMachine()-»sentinelFailoverSelectSlave()-»sentinelSelectSlave()
你可以看到詳細的選舉過程。
當前執行故障修復的哨兵會遍歷主機的所有從機,只有足夠健康的從機才能被成為候選主機。足夠健康的條件包括:
- 不能有下面三個標記中的一個:SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED
- ping 心跳正常
- 優先級不能為 0(slave->slave_priority)
- INFO 數據不能超時
- 主從連接斷線會時間不能超時
滿足以上條件就有機會成為候選主機,如果經過上面的篩選之后有多台從機,那么這些從機會按下面的條件排序:
- 優選選擇優先級高的從機
- 優先選擇主從復制偏移量高的從機,即從機從主機復制的數據越多
- 優先選擇有 runid 的從機
- 如果上面條件都一樣,那么將 runid 按字典順序排序
所選用的排序算法是常用的快排。這是一個比較曲折的過程。如果沒有從機符合要求,譬如最極端的情況,所有從機都跟着掛了,那么故障修復會失敗;否則最終會確定一個從機成為候選主機。從機可以進入下一個狀態:SLAVEOF_NOONE。
SLAVEOF_NOONE
這一步中,哨兵主要做的是向候選主機發送slaveof noone 命令。我們知道,slaveof noone 命令可以讓一個從機轉變為一個主機,Redis 從機收到會做從從機到主機的轉換。發送 slaveof noone 命令之后,哨兵還會向候選主機發送 config rewrite 讓候選主機當前配置信息寫入配置文件,以方便候選從機下次重啟的時候可以恢復。
void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) { int retval; // 與候選從機的連接必須正常,且故障修復沒有超時 /* We can't send the command to the promoted slave if it is now * disconnected. Retry again and again with this state until the timeout * is reached, then abort the failover. */ if (ri->promoted_slave->flags & SRI_DISCONNECTED) { if (mstime() - ri->failover_state_change_time > ri->failover_timeout) { sentinelEvent(REDIS_WARNING,"-failover-abort-slave-timeout",ri,"%@"); sentinelAbortFailover(ri); } return; } /* Send SLAVEOF NO ONE command to turn the slave into a master. * We actually register a generic callback for this command as we don't * really care about the reply. We check if it worked indirectly observing * if INFO returns a different role (master instead of slave). */ retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0); ...... }
WAIT_PROMOTION
這一狀態純粹是為了等待上一個狀態的執行結果(如候選主機的一些狀態)被傳播到此哨兵上,至於是如何傳播的,之前我們有提到過 INFO 數據傳輸的過程。這一狀態的執行函數 sentinelFailoverWaitPromotion() 只做了超時的判斷,如果超時就會停止故障修復。那狀態是如何轉變的呢?就在哨兵捕捉到候選主機狀態的時候。我們可以看到,在哨兵處理 Redis 服務器 INFO 輸出的回調函數 sentinelInfoReplyCallback() 中,故障修復的狀態從 WAIT_PROMOTION 轉變到了下一個狀態 RECONF_SLAVES。
RECONF_SLAVES
這是故障修復狀態機里面的最后一個狀態,后面還會有一個狀態。這一狀態主要做的是向其他非候選從機發送 slaveof promote_slave,即讓候選主機成為他們的主機。其中會涉及幾個 Redis 服務器狀態的標記:SRI_RECONF_SENT,SRI_RECONFINPROG,SRI-RECONF_DONE,分別表示已經向從機發送 slaveof 命令,從機正在重新配置(這里需要一些時間),配置完成。同樣,哨兵是通過 INFO 數據傳輸中獲知這些狀態變更的。
詳細重新配置過程可以在
sentinelTimer()-»sentinelHandleDictOfRedisInstances()- »sentinelHandleRedisInstance()-»sentinelFailoverStateMachine()- »sentinelFailoverReconfNextSlave()-»sentinelSelectSlave()
最后會做從機配置狀況的檢測,如果所有從機都重新配置完成或者超時了,會進入最后一個狀態 UPDATE_CONFIG。
UPDATE_CONFIG
這里還存在最后一個狀態 UPDATE_CONFIG。在定時程序中如果發現進入了這一狀態,會調用sentinelFailoverSwitchToPromotedSlave()-»sentinelResetMasterAndChangeAddress()。因為主機和從機發生了修改,所以 sentinel.masters 肯定需要修改,譬如主機的IP 地址和端口,所以最后的工作是將修改並整理哨兵服務器保存的信息,而這正是 sentinelResetMasterAndChangeAddress()的主要工作。
int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) { sentinelAddr *oldaddr, *newaddr; sentinelAddr **slaves = NULL; int numslaves = 0, j; dictIterator *di; dictEntry *de; newaddr = createSentinelAddr(ip,port); if (newaddr == NULL) return REDIS_ERR; // 保存從機實例 /* Make a list of slaves to add back after the reset. * Don't include the one having the address we are switching to. */ di = dictGetIterator(master->slaves); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *slave = dictGetVal(de); if (sentinelAddrIsEqual(slave->addr,newaddr)) continue; slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1)); slaves[numslaves++] = createSentinelAddr(slave->addr->ip, slave->addr->port); } dictReleaseIterator(di); // 主機也被視為從機添加到從機數組 /* If we are switching to a different address, include the old address * as a slave as well, so that we'll be able to sense / reconfigure * the old master. */ if (!sentinelAddrIsEqual(newaddr,master->addr)) { slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1)); slaves[numslaves++] = createSentinelAddr(master->addr->ip, master->addr->port); } // 重置主機 // sentinelResetMaster() 會將很多信息清空,也會設置很多信息 /* Reset and switch address. */ sentinelResetMaster(master,SENTINEL_RESET_NO_SENTINELS); oldaddr = master->addr; master->addr = newaddr; master->o_down_since_time = 0; master->s_down_since_time = 0; // 將從機恢復 /* Add slaves back. */ for (j = 0; j < numslaves; j++) { sentinelRedisInstance *slave; slave = createSentinelRedisInstance(NULL,SRI_SLAVE,slaves[j]->ip, slaves[j]->port, master->quorum, master); releaseSentinelAddr(slaves[j]); if (slave) { sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@"); sentinelFlushConfig(); } } zfree(slaves); // 銷毀舊的地址結構體 /* Release the old address at the end so we are safe even if the function * gets the master->addr->ip and master->addr->port as arguments. */ releaseSentinelAddr(oldaddr); sentinelFlushConfig(); return REDIS_OK; }
還有一個問題:故障修復過程中,一直沒有發送 SLAVEOF promoted_slave 給舊的主機,因為已經和舊的主機斷開連接,哨兵沒有選擇在故障修復的時候向它發送任何的數據。但在故障修復的最后一個狀態中,哨兵依舊有將舊的主機塞到新主機的從機列表中,所以哨兵還是會超時發送 INFO HELLO 等數據,對舊的主機抱有希望。如果因為網絡環境的不佳導致的故障修復,那舊的主機很可能恢復過來,只是這時它是一台從機了。哨兵選擇在這個時候,發送 slaveof onone 重新配置舊的主機。
就此,故障修復結束。故障修復為 Redis 集群很好的自適應和自修復性。當某主機因為異常或者宕機而不能提供服務的時候,故障修復還能讓 Redis 集群繼續提供服務。