redis 哨兵

black_monkey發表於2020-06-18

哨兵作用

哨兵(sentinel) 是一個分散式系統,是程式高可用性的一個保障。用於監視任意多個主伺服器,以及這些主伺服器屬下的所有從伺服器,當出現故障時通過投票機制選擇新的master並將所有slave連線到新的master。

監控

不斷地檢查master和slave是否正常執行 master存活檢測、master與slave執行情況檢測。

通知

當被監控地伺服器出現問題時,向其他(哨兵間,客戶端)傳送通知。

自動故障轉移

斷開master與slave連線,選取一個slave作為master,將其他slave連線到新的master,並告知客戶端新的伺服器地址。

注意

哨兵也是一臺redis伺服器,只是不提供資料服務,通常哨兵配置數量為單數

啟動哨兵

配置檔案

哨兵預設的配置檔案 sentinel.conf

一般的以 sentinel_port.conf 命名 哨兵的配置檔案

配置資訊

port  26379  (埠號)
dir  /tmp  (哨兵執行資訊儲存)
monitor mymaster 127.0.0.1 6379 2
# mymaster  (master 名字 隨意)
# 127.0.0.1 6379  (IP + 埠號)
# 2  (哨兵個數 //2 + 1  當有 2 個哨兵認為 master 掛了 就掛了)
down-after-milliseconds mymaster 30000 (單位 毫秒 )

parallel-syncs mymaster 1 ( 新的master 一次有多少個 slave 同步,設定的越小,完成資料同步的時間越長,響應的伺服器壓力越小。)
failover-timeout mymaster 180000( 3 分鐘 如果沒有同步完成 就判定為同步超時)

啟動

配置主從結構,以 1master 2 slave為例。

1 先啟動 master 和 slave

主從配置 參看 主從篇部落格主從

redis-server config_6379.conf
redis-server config_6380.conf
redis-server config_6381.conf

2 啟動哨兵

redis-sentinel sentinel_26379.conf
redis-sentinel sentinel_26380.conf
redis-sentinel sentinel_26381.conf

Sentinel 命令

PING:PONG
SENTINEL masters :列出所有被監視的主伺服器,以及這些主伺服器的當前狀態。
SENTINEL slaves :列出給定主伺服器的所有從伺服器,以及這些從伺服器的當前狀態。
SENTINEL get-master-addr-by-name : 返回給定名字的主伺服器的 IP 地址和埠號。 如果這個主伺服器正在執行故障轉移操作, 或者針對這個主伺服器的故障轉移操作已經完成, 那麼這個命令返回新的主伺服器的 IP 地址和埠號。
SENTINEL reset : 重置所有名字和給定模式 pattern 相匹配的主伺服器。 pattern 引數是一個 Glob 風格的模式。 重置操作清楚主伺服器目前的所有狀態, 包括正在執行中的故障轉移, 並移除目前已經發現和關聯的, 主伺服器的所有從伺服器和 Sentinel 。
SENTINEL failover : 當主伺服器失效時, 在不詢問其他 Sentinel 意見的情況下, 強制開始一次自動故障遷移 (不過發起故障轉移的 Sentinel 會向其他 Sentinel 傳送一個新的配置,其他 Sentinel 會根據這個配置進行相應的更新)。

初始化Sentinel

初始化伺服器

從下面啟動程式碼可以看出啟動方式由函式 checkForSentinelMode 來決定,是否使用 sentinel 的模式進行一個啟動, 新增的指令也是用的 sentinelcmds 的命令表

int checkForSentinelMode(int argc, char **argv) {
    int j;

    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

// 檢查伺服器是否以 Sentinel 模式啟動
server.sentinel_mode = checkForSentinelMode(argc,argv);

// 初始化伺服器
initServerConfig();  // 在第二步介紹該函式

// 如果伺服器以 Sentinel 模式啟動,那麼進行 Sentinel 功能相關的初始化
// 併為要監視的主伺服器建立一些相應的資料結構
if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}

從原始碼我們可以看出哨兵的啟動有兩種方式

