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

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

Sentinel哨兵

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

哨兵:監(jiān)視、通知、自動故障恢復

啟動與初始化

Sentinel 的本質(zhì)只是一個運行在特殊模式下的 Redis 服務器,所以啟動 Sentinel 的步驟如下:

  1. 初始化一個普通的 Redis 服務器,不過也有一些不同:

    image_lymtics

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

    舉兩個例子:

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

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

      // 服務器在 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)

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

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

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

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

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

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

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

與服務器進行通信

獲取主服務器信息

image_lymtics

如上圖所示:

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

獲取從服務器信息

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

image_lymtics

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

image_lymtics

并更新自己保存的信息。

發(fā)送頻道信息

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

PUBLISH __sentinel__:hello "xxx"

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

image_lymtics

即兩部分:

  • 自己的信息
  • 主服務器的信息

接收頻道消息

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

PUBLISH __sentinel__:hello "xxx"

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

SUBSCRIBE __sentinel__:hello

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

sequenceDiagram participant s1 as Sentinel participant f as 服務器的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 字典變量(這個變量保存了所有監(jiān)視這個服務器的 Sentinel)

  • 鍵位Sentinel的IP和端口
  • 值指向sentinel實例

image_lymtics

而具體的更新流程是:

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

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

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

因為之所以和服務器需要創(chuàng)建訂閱鏈接就是用來發(fā)現(xiàn)未知的新的 Sentinel 的。

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

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

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

sequenceDiagram participant Sl as Slaver participant M as Master participant Se as Sentinel note over Se: 以本實例的視角來看 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

實例對 PING 的回復有兩種:

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

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

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

Sentinel 也要問問別的監(jiān)控目標的 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: 認為主節(jié)點客觀下線 note over s2: 我們來進行選舉吧!

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

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

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

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

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

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

選舉領(lǐng)頭 Sentinel

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

我的理解:

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

選舉的策略是:

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

如果在給定時限中沒有選出leader,則在一段時間后再次進行選舉,直到選出leader。

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

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

故障轉(zhuǎn)移

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

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

如何選新的服務器:

  • 篩選排除:
    • 下線的、斷線狀態(tài)的
    • 最近5秒內(nèi)都沒有回復過leader的INFO命令的服務器
    • 與前任主服務器斷開超過 down-after-milliseconds * 10 的服務器
  • 優(yōu)先選擇:
    • 優(yōu)先級較高
    • 復制偏移量較大
    • 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: 恢復上線 SL ->> OM: Slaveof Slave2

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

開通會員,享受整站包年服務立即開通 >