深入淺析Redis中的sentinel故障轉移

本篇文章帶大家了解一下redis中的故障轉移(sentinel),希望對大家有所幫助!

深入淺析Redis中的sentinel故障轉移

當兩臺以上的redis實例形成了主備關系,它們組成的集群就具備了一定的高可用性:當master發生故障的時候,slave可以成為新的master對外提供讀寫服務,這種運營機制成為failover。【相關推薦:redis

那么誰來發現master的故障做failover決策?

一種方式是,保持一個daemo進程,監控著所有的master-slave節點,如下圖所示:

深入淺析Redis中的sentinel故障轉移

一個Redis集群里面有一個master和兩個slave,這個daemon進程監控著這三個節點。但daemon為單節點,本身可用性無法保證。需要引入多daemon,如下圖所示:

深入淺析Redis中的sentinel故障轉移

多個daemon解決了可用性問題,但又出現了一致性問題,如何就某個master是否可用達成一致?例如上圖兩個daemon1和和master網絡不通,daemon和master連接暢通,那此時mater節點是否需要failover那?

Redis的sentinel提供了一套多daemon間的交互機制,多個daemon間組成一個集群,成為sentinel集群,daemon節點也稱為sentinel節點。如下圖所示:

深入淺析Redis中的sentinel故障轉移

這些節點相互間通信、選舉、協商,在master節點的故障發現failover決策上表現出一致性。

sentinel集群監視任意多個master以及master下的slave,自動將下線的master從其下的某個slave升級為新的master代替繼續處理命令請求。

啟動并初始化Sentinel

啟動一個Sentinel可以使用命令:

./redis-sentinel?../sentinel.conf

或者命令:

./redis-server?../sentinel.conf?--sentinel

當一個Sentinel啟動時,它需要執行以下步驟:

初始化服務器

Sentinel本質上是運行在特殊模式下的Redis服務器,它和普通的Redis服務器執行的工作不同,初始化過程也不完全相同。如普通的Redis服務器初始化會載入RDB或者AOF文件來恢復數據,而Sentinel啟動時不會載入,因為Sentinel并不使用數據庫。

將普通Redis服務器使用的代碼替換成Sentinel專用代碼

將一部分普通Redis服務器使用的代碼替換成Sentinel專用代碼。如普通Redis服務器使用server.c/redisCommandTable作為服務器的命令表:

truct?redisCommand?redisCommandTable[]?=?{ ????{"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0}, ????{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}, ????{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}, ????{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0}, ????{"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0}, ????{"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0}, ????{"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0}, ????..... ????{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0}, ????{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0}, ????{"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0}, ????{"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0}, ????{"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0}, ????{"bitfield",bitfieldCommand,-2,"wm",0,NULL,1,1,1,0,0}, ????{"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0}, ????{"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0}, ????{"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0}, ????{"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0}, ????{"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0}, ????{"mget",mgetCommand,-2,"rF",0,NULL,1,-1,1,0,0}, ????{"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0}, ????{"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0} ????...... ????}

Sentinel使用sentinel.c/sentinelcmds作為服務器列表,如下所示:

