當(dāng)前位置:首頁 > IT技術(shù) > 其他 > 正文

Redis設(shè)計(jì)與實(shí)現(xiàn)2.2:Sentinel
2022-04-29 14:06:18

Sentinel哨兵

這是《Redis設(shè)計(jì)與實(shí)現(xiàn)》系列的文章,系列導(dǎo)航:Redis設(shè)計(jì)與實(shí)現(xiàn)筆記

哨兵:監(jiān)視、通知、自動(dòng)故障恢復(fù)

啟動(dòng)與初始化

Sentinel 的本質(zhì)只是一個(gè)運(yùn)行在特殊模式下的 Redis 服務(wù)器,所以啟動(dòng) Sentinel 的步驟如下:

  1. 初始化一個(gè)普通的 Redis 服務(wù)器,不過也有一些不同:

    image_lymtics

  2. 將一部分 Redis 服務(wù)器使用的代碼替換成 Sentinel 專用代碼

    舉兩個(gè)例子:

    1. 服務(wù)器端口由 redis.h/REDIS_SERVERPORT 修改為 sentinel.c/REDIS_SENTINELPORT

    2. 服務(wù)器的命令表替換為 sentinel.c/sentinelcmds

      // 服務(wù)器在 sentinel 模式下可執(zhí)行的命令
      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}
      };
      
  3. 初始化 Sentinel 狀態(tài)

    可以看一下這個(gè)狀態(tài)的定義:

    /* Main state. */
    /* Sentinel 的狀態(tài)結(jié)構(gòu) */
    struct sentinelState {
    
    // 當(dāng)前紀(jì)元
    uint64_t current_epoch;     /* Current epoch. */
    
    // 保存了所有被這個(gè) sentinel 監(jiān)視的主服務(wù)器
    // 字典的鍵是主服務(wù)器的名字
    // 字典的值則是一個(gè)指向 sentinelRedisInstance 結(jié)構(gòu)的指針
    dict *masters; 
    
    // 是否進(jìn)入了 TILT 模式?
    int tilt;           /* Are we in TILT mode? */
    
    // 目前正在執(zhí)行的腳本的數(shù)量
    int running_scripts;    /* Number of scripts in execution right now. */
    
    // 進(jìn)入 TILT 模式的時(shí)間
    mstime_t tilt_start_time;   /* When TITL started. */
    
    // 最后一次執(zhí)行時(shí)間處理器的時(shí)間
    mstime_t previous_time;     /* Last time we ran the time handler. */
    
    // 一個(gè) FIFO 隊(duì)列,包含了所有需要執(zhí)行的用戶腳本
    list *scripts_queue;    /* Queue of user scripts to execute. */
    
    } sentinel;
    
  4. 初始化 Sentinel 狀態(tài)的 masters 屬性

    dict *masters; 是一個(gè)字典結(jié)構(gòu),鍵是被監(jiān)視主服務(wù)器的名稱,值是主服務(wù)器對應(yīng)的 sentinel.c/sentinelReidsInstance 結(jié)構(gòu)

    這個(gè)初始化是根據(jù)被載入的 Sentinel 配置文件來進(jìn)行的

  5. 創(chuàng)建網(wǎng)絡(luò)連接

    Sentinel 將成為主服務(wù)器的客戶端,它可以向主服務(wù)器發(fā)送命令,并從命令回復(fù)中獲取相關(guān)的信息。會(huì)創(chuàng)建兩個(gè)連向主服務(wù)器的異步網(wǎng)絡(luò)連接:

    • 一個(gè)是命令連接,專門用于向主服務(wù)器發(fā)送命令,并接收命令回復(fù)
    • 一個(gè)是訂閱連接,專用用于訂閱主服務(wù)器的 __sentinel__:hello 頻道(訂閱的好處是可以防止消息丟失)

與服務(wù)器進(jìn)行通信

獲取主服務(wù)器信息

image_lymtics

如上圖所示:

  • Sentinel 默認(rèn)會(huì)以每10秒一次的頻率發(fā)送 INFO 命令,獲取主節(jié)點(diǎn)的信息
  • 主節(jié)點(diǎn)會(huì)返回自身和其從節(jié)點(diǎn)的信息
  • Sentinel 接收到信息后,更新自己的 masters 字典,如果有新節(jié)點(diǎn),則創(chuàng)建之

獲取從服務(wù)器信息

當(dāng) Sentinel 發(fā)現(xiàn)主服務(wù)器有新的從節(jié)點(diǎn)時(shí),會(huì)創(chuàng)建到從節(jié)點(diǎn)的命令連接和訂閱鏈接:

image_lymtics

同樣的,以10秒一次的頻率發(fā)送 INFO 命令并獲取返回信息:

image_lymtics

并更新自己保存的信息。

發(fā)送頻道信息

