哨兵模式
- 1、啟動并初始化sentinel
- 1.1 初始化服務器
- 1.2 使用Sentinel代碼
- 1.3 初始化sentinel狀態
- 1.4 初始化sentinel狀態的master屬性
- 1.5 創建連向主服務器的網絡連接
- 2、獲取主服務器信息
- 3、獲取從服務器的信息
- 4、向主從服務器發送信息
- 5、接受主從服務器的頻道信息
- 6、檢測主觀下線狀態
- 7、檢查客觀下線狀態
- 8、選擇領頭的sentinel
- 9、故障轉移
- 9.1 選擇新主服務器
- 9.2 修改從服務器復制目標
- 9.3 將舊的主服務器轉換為從服務器
如有侵權,請聯系~
如有錯誤,也歡迎批評指正~
本篇文章大部分是來自學習《Redis設計與實現》的筆記
哨兵(Sentinel)是Redis的高可用解決方案:由一個或者多個哨兵實例組成的哨兵系統,可以監視任意多個主服務器以及這些主服務器所屬下的所有從服務器,并且在被監視的主服務器下線時自動的將從某個服務器升級為新的主服務器,然后將新的主服務器替換已經下線的主服務器繼續處理命令請求。
1、啟動并初始化sentinel
啟動一個Sentinel可以使用如下命令:
redis-sentinel /path/your/sentinel.conf
redis-server /path/your/sentinel.conf --sentinel
啟動一個sentinel服務器,需要執行如下步驟:
- 初始化服務器
- 將普通的Redis服務器使用的代碼代替為Sentinel專用代碼
- 初始化Sentinel狀態
- 根據給定的配置文件,初始化Sentinel的監視主服務器列表
- 創建與主服務器的網絡連接
1.1 初始化服務器
因為Sentinel哨兵實例本質上就是一個運行在特殊模式下的Redis服務器,所以這一步初始化服務器就是初始化一個redis實例,參考Redis設計與實現-底層實現。
但是畢竟兩者工作的方式不一樣,所以初始化過程還是有所不一樣,例如哨兵初始化是不需要RDB文件或者AOF文件還原數據庫。
1.2 使用Sentinel代碼
將普通redis使用的代碼替換為sentinel哨兵使用的代碼。例如,redis的服務端端口號為6379,哨兵使用26379.
define REDIS_SERVERPORT 6379
define REDIS_SENTINEL_PORT 26379
哨兵的命令表也和redis服務器的不同,哨兵對客戶端提供的命令PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE,不對外提供set get等redis的基礎命令。
1.3 初始化sentinel狀態
接下來會初始化哨兵的狀態sentinelState結構,SentinelState 是 Redis Sentinel 內部用來表示全局狀態的數據結構。它包含了所有監控的主節點、從節點、哨兵實例以及配置信息。
typedef struct sentinelState {uint64_t current_epoch; // 當前的紀元(epoch),用于故障轉移選舉dict *masters; // 主節點字典,key 為被監控主節點名稱,value 為 sentinelRedisInstanceint tilt; // 是否處于傾斜模式(Tilt Mode)int running_scripts; // 當前正在運行的腳本數量mstime_t tilt_start_time; // 傾斜模式開始的時間mstime_t previous_time; // 上一次時間戳,用于檢測時間跳躍list *scripts_queue; // 腳本隊列,用于執行用戶定義的腳本
} sentinelState;
1.4 初始化sentinel狀態的master屬性
sentinel狀態會保存所有監控的主服務器的信息master,而這個master的key就是主服務器的名字,value就是sentinelRedisInstance。這一步就是初始化master字典。為什么只保存主服務器信息?因為從服務器是動態的,sentinel會通過主服務器進行獲取。
typedef struct sentinelRedisInstance {int flags; // 實例標志位(如主節點、從節點、哨兵等)char *name; // 實例名稱(僅主節點有名稱)redisAsyncContext *cc; // 與該實例的命令連接redisAsyncContext *pc; // 與該實例的發布/訂閱連接mstime_t last_pub_time; // 上一次發送 PUBLISH 消息的時間mstime_t last_hello_time; // 上一次收到 HELLO 消息的時間mstime_t last_master_down_time; // 上一次檢測到主節點下線的時間mstime_t down_after_period; // 判定實例下線的時間閾值mstime_t info_refresh; // INFO 命令刷新的時間間隔dict *renamed_commands; // 重命名的 Redis 命令映射int parallel_syncs; // 故障轉移后允許同時同步的從節點數量int quorum; // 判定主節點不可用所需的最小哨兵數量list *sentinels; // 監控該主節點的所有哨兵實例list *slaves; // 主節點的所有從節點struct sentinelRedisInstance *master; // 如果是從節點,指向其主節點...
} sentinelRedisInstance;
1.5 創建連向主服務器的網絡連接
哨兵sentinel會向主服務器建立網絡連接,針對于一個主服務器需要建立兩個網絡連接:
- 用于命令回復的網絡連接,主要用來向主服務器發送命令請求,接受命令回復
- 用于訂閱功能的網絡連接,用來訂閱主服務器的_sentinel_:hello頻道
2、獲取主服務器信息
sentinel哨兵會每10秒向監控的主服務器發送INFO命令,并通過主服務器返回的INFO命令回復進行分析更新相應主服務器的sentinelRedisInstance。INFO命令主服務器返回的結果如下:
# server部分
redis_version:6.2.6 # Redis 版本號
redis_mode:standalone # Redis 模式 (standalone 或 cluster)
os:Linux 5.4.0-42-generic x86_64 # 操作系統信息
arch_bits:64 # 架構位數 (32 或 64)
process_id:12345 # Redis 進程 ID
run_id:abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 # 運行 ID
tcp_port:6379 # Redis 服務端口
uptime_in_seconds:123456 # Redis 運行時間(秒)
uptime_in_days:1 # Redis 運行時間(天)
hz:10 # Redis 內部定時器頻率# client部分
connected_clients:10 # 當前連接的客戶端數量
client_recent_max_input_buffer:2 # 最大輸入緩沖區大小
client_recent_max_output_buffer:0 # 最大輸出緩沖區大小
blocked_clients:0 # 被阻塞的客戶端數量# replication部分
role:master # 角色:主節點
connected_slaves:2 # 已連接的從節點數量
slave0:ip=192.168.1.2,port=6379,state=online,offset=12345678,lag=0
slave1:ip=192.168.1.3,port=6379,state=online,offset=12345678,lag=1
master_replid:abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
master_replid2:00000000000000000000000000000000000000000000000000000000
master_repl_offset:12345678 # 主節點的復制偏移量
second_repl_offset:-1 # 第二個復制 ID 的偏移量
repl_backlog_active:1 # 是否啟用了復制積壓緩沖區
repl_backlog_size:1048576 # 復制積壓緩沖區大小
repl_backlog_first_byte_offset:12345 # 積壓緩沖區的第一個字節偏移量
repl_backlog_histlen:12345 # 積壓緩沖區的歷史長度....
通過分析上述命令回復會更新對應主服務器的sentinelRedisInstance,這個命令回復也會返回從服務器的信息。如果有新增的從服務器就會為從服務器創建新的sentinelRedisInstance,添加到主服務的sentinelRedisInstance.slaves屬性中。
3、獲取從服務器的信息
當有新的從服務器出現的時候,sentinel會在該主服務下的slaves中添加一個sentinelRedisInstance實例,并且和從服務器創建命令連接和訂閱連接。
命令創建完成之后,sentinel默認10秒通過命令連接向從服務器發一次info命令,并且返回:
# Replication
role:slave # 角色為從節點
master_host:127.0.0.1 # 主節點IP
master_port:6379 # 主節點端口
master_link_status:up # 與主節點的連接狀態(up/down)
master_last_io_seconds_ago:1 # 上次與主節點通信的秒數(反映復制延遲)
master_sync_in_progress:0 # 是否正在全量同步(0=否,1=是)
slave_repl_offset:123456 # 當前復制偏移量(用于評估數據同步進度)
slave_priority:100 # 從節點優先級(故障轉移時影響選舉)
slave_read_only:1 # 是否只讀模式(默認1)
# Server
redis_version:6.2.6 # Redis版本
os:Linux 5.4.0-80-generic # 運行的操作系統
uptime_in_seconds:86400 # 運行時間(秒)
# Clients
connected_clients:3 # 當前客戶端連接數
根據從服務器返回的數據,更新sentinelRedisInstance實例。
4、向主從服務器發送信息
sentinel每兩秒就會通過訂閱連接向主從服務器發送信息。當 Sentinel 在 sentinel:hello 頻道上發布消息時,消息內容通常包含以下信息:
publish __sentinel__:hello <sentinel-ip>,<sentinel-port>,<runid>,<current_epoch>,<master-name>,<master-ip>,<master-port>,<master-config-epoch>
- sentinel-ip 和 sentinel-port:當前 Sentinel 的地址。
- runid:當前 Sentinel 的運行 ID。
- current_epoch:當前的配置紀元(用于協調故障轉移)。
- master-name:被監控的主節點名稱。
- master-ip 和 master-port:主節點的地址。
- master-config-epoch:主節點的配置紀元。
5、接受主從服務器的頻道信息
有關頻道的訂閱關系,針對于一個主從服務Redis集群會分配單獨的訂閱頻道,即不同redis主服務器之間的頻道是相互獨立的。針對于同一個頻道,不僅所有的哨兵和redis主從服務器都訂閱這個頻道,而且他們也都可以向這個頻道發布消息。
所以通過這個頻道 redis服務器可以向監聽自己的所有sentinel發送消息。
通過上圖可以知道,一個哨兵sentinel發送消息之后除了redis服務器可以接收到數據之外,其他監聽這個redis主服務的sentinel也會收到消息。
一個sentinel發送消息之后,sentinel的處理:
- 如果發現接收到的sentinel runId和自己的一樣,說明是自己發送的,則丟棄
- 如果發現不一樣,則根據發送的消息解析出sentinel和主服務器的信息。通過主服務器的信息在自己的sentinelServer的master字典中找到這個主服務器的sentinelRedisInstance實例,然后查看該實例下sentinels【監聽這個主服務器的其他sentinel】是否存在。如果存在則更新數據;否則創建實例,并且與這個sentinel彼此創建命令連接。
sentinel和redis服務器的命令連接和訂閱關系。【本圖沒有畫各個sentinel與redis服務器的命令連接】(sentinel之間只有命令連接,沒有訂閱)
6、檢測主觀下線狀態
默認情況下,sentinel會每秒都會向與其建立命令連接的所有實例【主服務器、從服務器以及與其相連的sentinel】發送ping命令,并根據實例返回的命令回復是否有限【有效回復+PONG、- LOADING、- MASTERDOWN】進行判斷是否在線。
sentinel會根據配置文件中down-after-milliseconds參數判斷該實例是否為主觀下線。即一個實例連續down-after-milliseconds毫秒內一直無效回復,則標記為主觀下線,在該實例的sentinelRedisInstance的flags屬性設置為sri_s_down標識。
注意每個sentinel配置文件中的down-after-milliseconds值可能不同,有的可能為1000,有的可能為5000。所以,一個實例下線并不一定所有的sentinel都同時認為下線。
當一個 Sentinel 實例判斷另一個 Sentinel 實例為主觀下線后,它會執行以下操作:
- 記錄主觀下線狀態:當前 Sentinel 會將目標 Sentinel 標記為 主觀下線,并記錄其狀態。
這一狀態僅存在于當前 Sentinel 的視角中,并不會立即影響系統的整體行為。 - 通知其他 Sentinel 實例:通知其他 Sentinel 實例自己對目標 Sentinel 的判斷結果。其他 Sentinel 實例可以接收到這些消息,但它們會根據自己的健康檢查結果獨立判斷目標 Sentinel 是否也處于主觀下線狀態。
- 不觸發客觀下線
7、檢查客觀下線狀態
當sentinel判斷一個主服務器為主觀下線之后,為了進一步判斷是否真的下線,會問其他sentinel哨兵是否下線。當超過quorum個sentinel認為該主服務器下線,則就會將這個redis主服務器標記為客觀下線。【從服務器不需要進行判斷客觀下線,影響比較小】
SENTINEL IS-MASTER-DOWN-BY-ADDR 是 Redis Sentinel 提供的一個命令,用于檢查某個主節點是否被標記為下線(主觀下線或客觀下線),并參與領導者選舉的協商過程。sentinel通過命令連接進行發送的,不是通過hello頻道發送的。
SENTINEL IS-MASTER-DOWN-BY-ADDR命令:
SENTINEL IS-MASTER-DOWN-BY-ADDR <master-ip> <master-port> <current-epoch> <runid>
參數說明:
- master-ip和 master-port:指定要檢查的主節點的 IP 地址和端口號。
- current-epoch:當前的配置紀元(configuration epoch)。配置紀元是一個遞增的數字,用于協調多個 Sentinel 實例之間的狀態變更和領導者選舉。
- runid:如果該參數為 *,表示當前 Sentinel 只是詢問其他 Sentinel 是否認為主節點已下線。
如果該參數為具體的運行 ID(如當前 Sentinel 的 runid),表示當前 Sentinel 希望競選成為領導者。
目標 Sentinel 返回包含三個參數的響應:
- down_state:
- 1 表示目標 Sentinel 認為主節點已下線;
- 0 表示認為主節點在線。
- leader_runid:
- 若為 *,表示回復僅用于主節點狀態檢測;
- 若為具體 runid,表示目標 Sentinel 同意該 runid 對應的 Sentinel 成為故障轉移的領導者。
- leader_epoch: 目標 Sentinel 的局部領頭配置紀元(僅當 leader_runid 不為 * 時有效)
sentinel會根據該命令的回復判斷,是否超過quorum個sentinel回復了一下線。客觀下線則會將該redis主服務器實例的sentinelRedisInstance的flags屬性設置為sri_o_down標識。
同樣,不同的sentinel可能配置文件不一樣,從而會出現quorum大小不一樣,可能出現有的sentinel認為主服務器已經下線,有的認為沒有客觀下線。
8、選擇領頭的sentinel
只要判斷主機已經客觀下線之后,監聽這個主機的各個sentinel進行協商,選擇一個領頭的sentinel,由這個領頭的sentinel負責故障轉移。
- 源哨兵期望成為領頭的sentinel,所以源sentinel向目標sentinel發送sentinel is-master-down-by-addr命令,并且runid為源sentinel的runid。
- 目標sentinel會根據先到先得,如果沒有在這個記元投遞給其他sentinel,就會投遞給這個sentinel,回復的leader_runid和leader_epoch分別為源sentinel的runid和目標sentinel的epoch。否則就會拒絕
- 源sentinel接收到命令回復之后,會判斷epoch和自己的epoch是否一樣以及回復的runid是否等于自己的epoch。
- 如果某個sentinel超過半數以上,則會成為領頭sentinel執行故障轉移。如果都沒有滿足這一條件的,則過一段時間再進行選舉。每次選舉epoch都會加1,無論是否成功。
9、故障轉移
在選擇了領頭sentinel之后,由領頭sentinel負責故障轉移。進行故障轉移主要步驟為:從那些可用的從服務器中選擇主服務器、將其他從服務器復制新的主服務器、將已經下線的主服務器也作為新主服務器的從服務器。
9.1 選擇新主服務器
領頭sentinel從所有從服務器中選擇狀態良好、數據完整的從服務器作為新主服務器,并向這個從服務器發送slave no one命令,將這服務器設置為主服務器。選擇主服務的步驟大致如下:
- 從所有的從服務器中刪除處于下線的服務器
- 從所有的從服務器中刪除5秒內沒有回復info命令的服務器
- 然后從剩余的從服務器根據【 優先級 > 復制偏移量大 > 運行runId最小】的原則選擇作為從服務器作為主服務器
9.2 修改從服務器復制目標
將其他從服務器復制新的主服務器,領頭sentinel會向其他的從服務器發送slave <新主服務器ip> <新主服務器port>。
9.3 將舊的主服務器轉換為從服務器
接下來就是將舊的主服務器轉換為從服務器,當舊的主服務器重新啟動之后,sentinel會向它發送slave <新主服務器ip> <新主服務器port>命令。