redis-sentinel sentinel_xxx.conf
redis-server sentinel_xxx.conf --sentinel

無論哪種方式啟動redis,都會執行 initServerConfig ,不同的是 Sentinel 還會 執行initSentinelConfiginitSentinel 兩個初始化函式。接下來看看這兩個函式都幹了什麼~ 。

替換 Sentinel 的專用程式碼

initSentinelConfig() 這個函式會用 Sentinel 配置的屬性覆蓋伺服器預設的屬性。

void initSentinelConfig(void) {
    server.port = REDIS_SENTINEL_PORT;//26379
}

initSentinel() 會進行一個命令表的載入。一個主要的查詢命令 INFO 也不同於普通伺服器,而是使用一個特殊的版本。

// 初始化伺服器 Sentinel 伺服器
void initSentinel(void) {
    int j;

    // 刪除 普通 Redis 伺服器的命令表(該表用於普通模式)
    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);
        redisAssert(retval == DICT_OK);
    }

    /* 初始化 Sentinel 的狀態 這是為了故障轉移階段選取 切換執行者 記錄的狀態 */
    sentinel.current_epoch = 0;

    // 儲存 主伺服器 資訊的字典 (這裡記錄了監測的主伺服器的資訊)
    sentinel.masters = dictCreate(&instancesDictType,NULL);

    // 初始化 TILT 模式的相關選項
    sentinel.tilt = 0;
    sentinel.tilt_start_time = 0;
    sentinel.previous_time = mstime();

    // 初始化指令碼相關選項
    sentinel.running_scripts = 0;
    sentinel.scripts_queue = listCreate();
}

// sentinel 的指令集合
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},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};

初始化 Sentinel 狀態

在完成命令表載入之後,緊接著會進行 sentinelStatesentinelRedisInstance 結構的一個初始化。

Sentinel 狀態中的 masters 字典記錄了所有被監視的主伺服器資訊,鍵為伺服器名字,值為被監視主伺服器對應的sentinel.c/sentinelRedisInstance結構。每個sentinelRedisInstance例項結構代表監視一個Redis伺服器例項,這個例項可以是主伺服器,也可以是從伺服器,或者另外一個sentinel伺服器。

對於sentinelState的初始化將引發對masters字典的初始化,而masters字典的初始化是根據被該入的sentinel配置檔案(sentinel_26379.conf)來進行的。主要為被監控 masterip port

注意 這些都是有 sentinel 來維護和使用的。

sentinelState

struct sentinelState {

    // 當前紀元 用做故障轉移
    uint64_t current_epoch;     /* Current epoch. */

    // 儲存了所有被這個 sentinel 監視的主伺服器
    // 字典的鍵是主伺服器的名字
    // 字典的值則是一個指向 sentinelRedisInstance 結構的指標,可以是主伺服器,從伺服器或者其他sentinel節點
    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. */

} sentinel;

sentinelRedisInstance

name
例項的名字
主伺服器的名字由使用者在配置檔案中設定
從伺服器以及 Sentinel 的名字由 Sentinel 自動設定
格式為 ip:port ,例如 "127.0.0.1:26379"

runid
例項的執行 ID

sentinelAddr
例項的地址

主伺服器例項特有的屬性

sentinels
其他同樣監控這個主伺服器的所有 sentinel

slaves
如果這個例項代表的是一個主伺服器
那麼這個字典儲存著主伺服器屬下的從伺服器
字典的鍵是從伺服器的名字,字典的值是從伺服器對應的 sentinelRedisInstance 結構

quorum
判斷這個例項為客觀下線(objectively down)所需的支援投票數量

parallel_syncs
SENTINEL parallel-syncs 選項的值
在執行故障轉移操作時,可以同時對新的主伺服器進行同步的從伺服器數量

auth_pass
連線主伺服器和從伺服器所需的密碼

從伺服器例項特有的屬性

master_link_down_time
主從伺服器連線斷開的時間

slave_priority
從伺服器優先順序

slave_reconf_sent_time
執行故障轉移操作時,從伺服器傳送 SLAVEOF 命令的時間

master
主伺服器的例項(在本例項為從伺服器時使用)

