Redis高可用之哨兵機制實現細節

楊同學technotes發表於2022-12-23

Redis高可用之哨兵機制實現細節

本文來自我的 technotes [1] Redis篇,歡迎你常來逛逛。

正文

在上一篇的文章《Redis高可用全景一覽》中,我們學習了 Redis 的高可用性。高可用性有兩方面含義:一是服務少中斷,二是資料少丟失。主從庫模式和哨兵保證了服務少中斷,AOF 日誌和 RDB 快照保證了資料少丟失。

並且我們學習了哨兵三個職責,分別是:監控、選主(選擇主庫)和通知。今天我們就來詳細學習一下。

首先吶,在哨兵啟動前,我們要對哨兵進行配置。Redis 原始碼中包含了一個名為 sentinel.conf [2] 的檔案,該檔案中部分配置如下:

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000

第一行配置指示 Sentinel 去監視一個名為 mymaster 的主伺服器, 這個主伺服器的 IP 地址為 127.0.0.1 , 埠號為 6379 , 而將這個主伺服器判斷為失效至少需要 2 個 Sentinel 同意 。

可以看到,我們僅僅設定了主庫的 IP 和埠。並沒有配置其他哨兵的連線資訊啊,這些哨兵例項既然都不知道彼此的地址,又是怎麼組成叢集的呢?

哨兵例項之間可以相互發現,要歸功於 Redis 提供的 pub/sub 機制,也就是釋出 / 訂閱機制。在主從叢集中,主庫上有一個名為__sentinel__:hello的頻道,不同哨兵就是透過它來相互發現,實現互相通訊的。

一、哨兵叢集的組成

每隔2秒, 每個 Sentinel 節點就會向 Redis 資料節點的__sentinel__:hello頻道上傳送該 Sentinel 節點對於主節點的判斷以及當前 Sentinel 節點的資訊。

哨兵間的通訊

舉個例子。在上圖中,哨兵 1 把自己的 IP(172.16.19.3)和埠(26579)釋出到__sentinel__:hello頻道上,哨兵 2 和 3 訂閱了該頻道。那麼此時,哨兵 2 和 3 就可以從這個頻道直接獲取哨兵 1 的 IP 地址和埠號。

然後,哨兵 2、3 可以和哨兵 1 建立網路連線。透過這個方式,哨兵 2 和 3 也可以建立網路連線,這樣一來,哨兵叢集就形成了。它們相互間可以透過網路連線進行通訊,比如說對主庫有沒有下線這件事兒進行判斷和協商。

哨兵除了彼此之間建立起連線形成叢集外,還需要和從庫建立連線。這是因為,在哨兵的監控任務中,它需要對主從庫都進行心跳判斷,而且在主從庫切換完成後,它還需要通知從庫,讓它們和新主庫進行同步。

那麼,哨兵又是如何知道從庫的 IP 地址和埠的呢?

二、獲取從節點資訊

這是由哨兵向主庫傳送 INFO 命令來完成的。就像下圖所示,哨兵 2 給主庫傳送 INFO 命令,主庫接受到這個命令後,就會把從庫列表返回給哨兵。接著,哨兵就可以根據從庫列表中的連線資訊,和每個從庫建立連線。哨兵 1 和 3 也可以透過相同的方法和從庫建立連線。

獲取從節點資訊

下面是在一個主節點上執行 info 命令的結果片段:

# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=4917,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=4917,lag=1

之後,哨兵會在這個連線上持續地對從庫進行監控。每隔10秒, 哨兵節點就會向主節點和從節點傳送 info 命令,獲取叢集最新的拓撲結構。這樣,當有新的從節點加入時就可以立刻感知出來。節點不可達或者選定新主庫後,也可以透過 info 命令實時更新節點拓撲資訊。

執行info命令

有了叢集的資訊,哨兵終於可以開始它的工作了。第一項職責:判斷主從庫是否下線。

三、如何判斷主從庫下線?

3.1 定時執行 ping 命令

哨兵程式在執行時,每隔1秒,會向主節點、 從節點、 其餘 Sentinel 節點傳送一條 ping 命令,檢測它們是否仍然線上執行。如果主、從庫沒有在規定時間內響應哨兵的 ping 命令,哨兵就會把它標記為「下線狀態」。

執行ping命令

如果檢測的是主庫,那麼,哨兵還不能簡單地開啟主從切換。因為很有可能存在這麼一個情況:那就是哨兵誤判了,其實主庫並沒有故障。

