【Redis 源碼】集羣之哨兵 sentinel 故障轉移
前言:
各位看官大家好,這個主題內容比較長然後接着上一章就拆成了兩個部分。那麼我們接着上一章內容開始說。上一章中我們說到哨兵定時器 sentinelTimer 它們作用。sentinelTimer 方法中執行哨兵模式中的任務。包括執行定期操作比如 PING、分析主服務和從服務的 INFO 命令、故障轉移等等。那麼這一章我們就先從 sentinelTimer 開始說起。
(一) 基本結構
1.1 sentinelTimer 定時程序
圖中爲 sentinelTimer 調用鏈路,須線部分爲調用 aeCreateTimeEvent 註冊 serverCron 事件。
sentinel.c 中 sentinelTimer 方法:
void sentinelTimer(void) {
//檢測是否需要開啓sentinel TILT模式
sentinelCheckTiltCondition();
//對哈希表中的每個服務器實例執行調度任務
sentinelHandleDictOfRedisInstances(sentinel.masters);
//執行腳本命令,
sentinelRunPendingScripts();
//清理已經執行完腳本的進程,
sentinelCollectTerminatedScripts();
//kill執行時間超時的腳本
sentinelKillTimedoutScripts();
/*
* 爲了防止多個哨兵同時選舉時故意錯開定時程序執行的時間。
*/
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
1.2 哨兵結構介紹
基本數據結構
struct sentinelState {
char myid[CONFIG_RUN_ID_SIZE+1]; /* 哨兵ID */
uint64_t current_epoch; /* Current epoch. */
dict *masters; /* 存儲哨兵監聽服務器的信息 對應一個
sentinelRedisInstance 結構體指針 */
int tilt; /* 判斷 TILT 模式 */
int running_scripts; /* 當前正在執行的腳本數。*/
mstime_t tilt_start_time; /* TITL 開始時間. */
mstime_t previous_time; /* 上次處理程序運行時間. */
list *scripts_queue; /* 要執行的用戶腳本隊列. */
char *announce_ip; /* IP 地址(gossip協議握手地址) */
int announce_port; /* 端口 (gossip協議端口) */
unsigned long simfailure_flags; /* 故障模擬狀態. */
} sentinel;
每一個哨兵都有一個 sentinel 結構,裏面維護着多個主機連接。每個主機連接信息都維護着一個 sentinelRedisInstance,通過這個結構維護着所有主機連接的關係。
sentinelRedisInstance結構信息:
typedef struct sentinelRedisInstance {
int flags; /* 記錄哨兵類型以及實力當前狀態 */
char *name; /* 哨兵名稱格式爲ip:port ,例如"127.0.0.1:26379" */
char *runid; /* 哨兵運行ID.*/
uint64_t config_epoch; /* 配置紀元,用於故障轉移. */
sentinelAddr *addr; /* 實例地址. */
//...省略
mstime_t s_down_since_time; /* 主觀下線標記時間. */
mstime_t o_down_since_time; /* 客觀下線標記時間. */
mstime_t down_after_period; /* 實例無響應多少毫秒之後纔會被判斷爲主觀下線(subjectively down ),SENTINEL down-after-milliseconds 選項設定的值 */
//。。。省略
/* Master specific. */
dict *sentinels; /* 其他sentinels. */
dict *slaves; /* 這個master的slave */
unsigned int quorum;/* 判斷這個實例客觀下線(objectively down )所需的支持投票數量,SENTINEL monitor <master-name> <IP> <port> <quorum> 選項中的quorum 參數 */
int parallel_syncs; /* 在執行故障轉移操作時,可以同時對新的主服務器進行同步的從服務器數量,SENTINEL parallel-syncs <master-name> <number> 選項的值. */
char *auth_pass; /* Password to use for AUTH against master & slaves. */
//。。。省略
mstime_t failover_start_time; /* 上次故障轉移嘗試開始時間. */
mstime_t failover_timeout; /* 刷新故障遷移狀態的最大時間限制. SENTINEL failover-timeout <master-name> <ms> 選項的值*/
//。。。省略
} sentinelRedisInstance;
1.3 sentinel 建立網絡連接
創建與被監聽的 master 網絡連接後,sentinel 會成功 master 的客戶端,它會向 master 發送命令。
並從 master 的響應中獲取 master 的信息。對於每個被監聽者的 master,sentinel 會向創建兩個異步的網絡連接。
該連接通過 sentinelReconnectInstance 函數創建,一個鏈接爲 commands 鏈接。另外一個鏈接爲 Pub / Sub 連接。訂閱發佈會創建一個__sentinel__:hello 的通道。
1.4 sentinel 命令集
# 重置名字匹配正則表達式的所有master狀態信息,清除之前存儲的狀態信息和slaves信息。PS:節點只要加入過sentinel,信息就會保存而不會自動清除
sentinel reset <pattern>
# 用於改變關於master的配置,例如 sentinel set mymaster down-after-milliseconds 1000 ,此命令修改了當節點第一次失去連接到判定其下線所經過的時間
sentinel set <name> <option> <value>
# 告訴sentinel去監聽新的master
sentinel monitor <name> <ip> <port> <quorum>
# 命令sentinel放棄對某個master的監聽
sentinel remove <name>
# 這個參數設置集羣從判斷節點掛掉,到執行故障轉移操作(即重新選舉master節點)的時間
sentinel failover-timeout mymaster 10000
# 獲取哨兵監視某個sentinel的信息
sentinel sentinels <master-name>
# 獲取sentinel監視的某個master的slaves信息
sentinel slaves <master-name>
# 獲取sentinel 監視的某個 master信息
sentinel master <name>
# 獲取sentinel監視所有的master信息
sentinel masters
# 詢問該sentinel,該 ip,port的master是否爲down狀態,
# 如果該sentinel爲tilt模式,會不理會這個詢問,不去判斷
# 該master是否爲主觀下線狀態,直接回復正常狀態。
sentinel is-master-down-by-addr <ip> <port> <current-epoch> <runid>
# 根據master名字獲取到master的ip和port
sentinel get-master-addr-by-name <master-name>
# 將sentinel 狀態信息寫入到配置文件當中
setinel flushconfig
# 檢查可投票同意master on failure的sentinel+1的個數以及相關狀態
# (可用的投票個數是否大於master 的quorum,需要quorum個同意master on failure)
setinel ckquorum <name>
(二) 發現故障
2.1 如何確定故障
提及到確認故障,哨兵中確認故障有兩種形式分爲對應兩種狀態 SRI_S_DOWN(主觀下線)和 SRI_O_DOWN(客觀下線)。
1) 主觀下線
主觀下線會涉及到一個方法 sentinelCheckSubjectivelyDown,圖中會主觀下線鏈路
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;
/* 檢測command 連接是否被關閉 */
if (ri->link->cc &&
(mstime() - ri->link->cc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
ri->link->act_ping_time != 0 && /* Ther is a pending ping... */
/* The pending ping is delayed, and we did not received
* error replies as well. */
(mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
(mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
{
instanceLinkCloseConnection(ri->link,ri->link->cc);
}
/* 檢測pubsub連接是否需要被關閉
*/
if (ri->link->pc &&
(mstime() - ri->link->pc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
(mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
{
instanceLinkCloseConnection(ri->link,ri->link->pc);
}
/* 更新SRI_S_DOWN狀態
*/
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 {
/* Is subjectively up */
if (ri->flags & SRI_S_DOWN) {
sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
}
}
}
- 檢測 command 連接是否被關閉;
2. 檢測 pubsub 連接是否需要被關閉 ;
3. 更新 SRI_S_DOWN 狀態狀態,更新狀態有如下兩個規則:
3.1 超過 ri->down_after_period,代表超過響應時間,及 ping 無響應請求。該時間默認走的時 SENTINEL_DEFAULT_DOWN_AFTER 宏爲 30s。
3.2 SLAVE 上報連續時間間隔要大於 ri->down_after_period+SENTINEL_INFO_PERIOD_2,及(30s + 10s_2),如果超過這個時間代表 slave 長時間連續不到 master,所以視爲主觀下線。
2) 客觀下線
說到客觀下線是,我們要思考一個問題。當一臺 master 服務已經掉線,並且已經維護自己的狀態爲 SRI_S_DOWN。由於在哨兵集羣中,ri->down_after_period 值可能不一樣。判斷 master 下線的時間間隔可能不一樣。所以必須去詢問 sentinel 節點這臺 master 服務是否下線。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
//。。。省略
/* Only masters */
if (ri->flags & SRI_MASTER) {
sentinelCheckObjectivelyDown(ri);
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
sentinelFailoverStateMachine(ri);
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
在上面代碼中可以看到客觀下線只能 master 中使用。然後看一下 sentinelAskMasterStateToOtherSentinels 方法,該方法檢測 master 主觀下線後去詢問其他 sentinel。
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
dictIterator *di;
dictEntry *de;
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
char port[32];
int retval;
/* If the master state from other sentinel is too old, we clear it. */
if (elapsed > SENTINEL_ASK_PERIOD*5) {
ri->flags &= ~SRI_MASTER_DOWN;
sdsfree(ri->leader);
ri->leader = NULL;
}
/* 滿足下列情況纔可以詢問其他哨兵:
*
* 1) 主觀下線是否在進行
* 2) Sentinel是否連接
* 3) 我們沒有在哨兵詢問期內收到信息,1秒內. */
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;
/* Ask */
ll2string(port,sizeof(port),master->addr->port);
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"SENTINEL is-master-down-by-addr %s %s %llu %s",
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);
}
通過遍歷維護的 master->sentinels 結構向其他 sentinel 節點發送命令:SENTINEL is-master-down-by-addr
命令格式如下:
SENTINEL is-master-down-by-addr <current_epoch> <leader_id>
命令詢問其他 sentinel 是否同意主服務器已下線。
接受 SENTINEL is-master-down-by-addr 命令返回狀態:
<down_state> <leader_runid> <leader_epoch>
down_state:爲 1 代表主服務器已下線,0 表示主服務器未下線。
leader_runid:領頭 sentinal id。
leader_epoch:領頭 sentinel 當前投票紀元。
(三) 故障轉移
3.1 故障狀態
當某個主節點進行故障轉移時,該主節點的的故障轉移狀態,master->failover_state, 依次會經歷 6 個狀態:
狀態宏:
SENTINEL_FAILOVER_STATE_NONE 0 /*沒有故障轉移在進行*/
//以下爲經歷的6個狀態
SENTINEL_FAILOVER_STATE_WAIT_START 1 /* sentinel接手故障轉移*/
SENTINEL_FAILOVER_STATE_SELECT_SLAVE 2 /* 選擇slave成爲master*/
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 3 /* 發送slaveof no one給新master */
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 4 /* 等待新master升級完成,超時終止故障轉移*/
SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5 /* 新master升級完成後,讓slaves複製新master */
SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6 /* 監視新master */
3.2 狀態機
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
serverAssert(ri->flags & SRI_MASTER);
if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;
switch(ri->failover_state) {
case SENTINEL_FAILOVER_STATE_WAIT_START:
sentinelFailoverWaitStart(ri); //sentinel接手故障轉移
break;
case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
sentinelFailoverSelectSlave(ri); //選擇slave成爲master
break;
case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
sentinelFailoverSendSlaveOfNoOne(ri); //發送slaveof no one給新master
break;
case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
sentinelFailoverWaitPromotion(ri); //等待新master升級完成,超時終止故障轉移
break;
case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
sentinelFailoverReconfNextSlave(ri); //新master升級完成後,讓slaves複製新master
break;
}
}
狀態機變化過程:
總結
1)一個哨兵結構中可以維護多個主機,包括 master,slave,sentinel。
2)確定故障分爲:主觀下線和客觀下線兩種。
3)主觀下線:爲一段時間內 ping 返回無效,探測所有節點都是一致的,則爲主觀下線。主觀下線的時間是可以配置的,以 master 配置維度爲準。
4)客觀下線:客觀下線只針對於 master 節點,且需要 master 爲主觀下線,並通過其他 sentinel 節點發送 SENTINEL is-master-down-by-addr 詢問其他節點 master 下線問題,達成共識的一個狀態。
5) 哨兵會創建兩個鏈接一個 commands 鏈接,一個訂閱發佈鏈接。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Dr87prK8LmU85dZANir6zw