《Redis設計與實現》閱讀筆記11-Sentinel(哨兵)
15 哨兵
哨兵系統由一個或多個哨兵例項組成,可以監視任意多個主伺服器及其對應的所有從伺服器,並在監視的主伺服器下線的時候從其對應的從伺服器中選出一個作為新的主伺服器,然後讓剩餘的從伺服器去複製新的主伺服器,並在舊的主伺服器上線以後讓其成為新的主伺服器的從伺服器。
15.1 啟動並初始化哨兵
啟動一個 Sentinel 可以使用命令:
$ redis-sentinel /path/to/your/sentinel.conf
或者命令:
$ redis-server /path/to/your/sentinel.conf --sentinel
這兩個命令的效果完全相同。
當一個 Sentinel 啟動時, 它需要執行以下步驟:
- 初始化伺服器。
- 將普通 Redis 伺服器使用的程式碼替換成 Sentinel 專用程式碼。
- 初始化 Sentinel 狀態。
- 根據給定的配置檔案, 初始化 Sentinel 的監視主伺服器列表。
- 建立連向主伺服器的網路連線。
15.1.1 初始化伺服器
與普通的redis伺服器的初始化不同,哨兵伺服器有很多步驟不需要執行
功能 | 使用情況 |
---|---|
資料庫和鍵值對方面的命令, 比如 SET 、 DEL 、 FLUSHDB 。 | 不使用。 |
事務命令, 比如 MULTI 和 WATCH 。 | 不使用。 |
指令碼命令,比如 EVAL 。 | 不使用。 |
RDB 持久化命令, 比如 SAVE 和 BGSAVE 。 | 不使用。 |
AOF 持久化命令, 比如 BGREWRITEAOF 。 | 不使用。 |
複製命令,比如 SLAVEOF 。 | Sentinel 內部可以使用,但客戶端不可以使用。 |
釋出與訂閱命令, 比如 PUBLISH 和 SUBSCRIBE 。 | SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE PUNSUBSCRIBE 四個命令在 Sentinel 內部和客戶端都可以使用, 但 PUBLISH 命令只能在 Sentinel 內部使用。 |
檔案事件處理器(負責傳送命令請求、處理命令回覆)。 | Sentinel 內部使用, 但關聯的檔案事件處理器和普通 Redis 伺服器不同。 |
時間事件處理器(負責執行 serverCron 函式)。 | Sentinel 內部使用, 時間事件的處理器仍然是 serverCron 函式, serverCron 函式會呼叫 sentinel.c/sentinelTimer 函式, 後者包含了 Sentinel 要執行的所有操作。 |
15.1.2 使用哨兵專用程式碼
啟動哨兵以後,會將普通redis伺服器使用的程式碼替換成哨兵專用的程式碼,哨兵的伺服器使用也使用不同的伺服器,命令對應不同的實現函式。
這也解釋了為什麼在 Sentinel 模式下, Redis 伺服器不能執行諸如 SET 、 DBSIZE 、 EVAL 等等這些命令 —— 因為伺服器根本沒有在命令表中載入這些命令: PING 、 SENTINEL 、 INFO 、 SUBSCRIBE 、 UNSUBSCRIBE 、 PSUBSCRIBE 和 PUNSUBSCRIBE 這七個命令就是客戶端可以對 Sentinel 執行的全部命令了。
15.1.3 初始化哨兵狀態
在應用了 Sentinel 的專用程式碼之後, 接下來, 伺服器會初始化一個 sentinel.c/sentinelState 結構(後面簡稱“Sentinel 狀態”), 這個結構儲存了伺服器中所有和 Sentinel 功能有關的狀態 (伺服器的一般狀態仍然由 redis.h/redisServer 結構儲存):
struct sentinelState {
// 當前紀元,用於實現故障轉移
uint64_t current_epoch;
// 儲存了所有被這個 sentinel 監視的主伺服器
// 字典的鍵是主伺服器的名字
// 字典的值則是一個指向 sentinelRedisInstance 結構的指標
dict *masters;
// 是否進入了 TILT 模式?
int tilt;
// 目前正在執行的指令碼的數量
int running_scripts;
// 進入 TILT 模式的時間
mstime_t tilt_start_time;
// 最後一次執行時間處理器的時間
mstime_t previous_time;
// 一個 FIFO 佇列,包含了所有需要執行的使用者指令碼
list *scripts_queue;
} sentinel;
15.1.4 初始化哨兵狀態的master屬性
哨兵狀態的master屬性是一個字典結構,用主伺服器名字為鍵,以主伺服器對應的 sentinel.c/sentinelRedisInstance 結構為值。以此儲存哨兵監視的所有主伺服器。
sentinelRedisInstance 結構可以是一個主伺服器,也可以是一個從伺服器,還可以是一個哨兵。
typedef struct sentinelRedisInstance {
// 標識值,記錄了例項的型別,以及該例項的當前狀態
int flags;
// 例項的名字
// 主伺服器的名字由使用者在配置檔案中設定
// 從伺服器以及 Sentinel 的名字由 Sentinel 自動設定
// 格式為 ip:port ,例如 "127.0.0.1:26379"
char *name;
// 例項的執行 ID
char *runid;
// 配置紀元,用於實現故障轉移
uint64_t config_epoch;
// 例項的地址
sentinelAddr *addr;
// SENTINEL down-after-milliseconds 選項設定的值
// 例項無響應多少毫秒之後才會被判斷為主觀下線(subjectively down)
mstime_t down_after_period;
// SENTINEL monitor <master-name> <IP> <port> <quorum> 選項中的 quorum 引數
// 判斷這個例項為客觀下線(objectively down)所需的支援投票數量
int quorum;
// SENTINEL parallel-syncs <master-name> <number> 選項的值
// 在執行故障轉移操作時,可以同時對新的主伺服器進行同步的從伺服器數量
int parallel_syncs;
// SENTINEL failover-timeout <master-name> <ms> 選項的值
// 重新整理故障遷移狀態的最大時限
mstime_t failover_timeout;
// ...
} sentinelRedisInstance;
sentinelRedisInstance.addr 屬性是一個指向 sentinel.c/sentinelAddr 結構的指標, 這個結構儲存著例項的 IP 地址和埠號:
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
在初始化哨兵狀態的時候就會初始化他的master屬性,而master屬性的具體內容,由啟動哨兵的配置檔案來決定。
15.1.5 建立連向主伺服器的網路連線
初始化 Sentinel 的最後一步是建立連向被監視主伺服器的網路連線: Sentinel 將成為主伺服器的客戶端, 它可以向主伺服器傳送命令, 並從命令回覆中獲取相關的資訊。
對於每個被 Sentinel 監視的主伺服器來說, Sentinel 會建立兩個連向主伺服器的非同步網路連線:
- 一個是命令連線, 這個連線專門用於向主伺服器傳送命令, 並接收命令回覆。
- 另一個是訂閱連線, 這個連線專門用於訂閱主伺服器的 sentinel:hello 頻道。
為什麼有兩個連線?
在 Redis 目前的釋出與訂閱功能中, 被髮送的資訊都不會儲存在 Redis 伺服器裡面, 如果在資訊傳送時, 想要接收資訊的客戶端不線上或者斷線, 那麼這個客戶端就會丟失這條資訊。 因此, 為了不丟失 sentinel:hello 頻道的任何資訊, Sentinel 必須專門用一個訂閱連線來接收該頻道的資訊。 而另一方面, 除了訂閱頻道之外, Sentinel 還又必須向主伺服器傳送命令, 以此來與主伺服器進行通訊, 所以 Sentinel 還必須向主伺服器建立命令連線。 並且因為 Sentinel 需要與多個例項建立多個網路連線, 所以 Sentinel 使用的是非同步連線。
15.2 獲取主伺服器資訊
哨兵會以預設10秒一次的頻率向主伺服器傳送INFO命令來獲取主伺服器的資訊。哨兵一方面可以獲得主伺服器本身資訊(包括伺服器執行ID與在伺服器間的角色),另一方面能從主伺服器發回的資訊中分析出所有從伺服器的資訊,每個從伺服器都是以“slave”字串開頭的記錄,裡面包含了所有從伺服器的ip地址與埠號。
哨兵會利用主伺服器發回的自身資訊更新本身master屬性中的主伺服器例項結構(主伺服器重啟會更新執行ID),而從伺服器資訊則用於更新master屬性的主伺服器例項結構中的salves屬性(字典)中的從伺服器例項結構。
salves屬性的鍵是從伺服器的ip地址加埠結合而成(ip:port),如果這個從伺服器例項結構在salves屬性不存在就新建一個,如果存在就更新這個例項結構。
哨兵->master屬性(是一個字典,字典的值是主伺服器例項結構) 主伺服器例項結構->salves屬性(是一個字典,字典的值是主伺服器對應的從伺服器例項結構)
主伺服器例項結構中的flags屬性值為SRI_MASTER,從伺服器例項結構中的flags屬性值為SRI_SLAVE
15.3 獲取從伺服器資訊
哨兵不僅會與主伺服器建立訂閱連線和命令連線,同時也會與從伺服器建立訂閱連線和命令連線。
哨兵以每十秒一次的頻率向從伺服器傳送INFO命令,獲取從伺服器資訊(執行ID,角色資訊,ip和埠,連線狀態,優先順序,複製偏移量),並使用這些資訊對從伺服器例項結構進行更新。
15.4 向主從伺服器傳送訊息
哨兵會以預設2秒一次的頻率向所有被監視的主從伺服器傳送資訊,其中包括哨兵本身的資訊和其監視主伺服器的資訊(監視多個主伺服器時,傳送對應那組主從伺服器的主伺服器資訊)
15.5 接收主從伺服器的頻道資訊
每個哨兵都與其對應的主從伺服器建立命令連線和訂閱連線兩種連線。哨兵既會通過命令連線向伺服器的頻道傳送資訊,也會通過訂閱連線從這個頻道接收資訊。
一個哨兵向一個伺服器的頻道傳送的資訊,同時會被監視這個伺服器的其他哨兵接收到,監視同一個伺服器的哨兵們,通過這個伺服器為中轉就能發現彼此的存在,傳送的資訊在被其他哨兵接收到以後會用於更新其它哨兵對這個哨兵的認知。
如果收到訊息中哨兵ID與本身ID一樣就表示這是自己發的資訊,哨兵不做處理,不一樣時就會用於更新自己對其他哨兵的認知。
15.5.1 更新sentinels字典
當哨兵收到訂閱頻道發來的訊息時,就會分析訊息,如果確認訊息是其他哨兵傳送的,就先解讀訊息中的主伺服器資訊,根據主服務資訊,在自己的master字典中找到這個和其他哨兵一起監視的主伺服器。
接著訪問主伺服器例項結構的sentinels屬性(一個字典,儲存著其他監視這個伺服器的哨兵),再根據接收訊息中的哨兵資訊,在sentinels字典中查詢這個哨兵是否存在。
若存在就根據收到的資訊更新這個哨兵,若不存在就根據收到的資訊新建一個哨兵例項結構放入字典中。
字典的鍵為哨兵的ip地址和埠組成(ip:port),字典的值是哨兵對應的例項結構。
15.5.2 建立連向其他哨兵的命令連線
監視同一個主伺服器的哨兵之間彼此會建立命令連線(哨兵之間只有命令連線,沒有訂閱連線),使用命令連線可以使得哨兵之間進行資訊交換。
15.6 檢測主觀下線狀態
哨兵預設以每秒一次的頻率向其連線的例項(主伺服器,從伺服器,哨兵)傳送PING命令,並通過回覆來判定其狀態。
回覆為+PONG,-LOADING,-MSTERDOWN為有效回覆,其他回覆或沒有回覆為無效回覆。
哨兵配置檔案中的down-after-milliseconds指定了一個毫秒值,如果一個例項在down-after-milliseconds毫秒內連續向哨兵返回無效回覆,哨兵就會修改這個例項所對應的例項結構,將例項的flags屬性置位SRI_S_DOWN,表示其進入主觀下線狀態。
不同哨兵配置檔案中的down-after-milliseconds可能不同,所以對於例項是否進入主觀下線狀態,不同的哨兵可能有不同的判斷。
15.7 檢查客觀下線狀態
哨兵判定一個主伺服器為主觀下線狀態後就會詢問其他哨兵對這個主伺服器狀態的判斷,當哨兵從其他哨兵哪裡接收到足夠數量的已下線判斷後,哨兵就會把該主伺服器判定為客觀下線,並對其執行故障轉移操作。
15.7.1 傳送SENTINEL is-master-down-by-addr命令
哨兵使用SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
前兩個引數是主伺服器的地址和埠,後兩個引數是哨兵的配置紀元和執行ID(選取領頭哨兵時才使用)
15.7.2 接收SENTINEL is-master-down-by-addr命令
哨兵接收到SENTINEL is-master-down-by-addr命令後會根據傳遞的引數判斷主伺服器的狀態,並給哨兵一個包含三個引數的回覆
- down_state:下線判斷,1表示下線,0表示未下線
- leader_runid和leader_epoch:哨兵的執行ID和配置紀元,用於選舉領頭哨兵
15.7.3 接收SENTINEL is-master-down-by-addr命令回覆
接收到其他哨兵的回覆後,傳送命令的哨兵會根據回覆中確認主伺服器下線的數量決定主伺服器狀態,若達到指定數量,會把主伺服器例項結構中flags屬性的SRI_O_DOWN標識開啟,表示主伺服器客觀下線。
確認數量由配置檔案來決定,哨兵配置的quorm屬性。
由於不同的哨兵的配置可能不同,所以當所有哨兵對主伺服器狀態做出判斷以後,不同哨兵對主伺服器的狀態判斷不一樣。
15.8 選舉領頭哨兵
當一個主伺服器被判定為客觀下線以後,監視這個主伺服器的所有哨兵就會進行協商,選出一個領頭哨兵對下線的主伺服器進行故障轉移操作。
每個Sentinel都有成為領頭的能力,而且每次選舉無論是否成功,都會將配置紀元(confuguration epoch)的值自增,它實際上就是一個計數器。
區域性領頭:當一個Sentinel A向另一個Sentinel B傳送請求SENTINEL is-master-down-by-addr + (Sentinel A 的 runid )代表A想成為B的區域性領頭。
所以這種規則就是先到先得,最早向目標Sentinel傳送這個命令的必然成為它的Sentinel,後面的命令都會無效,當它的票數超過半數時,它就成為領頭Sentinel,然後對已經下線的主伺服器執行故障轉移操作。
15.9 故障轉移
在選舉出領頭的Sentinel之後,領頭Sentinel對已經下線的主伺服器執行故障轉移操作。步驟為:
在已下線的主伺服器屬下的所有從伺服器裡面,挑選一個從伺服器,並將其轉換為主伺服器。(根據從伺服器優先順序,相同優先順序選擇複製偏移量較大的從伺服器) 讓已下線屬下的所有從伺服器改為複製新的主伺服器,併成為新的主伺服器的從伺服器。 當舊的主伺服器重新上線之後,它就會成為新的主伺服器的從伺服器。