默認(rèn)情況下,Sentinel會(huì)以每兩秒一次的頻率,通過命令向所有被監(jiān)視的主服務(wù)器和從服務(wù)器發(fā)送命令:

PUBLISH __sentinel__:hello "xxx"

這條命令向服務(wù)器的 __sentinel__ 頻道發(fā)送了一條消息,在上面我用"xxx"表示出來了,其具體組成有:

image_lymtics

即兩部分:

  • 自己的信息
  • 主服務(wù)器的信息

接收頻道消息

前面提到了,Sentinel 會(huì)向服務(wù)器的頻道發(fā)送信息:

PUBLISH __sentinel__:hello "xxx"

另一方面,Sentinel 還會(huì)訂閱所有被監(jiān)視服務(wù)器的頻道:

SUBSCRIBE __sentinel__:hello

對于監(jiān)視同一個(gè)服務(wù)器的多個(gè) Sentinel 來說,這些消息會(huì)被用于更新其他 Sentinel 對發(fā)送信息的 Sentinel 的認(rèn)知,也會(huì)被用于更新其他 Sentinel 對被監(jiān)視服務(wù)器的認(rèn)知。

sequenceDiagram participant s1 as Sentinel participant f as 服務(wù)器的hello頻道 participant s2 as Sentinel s1 ->> f: "messageA" f -->> s2: "messageA" note over s2: 更新相關(guān)數(shù)據(jù) f -->> s1: "messageA" note over s1: 是我自己發(fā)的啊,那沒事了 s2 ->> f: "messageB" f -->> s1: "messageB" note over s1: 更新相關(guān)數(shù)據(jù) f -->> s2: "messageB" note over s2: 是我自己發(fā)的啊,那沒事了

而更新的具體數(shù)據(jù)是:sentinelState 結(jié)構(gòu)體的 dict *masters; 變量(上文提到過)指向的 sentinelRedisInstancesentinels 字典變量(這個(gè)變量保存了所有監(jiān)視這個(gè)服務(wù)器的 Sentinel)

  • 鍵位Sentinel的IP和端口
  • 值指向sentinel實(shí)例

image_lymtics

而具體的更新流程是:

flowchart LR A[/獲取一條信息/] --> B{{是我發(fā)的嗎}} --Y--> C[/那沒事了/] B --N--> 提取數(shù)據(jù) --> D{{是否之前見過這個(gè)Sentinel}} --Y--> E[/更新/] D --N--> F[/添加/]

這樣做的一個(gè)好處是,可以自動(dòng)發(fā)現(xiàn)其他 Sentinel,并形成相互連接的網(wǎng)絡(luò),而無需手動(dòng)配置。

Sentinel 之間只會(huì)創(chuàng)建命令鏈接,而不會(huì)創(chuàng)建訂閱鏈接。

因?yàn)橹院头?wù)器需要?jiǎng)?chuàng)建訂閱鏈接就是用來發(fā)現(xiàn)未知的新的 Sentinel 的。

服務(wù)器意外狀態(tài)

檢測主觀下線狀態(tài)

Sentinel 會(huì)以每秒一次的頻率向所有與他建立了命令簡介的實(shí)例(包括主、從、Sentinel服務(wù)器)發(fā)送 PING 命令,并通過返回信息判斷實(shí)例的狀態(tài)。

sequenceDiagram participant Sl as Slaver participant M as Master participant Se as Sentinel note over Se: 以本實(shí)例的視角來看 participant Se2 as Sentinel loop Every Second Se ->>+ M: PING Se ->>+ Sl: PING Se ->>+ Se2: PING M ->>- Se: REPLY Sl ->>- Se: REPLY Se2 ->>- Se: REPLY end

實(shí)例對 PING 的回復(fù)有兩種:

  • 有效回復(fù):+PING、-LOADING、-MASTERDOWN
  • 無效回復(fù):其他內(nèi)容或超時(shí)

如果一個(gè)實(shí)例在 down-after-milliseconds 配置的時(shí)間內(nèi)沒有返回有效回復(fù),就會(huì)被標(biāo)記為主觀下線狀態(tài)

檢測客觀下線狀態(tài)

Sentinel 也要問問別的監(jiān)控目標(biāo)的 Sentinel 的意見,才好決定是否是真的下線了。

sequenceDiagram participant s1 as sentinel participant s2 as sentinel participant s3 as sentinel note over s2: 我先發(fā)現(xiàn)的 s2 ->>+ s1: is-master-down-by-addr s2 ->>+ s3: is-master-down-by-addr s1 ->> s1: 解析、檢查 s3 ->> s3: 解析、檢查 s1 ->>- s2: multi bulk s3 ->>- s2: multi bulk s2 ->> s2: 匯總結(jié)果 note over s2: 認(rèn)為主節(jié)點(diǎn)客觀下線 note over s2: 我們來進(jìn)行選舉吧!