slave_master_host
INFO 命令的回覆中記錄的主伺服器 IP

slave_master_port
INFO 命令的回覆中記錄的主伺服器埠號

slave_master_link_status
INFO 命令的回覆中記錄的主從伺服器連線狀態

slave_repl_offset
從伺服器的複製偏移量

結構中的 sentinelAddr 儲存著物件的 地址和埠。

/* Address object, used to describe an ip:port pair. */
/* 地址物件,用於儲存 IP 地址和埠 */
typedef struct sentinelAddr {
    char *ip;
    int port;
} sentinelAddr;

建立連線

sentinel 會先去連線 sentinel masters 中的每一個 master,並在每一個 mastersentinel之間建立兩個非同步連線 一個 命令連線 一個 訂閱連結。此時 sentinel將成為 master 的客戶端它可以向主伺服器傳送命令,並從命令回覆中獲取相關資訊。

命令連線

專門用於向主伺服器傳送命令,並接收命令回覆。比如sentinel向主伺服器傳送INFO命令。

訂閱連線

專門用於訂閱主伺服器的 _sentinel_:hello頻道。 比如 sentinel向主,從,其它sentinel傳送sentinel本身和主庫資訊。

redis在釋出與訂閱功能中,被髮送的資訊都不會儲存在redis伺服器中,若訊息到來時,需要接收的客戶端不線上或者斷線,那麼這個客戶端就會丟失這條資訊。為了不丟失_sentinel_:hello頻道的任何資訊,sentinel必須專門的用一個訂閱連線來接收該頻道的資訊。

獲取主伺服器資訊

Sentinel 預設會以每10秒一次的頻率向主伺服器傳送INFO命令,通過分析命令回覆來獲取主伺服器的當前資訊。Sentinel可以獲取以下兩方面的資訊:

1主伺服器本身的資訊,包括伺服器run_id,role的伺服器角色。

2 主伺服器對應的所有從伺服器的資訊(從伺服器IP和埠)。

獲取從伺服器資訊

Sentinel發現有新的從伺服器出現時,Sentinel除了會為這個新的從伺服器建立相應的例項結構(sentinelRedisInstance)之外,還會建立到從伺服器的命令連線訂閱連線

Sentinel依然會像對待主伺服器那樣,每10s 傳送一個INFO命令來獲取從伺服器的當前資訊。

run_id、role、ip、port 、master_link_status(主從伺服器的連線狀態)、slave_priority(從伺服器的優先順序)等資訊。

向主從伺服器傳送資訊

在預設情況下, Sentinel會以每2秒一次的頻率,通過命令連線向,所有被監視的主伺服器和從伺服器傳送以下格式的命令:

PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

這條命令向伺服器的_sentinel_:hello頻道傳送了一條資訊,資訊的內容由多個引數組成:

(1) s_開頭的引數記錄的是sentinel本身的資訊。

(2) m_開頭的引數記錄的則是主伺服器的資訊,如果sentinel正在監視的是主伺服器,那麼這些引數就是主伺服器的資訊,如果sentinel正在監視的是從伺服器,那麼這些引數記錄就是從伺服器正在複製的主伺服器的資訊。

引數 描述
S_ip Sentinel的ip地址
S_port Sentinel的埠號
S_runid Sentinel的執行ID
S_epoch Sentinel 的當前配置紀元
m_name 主伺服器的名字
M_ip 主伺服器的IP地址
M_port 主伺服器的埠號
M_epoch 主伺服器的當前配置紀元

例如

"127.0.0.1,26379,e955b4c77398ef6b5f055bc7ebfd5e828dbed4fc,0,mymaster,127.0.0.1,6379,0"
# --------------------------------解釋------------------------------------------
127.0.0.1  # sentinel ip 地址
26379  # sentinel 埠號
e955b4c77398ef6b5f055bc7ebfd5e828dbed4fc  # sentinel的執行 id
0 # sentinel 當前配置紀元
mymaster # sentinel 監控的 master name
127.0.0.1 # master ip 地址
6379 # master 埠號
0 # master 當前配置紀元

