1.概述
Sentinel(哨崗、哨兵)是Redis高可用性的解決方案:由一個或多個Sentinel實例組成的Sentinel系統可以監視任意多個主服務器,以及這個主服務器下的從服務器,并在被監視主服務器下線時,自動將下線主服務器下的某個從服務器升級為新的主服務器,然后由新的主服務器代替已下線的主服務器繼續處理命令請求。
當主服務器下線時,Sentinel會采取如下操作:
Sentinel會選出一個從服務器作為新的主服務器,假設此時選出的從服務器為:從服務器2,此時系統會如下圖所示:
當已下線的主服務器重新上線后,變為如下所示:
2.啟動并初始化Sentinel
啟動Sentinel可以使用命令:
redis-sentinel /path/to/your/sentinel.conf
#或者命令
redis-server /path/to/your/sentinel.conf --sentinel
這兩個命令的效果完全一樣。
一個Sentinel實例啟動時,它需要執行以下步驟:
- 初始化服務器
- 將普通Redis服務器使用的代扣替換為Sentinel專用代碼
- 初始化Sentinel 狀態
- 根據給定的配置文件,初始化Sentinel的監視主服務器列表
- 創建連向主服務器的網絡連接
2.1 初始化服務器
Sentinel本質上是一個運行在特殊模式下的Redis服務器,Sentinel的啟動第一步就是啟動一個普通的Redis服務器。但是Sentinel和普通Redis服務器執行的工作不一樣,所以Sentinel的初始化過程和普通Redis服務器并不完全相同。
Sentinel在初始化時,不需要載入RDB文件或者AOF文件。
并且Sentinel向外提供的命令和普通Redis服務器也不是完全一樣的,像SET這一類命令Sentinel是沒有的。
2.2 使用Sentinel專用代碼
- 使用與普通Redis服務器不同的默認端口號
- 載入Sentinel需要使用的命令列表
Sentinle支持:PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE這七個命令
2.3 初始化Sentinel狀態
在應用了Sentinel的專用代碼之后,接下來服務器會初始化一個snetinel.c/sentinelState的結構,這個結構中保存了服務器中所有和Sentinel功能相關的狀態。
比較重要的一個屬性:
#保存了所有這個Sentinle監視的主服務器
#字典的鍵是主服務器的名字
#字典的值則是一個指向sentinelRedisInstance結構的指針
dict *masters;
2.4 初始化Sentinel狀態的masters屬性
對Sentinel狀態的初始化會引起對于masters字典的初始化,即初始化Sentinel監控的所有主服務器,它是根據Sentinel的配置文件來進行初始化的。
配置文件示例如下:
# master1 configure
sentinel monitor master1 127.0.0.1 6379 2
sentinel down-after-milliseconds master1 30000
sentinel parallel-syncs master1 1
sentinel failover-timeout master1 900000
# master2 configure
sentinel monitor master2 127.0.0.1 6379 2
sentinel down-after-milliseconds master2 30000
sentinel parallel-syncs master2 1
sentinel failover-timeout master2 900000
此時Sentinel將主服務器master1會創建為如下結構:
此時Sentinel狀態的結構如下所示:
2.5 創建連向主服務器的連接
初始化Sentinel的最后一步是創建連向主服務器的網絡連接。Sentinel將成為主服務器的客戶端,它可以向主服務器發送命令,并從命令回復中獲取相關信息。
Sentinel對每個被監視的主服務器會創建兩個異步網絡連接:
- 命令連接,這個連接專門用于向主服務器發送命令,并接收命令回復
- 訂閱連接,這個連接專門用于訂閱主服務器的sentinel:hello 頻道
為什么使用兩個連接?
Redis在使用訂閱功能時,如果在發送消息時,想要接收信息的客戶端不在線或者斷線,那么這個客戶端就會丟失這條消息,因此為了不丟失sentinel:hello 頻道的任何信息,sentinel必須開通一條專門的連接來接收該頻道的消息。但是由于Snetinel還必須向主服務器發送命令來獲取主服務器的相關信息,因此必須再開通一條命令連接。
因為Sentinel需要與多個實例創建多個網絡連接,所有Sentinel使用的異步連接
如下圖展示了一個Snetinel與兩個master建立連接的情況:
3.獲取主服務器信息
Sentinel會以每10秒一次的頻率,通過命令連接向被監視的主服務器發送INFO
命令,并通過分析INFO
的回復來獲取主服務器當前的狀態。
通過分析INFO
的回復,Sentinel可以獲取以下兩個方面的信息:
- 主服務器本身的信息,包括run_id域記錄的服務器運行ID,以及role域記錄的服務器角色
- 主服務器下從服務器的信息,每個從服務器都由一個
slave
字符串開頭的行記錄,每行的ip
記錄了從服務器的IP地址,port
記錄了從服務器的端口號,根據ip
和port
的信息Sentinel無需用戶來配置從服務器信息,即可自動發現從服務器。
根據run_id和role記錄的信息,Sentinel對主服務器的實例進行更新。
從服務器的信息會更新至主服務器實例結構中的slaves
字典中,這個字典記錄了主服務器下從服務器的名單:
- 字典的鍵是由Sentinel自動設置的從服務器的名字,格式為:ip:port
- 字典的值則是對應從服務器的實例結構。
具體結構如下圖所示:
注意:從服務器的flags
值為SRI_SLAVE
4.獲取從服務器信息
當Sentinel發現主服務器有新的從服務器出現時,Sentinel除了會為這個新的從服務器創建相應的實體結構之外,Sentinel還會創建連接到從服務器的命令連接和訂閱連接。
在創建命令連接后,會以每10秒一次的頻率發送INFO
命令,并解析返回的信息。提取出如下信息對從服務器實例進行更新:
- 從服務器運行run_id
- 從服務器角色role
- 主服務器的ip地址和端口號
- 主服務器的連接狀態:master_link_status
- 從服務器器優先級:slave_priority
- 從服務器的復制偏移量:slave_repl_offset
更新后從服務器結構如下所示:
5.向主服務器和從服務器發送消息
默認情況下Sentinel會以兩秒每次的頻率,通過命令連接向所有被監視的主服務器和從服務器發送如下格式的命令:
#s開頭的參數是Sentinel的信息
#m開頭的信息是主服務器的信息:如果Sentinel正在監視的為主服務器那么就是主服務器自身的信息;
#如果Sentinel監視的是從服務器那么就是從服務器復制的主服務器的信息
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
具體參數:
參數 | 含義 |
---|---|
s_ip | Sentinel的ip |
s_port | Sentinel的port |
s_runid | Sentinel的runid |
s_epoch | Sentinel當前的配置紀元(在選舉領頭Sentinel時使用) |
m_name | 主服務器的名字 |
m_ip | 主服務器的ip |
m_port | 主服務器的port |
m_epoch | 主服務器的配置紀元 |
6.接收來自主服務器和從服務器的頻道信息
Sentinel當與一個主服務器或從服務器建立器訂閱連接之后,Sentinel就會通過訂閱連接向服務器發送以下命令來進行訂閱消息:
SUBSCRIBE __sentinel__:hello
Senetinel會一直對sentinel:hello繼續訂閱直到Sentinel與服務器斷開連接為止。也就是說Sentinel既會通過命令連接向服務器發送sentinel:hello消息又會通過訂閱連接從服務器接收消息。
對于監視同一個服務器的多個Sentinel來說,一個Sentinel發送的消息會被其他Sentinel接收到,這些信息會被用于更新其他Sentinel對于發送消息的Sentnel的認知,也會被用于更新其他Sentinel對于被監視服務的認知。
當一個Sentinel從sentinel:hello收到一條信息時,Sentinel會對這條信息進行分析,提取出信息中的<s_ip>,<s_port>,<s_runid>等上面提到的8個參數:
- 如果記錄的<s_ip>與當前Sentinel一致,那么說明是自身發送的消息, 那么會丟棄這條消息
- 如果不一致,那么說明還有另外一個Sentinel在監視同一個服務器,接收消息的Sentinel會對器監視的主服務器實例結構中的sentinels字典進行更新
6.1 更新sentinels字典
- sentinels字典中鍵為Sentinel的名字,格式:ip:port
- sentinels字典中值為對應的Sentinel的實例結構
具體如下圖所示:
6.2 創建連接其他Sentinel的命令連接
Sentinel與Sentinel之間只會創建命令連接,不會創建訂閱連接,因為Sentinel需要通過接收主服務器和從服務器的訂閱信息來發現未知的Sentinel,對于相互已知的Sentinel不需要再建立訂閱連接來進行通信。
7.檢測主觀下線狀態
默認情況下Sentinel會向其監控的服務器(主服務器、從服務器、Sentinel)以每秒一次的頻率發送PING命令,并通過PING命令的回復來判斷具體實例的在線狀態。實例回復可以分為以下兩種情況:
- 有效回復:+PONG、-LOADING、-MASTERDOWN三種回復中的其中一種
- 無效回復:+PONG、-LOADING、-MASTERDOWN三種回復之外的回復,或者在指定時間內沒有回復
Sentinel的配置文件中的down-after-milliseconds
指定了Sentinel判斷實例進入主觀下線的時間長度:如果一個實例在down-after-milliseconds
毫秒內連續向Sentinel返回無效回復,那么Sentinel會修改這個實例對應的實例結構,在flags
屬性中打開SRI_S_DOWN
標識,以此來表示實例進入主觀下線狀態。
具體如下:
Sentinel的配置文件中的down-after-milliseconds
不但用來判斷主服務器的主觀下線時間,還用于此主服務器下所有的從服務器的主觀下線狀態的判斷。
每個Senttinel配置文件中down-after-milliseconds
的配置不一定會一樣,因此對于同一個服務器而言,一個Sentinel認為其下線但是另外一個Sentinel可能沒有認為其主觀下線。
8.檢測客觀下線狀態
當Sentinel將一個主服務器檢測為主觀下線后,為確認這個主服務器是否真的下線,它會向同時在監控這臺主服務器的其他Sentinel進行詢問,看他們是否也認為服務器進入下線狀態(可以是主觀下線或客觀下線),如果Sentinel從其他Sentinel哪里接收到足夠的數量的已下線判斷后,Sentinel就會將主服務器判定為客觀下線,對其執行故障轉移
檢測客觀下線主要分以下三步:
8.1 發送SENTINEL is-master-down-by-addr命令
命令格式:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
具體參數含義:
參數 | 含義 |
---|---|
ip | 被Sentinel判斷為主觀下線的主服務器ip地址 |
port | 被Sentinel判斷為主觀下線的主服務器port地址 |
current_epoch | Sentinel當前配置紀元,用于選舉領頭Sentinel |
runid | 可以為或Sentinel的運行ID;當為時,代表命令僅用于檢測主服務器客觀下線狀態;當為Sentinel的運行ID時,則用于選舉領頭Sentinel |
82. 接收SENTINEL is-master-down-by-addr命令
當一個Sentinel接收到另一個Sentinel發送的SENTINEL is-master-down-by-addr
時,會檢測當前監控的主服務的主觀下線狀態,并做出如下格式的回復:
- <down_state>
- <leader_runid>
- <leader_epoch>
具體參數含義:
參數 | 含義 |
---|---|
down_state | 目標Sentinel主服務器主觀下線狀態:1代表主服務已下線,0代表主服務器未下線 |
leader_runid | 可以為或目標Sentinel的局部領頭Sentinel的運行ID;當為時,代表命令僅用于檢測主服務器客觀下線狀態;當為目標Sentinel的局部領頭Sentinel的運行ID時,則用于選舉領頭Sentinel |
leader_epoch | 目標Sentinel局部領頭Sentinel的配置紀元,僅在leader_runid部位時有效,如果leader_runid為,則leader_epoch總是為0 |
8.3 接收SENTINEL is-master-down-by-addr命令回復
根據其他Sentinel返回的回復,Sentinel將統計其他Sentinel同意主服務器已下線的數量,這一數量達到配置指定的判斷客觀下線所需的數量時,Sentinel會將主服務器實例結構中的flags
屬性的SRI_O_DOWN
打開,表示主服務器已進入客觀下線狀態。具體如下圖所示:
配置文件中對于客觀下線的配置:
#此表示表示總共需要兩個Sentinel認為主服務器已進入主觀下線狀態,那么就可以判斷主服務為客觀下線
sentinel monitor master 127.0.0.1 6379 2
另:不同的Sentinel配置文件不同,因此對于同一主服務器認為其客觀下線的判斷也不一樣。
9.選舉領頭Sentinel
當一個主服務器被判定為主觀下線時,監控這個主服務器的各個Sentinel會進行協商,選舉一個領頭Sentinel,并由選舉出的這個領頭Sentinel對主服務器進行故障轉移。
具體選舉步驟如下:
1)所有在線Sentinel都有被選為領頭Sentinel的資格
2) 每次進行領頭Sentinel選舉之后,無論選舉是否成功,所有Sentinel的配置紀元值都會自增一次,配置紀元實際上就是一個計數器,并無其他特別之處
3) 在一個配置紀元里面,所有Sentinel都有一次將某個Sentinel設置為局部領頭Sentinel的機會,并且局部領頭Sentinel一旦設置,在這個配置紀元里面就不能在更改
4)每個發現主服務器客觀下線
的Sentinel都會要求其他Sentinel將其設置為局部領頭Sentinel
5)當一個Sentinel向另一個Sentinel(目標)發送SENTINEL is-master-down-by-addr時,并且命令中runid不為*而是源Sentinel自身的runid時,即表示源Sentinel要求目標Sentinel將其設置為自己的局部領頭Sentinel。設置局部領頭Sentinel的規則是先到先到,最先向目標Sentinel發送命令的源Sentinel被成功設置為目標Sentinel的局部領頭Sentinel。
6) 目標Sentinel在接收SENTINEL is-master-down-by-addr命令后,會向源Sentinel返回一條命令回復,回復中的<leader_runid>和<leader_epoch>分別記錄了被成功設置為自身局部領頭Sentinel的runid和epoch,源Sentinel在收到目標Sentinel返回的命令回復后,會對目標Sentinel回復的<leader_runid>和<leader_epoch>進行檢查,如果都與自身信息一致則表示目標Sentinel將自身設置為局部領頭Sentinel
7) 如果有半數以上的Sentinel將源Sentinel設置為局部領頭Sentinel,那么這個Sentinel就成為領頭Sentinel。因為需要半數以上,因此每次最多會有一個領個Sentinel被選舉出。
8) 如果在給定的時間內,沒有選出領頭Sentinel,那么各個Sentinel將在一段時間后再次進行選舉,直到選出領頭Sentinel
10.故障轉移
在選出領頭Sentinel之后,該Sentinel會對被判定為客觀下線的主服務器執行故障轉移:
1) 在已下線的主服務器屬下的從服務器里面挑選一個從服務器,并將其轉換為從服務器。
2) 讓已下線的主服務器下的剩余的從服務器改為復制新的主服務器
3) 將已下線的主服務器設置為新的主服務器的從服務器,當這個舊的主服務器重新上線時,它會成為新的主服務器的從服務器
10.1 選出新的主服務器
領頭Sentinel挑選出一個狀態良好、數據完整的從服務器,然后向這個從服務器發送SLAVEOF no one
命令,將這個從服務器轉換為主服務器。
具體挑選從服務器的規則如下:
- 刪除列表中所有處于下線或斷線狀態的從服務器
- 刪除最近5秒內沒有回復過領頭Sentinel的INFO命令的從服務器
- 刪除所有與已下線主服務器連接斷開查過 down-after-milliseconds*10時間的從服務器
- 根據從服務器的優先級進行排序
- 如果有多個同優先級的從服務器,那么按照復制偏移量進行排序
- 如果復制偏移量相同的從服務器出現多臺,那么將從服務器的運行id進行排序,并選出運行id最小的從服務器
11.參考資料
《Redis設計與實現》