is-master-down-by-addr 有幾個(gè)參數(shù),包含了:

  • ip、port:被審判的主機(jī)的ip和端口號(hào)
  • current_epoch:當(dāng)前的配置紀(jì)元,用以選舉領(lǐng)頭 Sentinel 進(jìn)行故障轉(zhuǎn)移
  • runid:
    • * 表示判斷客觀下線
    • 如果是 Sentinel 的運(yùn)行 ID 則用來選舉領(lǐng)頭

multi bulk 是 Sentinel 的返回值(為什么叫這個(gè)名字?文檔是這么叫的),包含了三個(gè)值:

  • down_state:是否下線
  • leader_runid:
    • * 表示僅僅用以檢測服務(wù)器的下線狀態(tài)
    • 如果是領(lǐng)頭 Sentinel 的 ID 則說明用于選舉領(lǐng)頭 Sentinel
  • leader_epoch:
    • 如果leader_runid為 * ,則為0
    • 否則為配置紀(jì)元

你應(yīng)該看出來了,上面的兩條命令有兩種作用:

  • 判斷是否下線
  • 選舉領(lǐng)頭 Sentinel

選舉領(lǐng)頭 Sentinel

當(dāng)一個(gè)主服務(wù)器被判斷為客觀下線后,監(jiān)視這個(gè)服務(wù)器的各個(gè) Sentinel 會(huì)進(jìn)行協(xié)商,選舉一個(gè)領(lǐng)頭的 Sentinel 并進(jìn)行故障轉(zhuǎn)移。

我的理解:

這里只有中間的 Sentinel 確定了客觀下線這一事實(shí),其他的 Sentinel 未必認(rèn)同,但是即便如此,只要有一個(gè) Sentinel 認(rèn)定了客觀下線的情況,其他 Sentinel 也會(huì)配合進(jìn)行選舉、故障轉(zhuǎn)移。

選舉的策略是:

  • 所有人都有機(jī)會(huì)當(dāng)選
  • 發(fā)現(xiàn)主觀下線的會(huì)向其他選手拉選票
  • 所有人都是給第一個(gè)要求投票的人
  • 超過一半選票的人當(dāng)選
sequenceDiagram participant s1 as Sentinel participant s2 as Sentinel participant s3 as Sentinel note over s1,s3: 我們都有機(jī)會(huì)成為Leader note over s1,s2: 我們都發(fā)現(xiàn)了目標(biāo)的主觀下線 loop 如果沒有選出leader s1 ->>+ s2: 請?jiān)趀poch任期選舉我,我是S1 s2 ->>- s1: 同意 s1 ->>+ s3: 請?jiān)趀poch任期選舉我,我是S1 note over s3: 好的,我這里先到先得 s3 ->>- s1: 同意 s2 ->>+ s3: 請?jiān)趀poch任期選舉我,我是S2 s2 ->>+ s1: 請?jiān)趀poch任期選舉我,我是S2 s1 ->>- s2: 同意 s3 ->>- s2: 抱歉,我選過別人了 note over s1,s3: 不管結(jié)果如何,都要epoch++ end s1 -> s1: 選票超過一半,當(dāng)選leader

如果在給定時(shí)限中沒有選出leader,則在一段時(shí)間后再次進(jìn)行選舉,直到選出leader。

這么一種做法有沒有可能在很長的一段時(shí)間內(nèi)都發(fā)生選舉失敗的情況呢?

這個(gè)可能要之后學(xué)習(xí)一下Raft算法的領(lǐng)頭選舉算法。

故障轉(zhuǎn)移

領(lǐng)頭 leader 將對已下線的主服務(wù)進(jìn)行故障轉(zhuǎn)移操作:

  1. 選一個(gè)新的主服務(wù)器
  2. 讓前任主服務(wù)器的所有從服務(wù)器跟著現(xiàn)任服務(wù)器
  3. 將前任設(shè)置為現(xiàn)任的從服務(wù)器

如何選新的服務(wù)器:

  • 篩選排除:
    • 下線的、斷線狀態(tài)的
    • 最近5秒內(nèi)都沒有回復(fù)過leader的INFO命令的服務(wù)器
    • 與前任主服務(wù)器斷開超過 down-after-milliseconds * 10 的服務(wù)器
  • 優(yōu)先選擇:
    • 優(yōu)先級較高
    • 復(fù)制偏移量較大
    • ID最小
sequenceDiagram participant OM as Old Master participant s as Slave1 participant NM as Slave2 participant SL as Sentinel Leader SL ->> NM: Slaveof no one loop every second SL ->>+ NM: INFO(你小子謀反地怎么樣了) end NM ->>- SL: REPL(我已經(jīng)成為Master了) SL ->> s: Slaveof Slave2 note over OM: 恢復(fù)上線 SL ->> OM: Slaveof Slave2

本文摘自 :https://www.cnblogs.com/

開通會(huì)員,享受整站包年服務(wù)立即開通 >