struct?redisCommand?sentinelcmds[]?=?{ ????{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0}, ????{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0}, ????{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0}, ????{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, ????{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0}, ????{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, ????{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0}, ????{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0}, ????{"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0}, ????{"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0}, ????{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}, ????{"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0} }

初始化Sentinel狀態

服務器會初始化一個sentinel.c/sentinelState結構(保存服務器中所有和Sentinel功能有關的狀態)。

struct?sentinelState?{ ? ????char?myid[CONFIG_RUN_ID_SIZE+1];?/*?This?sentinel?ID.?*/ ???? ????//當前紀元,用于實現故障轉移 ????uint64_t?current_epoch;?????????/*?Current?epoch.?*/ ???? ????//監視的主服務器 ????//字典的鍵是主服務器的名字 ????//字典的值則是一個指向sentinelRedisInstances結構的指針 ????dict?*masters;??????/*?Dictionary?of?master?sentinelRedisInstances. ???????????????????????????Key?is?the?instance?name,?value?is?the ???????????????????????????sentinelRedisInstance?structure?pointer.?*/ ????//是否進入tilt模式 ????int?tilt;???????????/*?Are?we?in?TILT?mode??*/ ???? ????//目前正在執行的腳本數量 ????int?running_scripts;????/*?Number?of?scripts?in?execution?right?now.?*/ ???? ????//進入tilt模式的時間 ????mstime_t?tilt_start_time;???????/*?When?TITL?started.?*/ ???? ????//最后一次執行時間處理器的時間 ????mstime_t?previous_time;?????????/*?Last?time?we?ran?the?time?handler.?*/ ???? ????//?一個FIFO隊列,包含了所有需要執行的用戶腳本 ????list?*scripts_queue;????????????/*?Queue?of?user?scripts?to?execute.?*/ ???? ????char?*announce_ip;??/*?IP?addr?that?is?gossiped?to?other?sentinels?if ???????????????????????????not?NULL.?*/ ????int?announce_port;??/*?Port?that?is?gossiped?to?other?sentinels?if ???????????????????????????non?zero.?*/ ????unsigned?long?simfailure_flags;?/*?Failures?simulation.?*/ ????int?deny_scripts_reconfig;?/*?Allow?SENTINEL?SET?...?to?change?script ??????????????????????????????????paths?at?runtime??*/ }

根據給定的配置文件,初始化Sentinel的監視主服務器列表

對Sentinel狀態的初始化將引發對masters字典的初始化,而master字典的初始化是根據被載入的Sentinel配置文件來進行的。

字典的key是監視主服務器的名字,字典的值則是被監控主服務器對應的sentinel.c/sentinelRedisInstance結構。

sentinelRedisInstance結構部分屬性如下:

typedef?struct?sentinelRedisInstance?{ ????//標識值,記錄了實例的類型,以及該實例的當前狀態 ????int?flags;??????/*?See?SRI_...?defines?*/ ???? ????//實例的名字 ????//主服務器的名字由用戶在配置文件中設置 ????//從服務器以及Sentinel的名字由Sentinel自動設置 ????//格式為ip:port,例如“127.0.0.1:26379” ????char?*name;?????/*?Master?name?from?the?point?of?view?of?this?sentinel.?*/ ???? ????//實例運行的ID ????char?*runid;????/*?Run?ID?of?this?instance,?or?unique?ID?if?is?a?Sentinel.*/ ???? ????//配置紀元,用于實現故障轉移 ????uint64_t?config_epoch;??/*?Configuration?epoch.?*/ ???? ????//實例的地址 ????sentinelAddr?*addr;?/*?Master?host.?*/ ???? ????//sentinel?down-after-milliseconds選項設定的值 ????//實例無響應多少毫秒之后才會被判斷為主觀下線(subjectively?down) ????mstime_t?down_after_period;?/*?Consider?it?down?after?that?period.?*/ ???? ????//sentinel?monitor?<master-name>?<ip>?<redis-port>?<quorum>選項中的quorum ????//判斷這個實例為客觀下線(objective?down)所需的支持投票的數量 ????unsigned?int?quorum;/*?Number?of?sentinels?that?need?to?agree?on?failure.?*/?? ????//sentinel?parallel-syncs?<master-name>?<numreplicas>?選項的numreplicas值 ????//在執行故障轉移操作時,可以同時對新的主服務器進行同步的從服務器數量 ????int?parallel_syncs;?/*?How?many?slaves?to?reconfigure?at?same?time.?*/ ???? ????//sentinel?failover-timeout?<master-name>?<milliseconds>選項的值 ????//刷新故障遷移狀態的最大時限 ????mstime_t?failover_timeout;??????/*?Max?time?to?refresh?failover?state.?*/ }</milliseconds></master-name></numreplicas></master-name></quorum></redis-port></ip></master-name>

例如啟動Sentinel時,配置了如下的配置文件:

#?sentinel?monitor?<master-name>?<ip>?<redis-port>?<quorum> sentinel?monitor?master1?127.0.0.1?6379?2  #?sentinel?down-after-milliseconds?<master-name>?<milliseconds> sentinel?down-after-milliseconds?master1?30000  #?sentinel?parallel-syncs?<master-name>?<numreplicas> sentinel?parallel-syncs?master1?1  #?sentinel?failover-timeout?<master-name>?<milliseconds> sentinel?failover-timeout?master1?900000</milliseconds></master-name></numreplicas></master-name></milliseconds></master-name></quorum></redis-port></ip></master-name>

則Sentinel則會為主服務器master1創建如下圖所示的實例結構:

深入淺析Redis中的sentinel故障轉移

Sentinel狀態以及masters字典的機構如下:

深入淺析Redis中的sentinel故障轉移

創建連向主服務器的網絡連接

創建連向被監視主服務器的網絡連接,Sentinel將成為主服務器的客戶端,向主服務器發送命令并從命令回復獲取信息。

Sentinel會創建兩個連向主服務器的異步網絡連接:

  • 命令連接,用于向主服務器發送命令并接收命令回復
  • 訂閱連接,訂閱主服務器的_sentinel_:hello頻道

深入淺析Redis中的sentinel故障轉移

Sentinel發送信息和獲取信息

  • Sentinel默認會以每十秒一次的頻率,通過命令連接向被監視的master和slave發送INFO命令

    通過master的回復可獲取master本身信息,包括run_id域記錄的服務器運行ID,以及role域記錄的服務器角色。另外還會獲取到master下的所有的從服務器信息,包括slave的ip地址和port端口號。Sentinel無需用戶提供從服務器的地址信息,由master返回的slave的ip地址和port端口號,可以自動發現slave。

    當Sentinel發現master有新的slave出現時,Sentinel會為這個新的slave創建相應的實例外,Sentinel還會創建到slave的命令連接和訂閱連接。

    根據slave的INFO命令的回復,Sentinel會提取如下信息:

    1.slave的運行ID run_id

    2.slave的角色role

    3.master的ip地址和port端口

    4.master和slave的連接狀態master_link_status

    5.slave的優先級slave_priority

    6.slave的復制偏移量slave_repl_offset

  • Sentinel在默認情況下會以每兩秒一次的頻率,通過命令連接向所有被監視的master和slave的_sentinel_:hello頻道發送一條信息

    發送以下格式的命令:

?????PUBLISH?_sentinel_:hello???"<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"</m_epoch></m_port></m_ip></m_name></s_epoch></s_runid></s_port></s_ip>

以上命令相關參數意義:

參數 意義
s_ip Sentinel的ip地址
s_port Sentinel的端口號
s_runid Sentinel的運行ID
s_runid Sentinel的運行ID
m_name 主服務器的名字
m_ip 主服務器的IP地址
m_port 主服務器的端口號
m_epoch 主服務器當前的配置紀元
  • Sentinel與master或者slave建立訂閱連接之后,Sentinel就會通過訂閱連接發送對_sentinel_:hello頻道的訂閱,訂閱會持續到Sentinel與服務器的連接斷開為止

命令如下所示:

SUBSCRIBE sentinel:hello

深入淺析Redis中的sentinel故障轉移

如上圖所示,對于每個與Sentinel連接的服務器 ,Sentinel既可以通過命令連接向服務器頻道_sentinel_:hello頻道發送信息,又通過訂閱連接從服務器的_sentinel_:hello頻道接收信息。

  • sentinel間會相互感知,新加入的sentinel會向master的_sentinel_:hello頻道發布一條消息,包括自己的消息,其它該頻道訂閱者sentinel會發現新的sentinel。隨后新的sentinel和其它sentinel會創建長連接。

相互連接的各個Sentinel可以進行信息交換。Sentinel為master創建的實例結構中的sentinels字典保存了除Sentinel本身之外,所有同樣監視這個主服務器的其它Sentinel信息。

前面也講到sentinel會為slave創建實例(在master實例的slaves字典中)。現在我們也知道通過sentinel相互信息交換,也創建了其它sentinel的實例(在master實例的sentinels字典中)。我們將一個sentinel中保存的實例結構大概情況理一下,如下圖所示:

深入淺析Redis中的sentinel故障轉移

從上圖可以看到slave和sentinel字典的鍵由其ip地址和port端口組成,格式為ip:port,其字典的值為其對應的sentinelRedisInstance實例。

master的故障發現

主觀不可用

默認情況下Sentinel會以每秒一次的頻率向所有與它創建了命令連接的master(包括master、slave、其它Sentinel)發送PING命令,并通過實例返回的PING命令回復來判斷實例是否在線。

PING命令回復分為下面兩種情況:

  • 有效回復:實例返回 +PONG、-LOADING、-MASTERDOWN三種回復的一種

  • 無效回復:除上面有效回復外的其它回復或者在指定時限內沒有任何返回

Sentinel配置文件中的設置down-after-milliseconds毫秒時效內(各個sentinel可能配置的不相同),連續向Sentinel返回無效回復,那么sentinel將此實例置為主觀下線狀態,在sentinel中維護的該實例flags屬性中打開SRI_S_DOWN標識,例如master如下所示:

深入淺析Redis中的sentinel故障轉移

客觀不可用

在sentinel發現主觀不可用狀態后,它會將“主觀不可用狀態”發給其它sentinel進行確認,當確認的sentinel節點數>=quorum,則判定該master為客觀不可用,隨后進入failover流程。

上面說到將主觀不可用狀態發給其它sentinel使用如下命令:

SENTINEL?is-master-down-by-addr?<ip>?<port>?<current_epoch>?<runid></runid></current_epoch></port></ip>

各個參數的意義如下:

  • ip:被sentinel判斷為主觀下線的主服務器的ip地址
  • port: 被sentinel判斷為主觀下線的主服務器的port地址
  • current_epoch:sentinel的配置紀元,用于選舉領頭Sentinel
  • runid:可以為*號或者Sentinel的運行ID,*號代表檢測主服務器客觀下線狀態。Sentinel的運行ID用于選舉領頭Sentinel

接受到以上命令的sentinel會反回一條包含三個參數的Multi Bulk回復

1)down_state> 目標sentinel對該master檢查結果,1:master已下線 2:master未下線

2)leader_runid> 兩種情況,*表示僅用于檢測master下線狀態 ,否則表示局部領頭Sentinel的運行ID(選舉領頭Sentinel)

3)leader_epoch> 當leader_runid為時,leader_epoch始終為0。不為時則表示目標Sentinel的局部領頭Sentinel的配置紀元(用于選舉領頭Sentinel)

其中節點數量限制quorum為sentinel配置文件中配置的

sentinel?monitor?<master-name>?<ip>?<redis-port>?<quorum></quorum></redis-port></ip></master-name>

quorum選項,不同的sentinel配置的可能不相同。

當sentinel認為master為客觀下線狀態,則會將master屬性中的flags的SRI_O_DOWN標識打開,例如master如下圖所示:

深入淺析Redis中的sentinel故障轉移

選舉Sentinel Leader

當一臺master宕機時,可能多個sentinel節點同時發現并通過交互確認相互的“主觀不可用狀態”,同時達到“客觀不可用狀態”,同時打算發起failover。但最終只能有一個sentinel節點作為failover發起者,那么就需要選舉出Sentinel Leader,需要開始一個Sentinel Leader選舉過程。

Redis的Sentinel機制采用類似于Raft協議實現這個選舉算法:

1.sentinelState的epoch變量類似于raft協議中的term(選舉回合)。

2.每一個確認了master“客觀不可用”的sentinel節點都會向周圍廣播自己的參選請求(SENTINEL is-master-down-by-addr ,current_epoch為自己的配置紀元,run_id為自己的運行ID)

3.每一個接收到參選請求的sentinel節點如果還沒接收到其它參選請求,它就將本回合的意向置為首個參選sentinel并回復它(先到先得);如果已經在本回合表過意向了,則拒絕其它參選,并將已有意向回復(如上所介紹的三個參數的Multi Bulk回復,down_state為1,leader_runid為首次接收到的發起參選請求的源sentinel的運行ID,leader_epoch為首次接收到的發起參選請求的源sentinel的配置紀元)

4.每個發起參選請求的sentinel節點如果收到超過一半的意向同意某個參選sentinel(可能是自己),則確定該sentinel為leader。如果本回合持續了足夠長時間未選出leader,則開啟下一個回合

leader sentinel 確定之后,leader sentinel從master所有的slave中依據一定規則選取一個作為新的master

故障轉移failover

在選舉出Sentinel Leader之后,sentinel leader對已下線master執行故障轉移:

  • sentinel leader對已下線的master的所有slave中,選出一個狀態良好、數據完整的slave,然后向這個slave發送:SLAVEOF no one 命令,將這個slave轉換為master。

    我們來看下新的master是怎么挑選出來的?Sentinel leader會將已下線的所有slave保存到一個列表,然后按照以下規則過濾篩選:

  • 優先級最高的slave,redis.conf配置中replica-priority選項來標識,默認為100,replica-priority較低的優先級越高。0為特殊優先級,標志為不能升級為master。

  • 如果存在多個優先級相等的slave,則會選擇復制偏移量(offset)最大的slave(數據更加完整)

  • 如果存在多個優先級相等,最大復制偏移量最大的slave,則選擇運行ID最小的slave

選出需要升級為新的master的slave后,Sentinel Leader會向這個slave發送SLAVEOF no one 命令。之后Sentinel會以每秒一次頻率(平時是十秒一次)向被升級slave發送INFO,當回復的role由slave變為master時Sentinel Leader就會知道已升級為master。

  • sentinel leader 向已下線的master屬下的slave發送SLAVEOF命令(SLAVEOF ),去復制新的master

  • 將舊的master設置為新的master的slave,并繼續對其監視,當其重新上線時Sentinel會執行命令讓其成為新的master的slave。

總結

Sentinel是Redis高可用的解決方案,Sentinel集群的節點數需要>=3.

默認每十秒Sentinel對master和slave執行info,用于發現master變更信息,主從關系以及發現新的slave節點。

默認每兩秒sentinel通過命令連接向所有被監視的master和slave的_sentinel_:hello頻道發送一條信息,來和其它sentinel交互信息

默認每一秒sentinel向master,slave,其它sentinel發送PING命令來判斷對方是否下線

Sentinel Leader是按照一定規則選舉出來的。

由Sentinel Leader進行故障轉移操作,選舉出新的master來替代已下線的master。

更多編程相關知識,請訪問:redis!!

? 版權聲明
THE END
喜歡就支持一下吧
點贊6 分享