誤判一般會發生在叢集網路壓力較大、網路擁塞,或者是主庫本身壓力較大的情況下。一旦哨兵誤判,啟動了主從切換,後續的選主和通知操作都會帶來額外的計算和通訊開銷。

那怎麼減少誤判呢?

3.2 主觀下線和客觀下線

哨兵機制通常會採用多例項組成的叢集模式進行部署,這也被稱為哨兵叢集。引入多個哨兵例項一起來判斷,就可以避免單個哨兵因為自身網路狀況不好,而誤判主庫下線的情況。同時,多個哨兵的網路同時不穩定的機率較小,由它們一起做決策,誤判率也能降低。

主觀下線和客觀下線

還記得我在文章開頭給出的 sentinel.conf 配置嗎?

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000

down-after-milliseconds選項就是 Sentinel 認為伺服器已經斷線的臨界閾值。

如果伺服器在該毫秒數之內, 沒有返回 Sentinel 傳送的 ping 命令的回覆, 或者返回一個錯誤, 那麼 Sentinel 將這個伺服器標記為主觀下線(subjectively down,簡稱 SDOWN )。

如果沒有足夠數量的 Sentinel 同意主庫已經下線, 當主庫重新向 Sentinel 的 PING 命令返回有效回覆時, 主庫的主觀下線狀態就會被移除。而如果超出 2 個 Sentinel 都將主庫標記為主觀下線之後, 主庫才會被標記為客觀下線(objectively down, 簡稱 ODOWN )。哨兵就要開始下一個決策過程了,即從許多從庫中,選出一個從庫來做新主庫。

四、如何選定新主庫?

4.1 初步篩選

設想一下,如果在選主時,一個從庫正常執行,我們把它選為新主庫開始使用了。可是,很快它的網路出了故障,此時,我們就又得重新選主了。這顯然不是我們期望的結果。所以,在選主時,除了要檢查從庫的當前線上狀態,還要判斷它之前的網路連線狀態。如果從庫總是和主庫斷連,而且斷連次數超出了一定的閾值,我們就有理由相信,這個從庫的網路狀況並不是太好,就可以把這個從庫篩掉了。

初步篩選

具體怎麼判斷呢?你使用配置項 down-after-milliseconds * 10。其中,down-after-milliseconds 是我們認定主從庫斷連的最大連線超時時間。如果在 down-after-milliseconds 毫秒內,主從節點都沒有透過網路聯絡上,我們就可以認為主從節點斷連了。如果發生斷連的次數超過了 10 次,就說明這個從庫的網路狀況不好,不適合作為新主庫。

這樣我們就過濾掉了不適合做主庫的從庫,完成了篩選工作。

接下來就要給剩餘的從庫打分了。我們可以分別按照三個規則依次進行三輪打分,這三個規則分別是從庫優先順序、從庫複製進度以及從庫 ID 號。

4.2 三輪打分

第一輪:優先順序最高的從庫得分高。

使用者可以透過 slave-priority 配置項,給不同的從庫設定不同優先順序。比如,你有兩個從庫,它們的記憶體大小不一樣,你可以手動給記憶體大的例項設定一個高優先順序。

第二輪:和舊主庫同步程度最接近的從庫得分高。

這個規則的依據是,如果選擇和舊主庫同步最接近的那個從庫作為主庫,那麼,這個新主庫上就有最新的資料。

如何判斷從庫和舊主庫間的同步進度呢?

主從庫同步時有個命令傳播的過程。在這個過程中,主庫會用 master_repl_offset 記錄當前的最新寫操作在 repl_backlog_buffer 中的位置,而從庫會用 slave_repl_offset 這個值記錄當前的複製進度。

主從庫同步時有個命令傳播的過程。在這個過程中,主庫會用 master_repl_offset 記錄當前的最新寫操作在 repl_backlog_buffer 中的位置,而從庫會用 slave_repl_offset 這個值記錄當前的複製進度。

如下圖所示,從庫 2 就應該被選為新主庫。

第三輪:ID 號小的從庫得分高。

每個例項都會有一個 ID,這個 ID 就類似於這裡的從庫的編號。目前,Redis 在選主庫時,有一個預設的規定:在優先順序和複製進度都相同的情況下,ID 號最小的從庫得分最高,會被選為新主庫。

到這裡,新主庫就被選出來了,接下來就是將從庫升級為主庫。但是問題又來了,這麼多哨兵,該由誰來執行主從切換操作呢?

4.3 由哪個哨兵執行主從切換?

