Redis 哨兵模式(Sentinel)
- 2020 年 4 月 11 日
- 筆記
上一篇我們介紹了 redis 主從節點之間的數據同步複製技術,通過一次全量複製和不間斷的命令傳播,可以達到主從節點數據同步備份的效果,一旦主節點宕機,我們可以選擇一個工作正常的 slave 成為新的主節點,並讓其他 slave 去同步它。
這是處理 redis 故障轉移的一個方式,但卻不具備生產實用性,因為畢竟是手動處理故障,而 redis 發生故障時間節點不可預知,我們需要一個自動監控組件幫我們自動處理故障轉移。
Redis 哨兵模式(Sentinel)就是一個自動地監控處理 redis 間故障節點轉移工作的一個「東西」,準確來說,Sentinel 其實是一個 redis 服務端程式,只不過運行在特殊的模式下,不提供數據存儲服務,只進行普通 redis 節點監控管理。
一、什麼是哨兵(Sentinel)
Sentinel 其實也是一個 redis 的服務端程式,它也會定時執行 serverCron 函數,只是裡面其他的程式用不到,用到的是對普通 redis 節點的監控以及故障轉移模組。
Sentinel 初始化的時候會清空原來的命令表,寫入自己獨有的命令進去,所以普通 redis 節點支援的數據讀寫命令,對 Sentinel 來說都是找不到命令,因為它根本就沒有初始化這些命令的執行器。
Sentinel 會定時的對自己監控的 master 執行 info 命令,獲取最新的主從關係,還會定時的給所有的 redis 節點發送 ping 心跳檢測命令,如果檢測到某個 master 無法響應了,就會在給其他 Sentinel 發送消息,主觀認為該 master 宕機,如果 Sentinel 集群認同該 master 下線的人數達到一個值,那麼大家統一意見,下線該 master。
下線之前需要做的是找 Sentinel 集群中的某一個來執行下線操作,這個步驟叫領導者選舉,選出來以後會從該 master 所有的 slave 節點中挑一個合適的作為新的 master,並讓其他 slave 重新同步新的 master。
其實以上我們就簡單的介紹了 Sentinel 是什麼,本質上做了哪些事情,等下我們會結合源碼細說其中的細節實現。這裡我們再看下,如何配置並啟動一個 Sentinel 監控。(生產環境建議配置大於三個)
第一步,啟動一個普通的 redis server 節點:
這一步沒什麼好說的,我們啟動在一個默認的 6379 埠上。
第二步,啟動三個不同的 slave 節點:
第三步,編寫 sentinel 配置文件:
我們解釋一下這幾條配置的含義,我們說過 Sentinel 其實是運行在特殊模式下的 redis server,所以它需要運行埠。緊接著我們通過命令 sentinel monitor mymaster 配置當前 sentinel 需要監控的主節點 redis 以及觸發客觀下線參數,sentinel down-after-milliseconds 配置了一個參數,master 最長響應時間,超過這個時間就主觀判斷它下線。
sentinel parallel-syncs 配置用於限制主從切換之後,最多的並行同步數據的從節點數量,因為我們知道,主從進行全量同步階段,從節點載入數據時是不提供服務的,如果這個參數越大,那麼主從切換完成的時間就越短,當然也會導致大量從節點不可提供讀服務,反之。
sentinel failover-timeout 配置了執行故障轉移的最大等待時間。
第四步,啟動 Sentinel:
使用命令,redis-sentinel [config],啟動三個 sentinel。
這樣的話,其實我們就完成了一個簡單的 sentinel 集群配置,下面我們手動的讓 master 宕機,看看整個 sentinel 有沒有為我們做故障轉移。
從結果上看來,sentinel 自動為我們把原先的從節點 7003 設置為新的 master,具體過程我們不細說,等下結合源碼詳細介紹,這裡我們應該大致對 sentinel 的實際應用有了大概的認識。
二、Sentinel 如何工作的
當我們使用命令 redis-sentinel 啟動 sentinel 的時候,
int main(int argc, char **argv) { 。。。。。 server.sentinel_mode = checkForSentinelMode(argc,argv); 。。。。。 if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); } 。。。。。 }
checkForSentinelMode 函數中會根據你的命令以及參數,檢查判斷是否是以 sentinel 模式啟動,如果是則返回 1,反之。如果是以 sentinel 啟動,則會進行一個 sentinel 的初始化操作。
void initSentinelConfig(void) { server.port = REDIS_SENTINEL_PORT; //26379 }
initSentinelConfig 實際上就是初始化當前 sentinel 運行埠,默認是 26379。
void initSentinel(void) { unsigned int j; //清空普通redis-server下可用的命令表 dictEmpty(server.commands,NULL); //載入sentinel需要的命令 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); serverAssert(retval == DICT_OK); } sentinel.current_epoch = 0; //根據配置文件,初始化自己需要監控的master(一個sentinel是可能監控多個 master的) sentinel.masters = dictCreate(&instancesDictType,NULL); sentinel.tilt = 0; sentinel.tilt_start_time = 0; sentinel.previous_time = mstime(); sentinel.running_scripts = 0; sentinel.scripts_queue = listCreate(); sentinel.announce_ip = NULL; sentinel.announce_port = 0; sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE; sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG; memset(sentinel.myid,0,sizeof(sentinel.myid)); }
initSentinel 主要的作用還是清空普通模式的 redis 命令表,載入獨屬於 sentinel 使用的命令,並初始化自己監控的 master 集合。
至此,sentinel 的初始化就算完成了,剩下的自動監控則在定時函數 serverCron 中,我們一起來看看。
//間隔 100 毫秒執行一次 sentinelTimer run_with_period(100) { if (server.sentinel_mode) sentinelTimer(); }
也就是說,sentinel 啟動之後,會間隔 100 毫秒在 serverCron 調用一次 sentinelTimer 函數處理一些重要事件(其實,sentinelTimer 中會修改執行間隔)。
void sentinelTimer(void) { sentinelCheckTiltCondition(); sentinelHandleDictOfRedisInstances(sentinel.masters); sentinelRunPendingScripts(); sentinelCollectTerminatedScripts(); sentinelKillTimedoutScripts(); server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ; }
sentinelTimer 函數體非常簡短,但不要高興太早。sentinelCheckTiltCondition 函數我們不去多說,redis 高度依賴系統時間,如果多次檢測到系統時鐘紀元不準確,它會判定當前系統不穩定,進入 TITL,類似一個休眠的狀態,不會為我們做故障轉移,僅僅收集數據,等待系統恢復穩定。
void sentinelHandleDictOfRedisInstances(dict *instances) { dictIterator *di; dictEntry *de; sentinelRedisInstance *switch_to_promoted = NULL; di = dictGetIterator(instances); //遞歸遍歷監控的所有 master,執行監控操作 while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); //這是監控的核心邏輯,下文細說 sentinelHandleRedisInstance(ri); if (ri->flags & SRI_MASTER) { //不論是 slave 還是其他 sentinel,都視作一個redisInstance sentinelHandleDictOfRedisInstances(ri->slaves); sentinelHandleDictOfRedisInstances(ri->sentinels); if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) { switch_to_promoted = ri; } } } if (switch_to_promoted) sentinelFailoverSwitchToPromotedSlave(switch_to_promoted); dictReleaseIterator(di); }
sentinelHandleRedisInstance 主要兩個部分組成,監控和故障轉移。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { sentinelReconnectInstance(ri); sentinelSendPeriodicCommands(ri); if (sentinel.tilt) { if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return; sentinel.tilt = 0; sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited"); } sentinelCheckSubjectivelyDown(ri); if (ri->flags & (SRI_MASTER|SRI_SLAVE)) { /* Nothing so far. */ } if (ri->flags & SRI_MASTER) { sentinelCheckObjectivelyDown(ri); if (sentinelStartFailoverIfNeeded(ri)) sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED); sentinelFailoverStateMachine(ri); sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS); } }
sentinelReconnectInstance 函數做兩件事,因為當前是一個 sentinel 實例,所以第一件事就是與當前遍歷的 instance 建立連接,不論它是 master、slave 或是 sentinel,並在成功建立連接後發送 ping 命令。第二,如果當前遍歷的是一個 master 或 slave,則會訂閱它的 sentinel_hello 頻道,當這個頻道上有消息更新,則會廣播所有訂閱的該頻道的客戶端。(訂閱這個頻道的主要作用還是用於發現其他 sentinel 以及與其他 sentinel 交流自己對監控的節點的看法)
sentinelSendPeriodicCommands 函數默認每間隔十秒給 master 和 slave 發送 info 命令,了解他們的主從關係,如果此 instance 被自己主觀下線了,那麼會加快發送 info 命令的頻率,以保證自己最快知道主從關係變化,還會每間隔一秒 ping 所有類型的實例。
以上其實是 sentinelHandleRedisInstance 中監控節點的部分,下面我們繼續看其故障轉移怎麼做的。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { sentinelReconnectInstance(ri); sentinelSendPeriodicCommands(ri); //判斷是否需要進入 tilt 模式 if (sentinel.tilt) { if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return; sentinel.tilt = 0; sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited"); } //判斷是否需要主觀下線該節點 sentinelCheckSubjectivelyDown(ri); if (ri->flags & (SRI_MASTER|SRI_SLAVE)) { /* Nothing so far. */ } if (ri->flags & SRI_MASTER) { //判斷是否需要客戶下線該節點 sentinelCheckObjectivelyDown(ri); //如果確定該節點客觀下線,進行領導者選舉 if (sentinelStartFailoverIfNeeded(ri)) sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED); //故障轉移 sentinelFailoverStateMachine(ri); sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS); } }
sentinelCheckSubjectivelyDown 檢測當前節點是否需要主觀下線,判斷條件是此節點對於自己的配置,如果當前這個實例超過配置的時間段沒有回復自己的 ping,那麼判斷它下線,設置主觀下線標誌位。
sentinelCheckObjectivelyDown 檢測當前是否達到客觀下線的條件,檢測邏輯是這樣的,遍歷所有的兄弟 sentinel 結構,看看他們有沒有把當前節點主觀下線,統計數量,如果達到 quorum,則判定該 master 客觀下線,設置標誌位並通過頻道通知到其他 兄弟 sentinel。
sentinelStartFailoverIfNeeded 判斷當前是否已有 sentinel 在進行故障轉移(通過 master 的一個標誌位,如果有 sentinel 正在進行故障轉移,這個標誌位會被設置),如果有,則自己不參與,什麼都不做。
sentinelAskMasterStateToOtherSentinels 會去給其他 sentinel 發送消息,要求它同意自己作為領導者對 master 進行故障轉移。具體怎麼做的呢,首先會拿到自己這邊關於所有兄弟 sentinel 的資訊進行一個遍歷,並給他們發送命令 is-master-down-by-addr 要求他們同意自己成為領導者,並設置回調函數 sentinelReceiveIsMasterDownReply 處理回復。
如果某個 sentinel 收到別人發來的領導者投票,且自己沒有給其他人投過票的話就會同意,反之不予理睬。
當某個 sentinel 收到足夠的票數,則它認為自己就是 leader,標誌 master 為故障轉移中,並進行真正的故障轉移操作。
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); break; //選擇一個要晉陞的從節點 case SENTINEL_FAILOVER_STATE_SELECT_SLAVE: sentinelFailoverSelectSlave(ri); break; //發送slaveof no one命令,使從節點變為主節點 case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE: sentinelFailoverSendSlaveOfNoOne(ri); break; //等待被選擇的從節點晉陞為主節點,如果超時則重新選擇晉陞的從節點 case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION: sentinelFailoverWaitPromotion(ri); break; //給所有的從節點發送slaveof命令,同步新的主節點 case SENTINEL_FAILOVER_STATE_RECONF_SLAVES: sentinelFailoverReconfNextSlave(ri); break; } }
sentinelFailoverStateMachine 故障轉移包括五個步驟,分五個 sentinelTimer 執行周期處理。當新 master 選舉完成,會給其他兄弟 sentinel 廣播,告知他們新的 master 已經出現,他們收到後,會撤銷對原 master 的主觀下線,並重新開始監控新的 master。
至此,我們對 Sentinel 的介紹與源碼分析就結束了,它本質上就是一個運行在特殊模式下的 redis-server,通過不斷 ping 主從節點,在感知他們可能出現故障之後,集體進行一個投票認定並選舉出一個人去執行 master 的客觀下線。
下一篇,我們看 redis 中更牛逼的 cluster。