接收來自主從伺服器的頻道資訊

Sentinel與一個主伺服器或者從伺服器建立起訂閱連線之後,Sentinel就會通過訂閱連線向伺服器傳送 subscribe_sentinel_:hello

對於每個與 Sentinel 連線的伺服器,Sentinel既通過命令連向伺服器的_sentinel_:hello頻道傳送資訊,又通過訂閱連線從伺服器的_sentinel_:hello頻道接收資訊。

因此當有新的Sentinel 連線進來時, 向訂閱連線中傳送的 subscribe_sentinel_:hello 被已有的Sentinel 接收(同時自己也會接受到來自自己的這條訊息)。

// 傳送 PUBLISH 命令的間隔
#define SENTINEL_PUBLISH_PERIOD 2000

if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
        /* PUBLISH hello messages to all the three kinds of instances. */
        sentinelSendHello(ri);
    }

/* 接收來自主伺服器和從伺服器的頻道資訊
當 sentinel 與一個主伺服器或者從伺服器建立起訂閱連線之後, sentinel 就會通過訂閱連線,向伺服器傳送以下命令:
*/
SUBSCRIBE __sentinel__:hello

/* Now we subscribe to the Sentinels "Hello" channel. */
// 傳送 SUBSCRIBE __sentinel__:hello 命令,訂閱頻道
retval = redisAsyncCommand(ri->pc,
        sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
        SENTINEL_HELLO_CHANNEL);

當一個Sentinel_sentinel_:hello頻道收到一條資訊時,Sentinel會對這條資訊進行分析,提取出資訊中 ip 、port、run_id 等8個引數,並進行以下檢查:如果這條訊息是自己發的,就直接忽略。如果是新進來的Sentinel , 此時Sentinel 會對 對應的主伺服器例項結構進行更新,即將新加進來的 Sentinel 新增到 sentinels 字典中。

每個Sentinel都有自己的一個sentinels字典,Sentinels字典資訊儲存了除自己之外的所有Sentinel資訊。

下線狀態

對於Redis的Sentinel中關於下線有兩個不同的概念:(1)主觀下線(Subjectively Down, 簡稱 Sdown) 指的是單個 Sentinel 例項對伺服器做出的下線判斷,此時不會進行故障轉移。(2) 客觀下線(Objectively Down, 簡稱 Odown)指的是多個 Sentinel 例項在對同一個伺服器做出 Sdown 判斷,此時目標sentinel會對主伺服器進行故障轉移。本篇具體詳細介紹。

主觀下線狀態

預設的Sentinel會以每秒一次的頻率向所有與它建立命令連線的例項(包括主、從、其他sentinel在內)傳送PING命令,並通過例項回覆來判斷例項是否線上。

合法的回覆

+pong-loading -masterdown

無效回覆

除此之外的所有回覆或者無回覆都被視作無效回覆。無回覆指在指定的時間內沒有回覆就認為是無回覆。

down-after-milliseconds  # 指定的時間 未收到回覆 視為無效

使用者設定down-after-milliseconds選項的值,不僅會被sentinel用來判斷主伺服器的主觀下線狀態,還會被用於判斷主伺服器下的所有從伺服器,以及同樣監視主伺服器的其他sentinel的主觀下線狀態。

-- 例如使用者向sentinel設定以了下配置:
sentinel  monitor master 127.0.0.1 6379 2
sentinel  down-after-milliseconds master 50000

這裡的master是主伺服器的名稱, 埠預設63792代表sentinel叢集中有2sentinel認為master 狀態下線時,才能真正認為該master已經不可用了(也就是客觀下線)。

這50000毫秒不僅會成為sentinel判斷master進入主觀下線的標準,還會判斷所有從庫、其它sentinel進入主觀下線的標準。

當多個sentinel設定的主觀下線時長可能不同

對於多個sentinel共同監視同一個主伺服器時,這些sentinel在配置檔案sentinle.conf中所設定的down-after-milliseconds值也可能不同,因此當一個sentinel將主伺服器判斷為主觀下線時,其它sentinel可能仍然會認為主伺服器處於線上狀態。只有全部的sentine都判斷進入了主觀下線狀態時,才會認為主master進入了主觀下線狀態。