任何一個哨兵例項只要自身判斷主庫“主觀下線”後,就會向其他 Sentinel 傳送 SENTINEL is-master-down-by-addr 命令來詢問對方是否認為主庫已下線。接著,其他哨兵例項會根據自己和主庫的連線情況,做出 Y 或 N 的響應,Y 相當於贊成票,N 相當於反對票。

此時,這個哨兵就可以再給其他哨兵傳送命令,表明希望由自己來執行主從切換,並讓所有其他哨兵進行投票。這個投票過程稱為“Leader 選舉”。選舉出來的 Leader 就是最終執行主從切換的哨兵。

例如,現在有 3 個哨兵,quorum 配置的是 2,我們來看一下選舉的過程是什麼樣的。

在 T1 時刻,S1 判斷主庫為“客觀下線”,它想成為 Leader,就先給自己投一張贊成票,然後分別向 S2 和 S3 傳送命令,表示要成為 Leader。

在 T2 時刻,S3 判斷主庫為“客觀下線”,它也想成為 Leader,所以也先給自己投一張贊成票,再分別向 S1 和 S2 傳送命令,表示要成為 Leader。

在 T3 時刻,S1 收到了 S3 的 Leader 投票請求。因為 S1 已經給自己投了一票 Y,所以它不能再給其他哨兵投贊成票了,所以 S1 回覆 N 表示不同意。同時,S2 收到了 T2 時 S3 傳送的 Leader 投票請求。因為 S2 之前沒有投過票,它會給第一個向它傳送投票請求的哨兵回覆 Y,給後續再傳送投票請求的哨兵回覆 N,所以,在 T3 時,S2 回覆 S3,同意 S3成為 Leader。

在 T4 時刻,S2 才收到 T1 時 S1 傳送的投票命令。因為 S2 已經在 T3 時同意了 S3 的投票請求,此時,S2 給 S1 回覆 N,表示不同意 S1 成為 Leader。發生這種情況,是因為 S3 和 S2 之間的網路傳輸正常,而 S1 和 S2 之間的網路傳輸可能正好擁塞了,導致投票請求傳輸慢了。

在 T5 時刻,S1 得到的票數是來自它自己的一票 Y 和來自 S2 的一票 N。而 S3 除了自己的贊成票 Y 以外,還收到了來自 S2 的一票 Y。此時,S3 不僅獲得了半數以上的 Leader 贊成票,也達到預設的 quorum 值(quorum 為 2),所以它最終成為了 Leader。

接著,S3 會開始執行選主操作,而且在選定新主庫後,會給其他從庫和客戶端通知新主庫的資訊。

五、將新主庫通知給從庫和客戶端

透過上文的學習,我們知道哨兵可以向主庫傳送 INFO 命令,來獲取從庫的 IP 地址和埠。

但是,哨兵不能只和主、從庫連線。因為,主從庫切換後,客戶端也需要知道新主庫的連線資訊,才能向新主庫傳送請求操作。所以,哨兵還需要把新主庫的資訊告訴客戶端。

那怎麼把新主庫的資訊告訴客戶端呢?

5.1 基於 pub/sub 機制的客戶端事件通知

從本質上說,哨兵就是一個執行在特定模式下的 Redis 例項,只不過它並不服務請求操作,只是完成監控、選主和通知的任務。所以,每個哨兵例項也提供 pub/sub 機制,客戶端可以從哨兵訂閱訊息。

下圖示中是一些重要的頻道,以及涉及的幾個關鍵事件。更多的頻道你可以在文末連結 [3] 中檢視。

客戶端從主庫讀取哨兵的配置檔案後,可以獲得哨兵的地址和埠,和哨兵建立網路連線。

當哨兵把新主庫選擇出來後,客戶端就會看到下面的 switch-master 事件。這個事件表示主庫已經切換了,新主庫的 IP 地址和埠資訊已經有了。這個時候,客戶端就可以用這裡面的新主庫地址和埠進行通訊了。

switch-master <master name> <oldip> <oldport> <newip> <newport>

小結

至此,哨兵的工作職責及細節我們就學習完了。我整理了本文知識消化鏈路,如下。

在sentinel.conf中配置哨兵
-> 沒有配置其他哨兵ip,怎麼組成叢集的?
-> 哨兵是怎麼知道從庫的IP和埠的?
-> 職責1:如何判斷主從庫下線了?
-> 職責2:如何選定新主庫?
-> 由哪個哨兵執行主從切換?
-> 職責3:如何把新主庫告訴客戶端?

更多技術乾貨,我會在後面的內容裡繼續分享,敬請關注。公眾號「楊同學technotes」,歡迎技術交流。

文中所提及的連結

附 Redis 文件

相關文章