客觀下線狀態

Sentinel將一個主伺服器判斷為主觀下線之後,為了確認這個主伺服器是否真的下線了,會向同樣監視這一主伺服器的其它Sentinel進行詢問,當有半數以上(看具體配置, 一般的是半數以上 例如sentinel monitor mymaster 127.0.0.1 6379 2 中 就為當 2 個判定下線時,就認為時客觀下線了)

master, 被確定客觀下線之後sentinel 們 會選出一個 決策者 去執行故障轉移操作。客觀下線條件只適用於主伺服器

is-master-down-by-addr命令用來判斷是否客觀下線

sentinel is-master-down-by-addr  ip  port  current_epoch  run_id

sentinel當前的配置紀元 current_epoch 用於選舉 決策者 sentinel, run_id可以是*或者sentinel的 執行id。

決策者選取

假設現在有4個sentinel 這四個sentinel 既是投票者,也是候選者(這四個必須時健康的)。

1 不能有下面三個標記中的一個:SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED

2 ping 心跳正常

3 優先順序不能為 0(slave->slave_priority)

4 INFO 資料不能超時

5 主從連線斷線會時間不能超時

投票的過程很簡單,每個sentinel 都將自己的ipportcurrent_epochrun_idis-master-down 傳送到 hello 頻道。

sentinel 第一個獲取到誰的 is-master-down 資訊, 就將自己的票投給對應的sentinel

一次過後 current_epoch 最大的,且超過了半數以上。則被選為決策者 否則再來一輪,每增加一輪 current_epoch + 1, 直到選出為止。

故障轉移

選取候選Slave

1 線上的

2 響應速度快的

3 與原 master 斷開連線最短的

4 優先原則

優先順序>offset>runid

最終選取出 新的 master 之後向新的 master 傳送

slaveof no one  # 斷開主從

然後宣告新的master

slaveof ip port  # 傳送新的IP 和  新的port

最後將原來的 master 作為從機。當重新上線時,sentinel 會傳送 salveof 命令使其成為從機。

總結

  • sentinel只是一個執行在特殊模式下的redis伺服器,它使用了和普通模式不同的命令表,以及區別與普通模式下使用的命令不同。

  • sentinel向主伺服器傳送INFO命令來獲得主伺服器屬下所有從伺服器的地址資訊,併為這些從伺服器建立相應的例項結構,以及連向這些從伺服器的命令連線和訂閱連線。

  • 一般情況下,sentinel以每10秒一次的頻率向被監視的主伺服器和從伺服器傳送INFO命令,當主伺服器處於下線狀態,或者sentinel正在對主伺服器進行故障轉移操作時,sentinel向從伺服器傳送INFO命令的頻率會改為1秒一次。

  • 對於監視同一個主伺服器和從伺服器的多個sentinel來說,它們會以每2秒一次的頻率,通過向被監視的_sentinel_:hello頻道傳送訊息來向其他sentinel宣告自己的存在。

  • 每個sentinel也會從_sentinel_:hello中頻道中接收其他sentinel發來的資訊,並根據這些資訊為其他sentinel建立相應的例項結構,以及命令連線。

  • sentinel只會與主伺服器和從伺服器建立命令連線和訂閱連線,sentinelsentinel之間則只建立命令連線。

  • sentinel以每秒一次的頻率向例項(包括主,從,其它sentinel)傳送PING命令,並根據例項的回覆來判斷例項是否線上,當一個例項在指定的時長中連續向sentinel傳送無效回覆時,sentinel會將這個例項判斷為主觀下線。

  • sentinel將一個主伺服器判斷為主觀下線時,它會向同樣的監視這個主伺服器的其他sentinel進行詢問,看它們是否同意這個主伺服器已經進入主觀下線狀態。

  • sentinel收集到足夠多的主觀下線投票之後,它會將主伺服器判斷為客觀下線,併發起一次針對主伺服器的故障轉移操作。

相關文章