Redis高可用之哨兵模式

Redis高可用之哨兵模式

原文: 極客時間 redis 專欄

我們在之前的文章裏面已經介紹了 redis 高可用之數據持久化,我們分別介紹了 AOF 持久化和 RDB 持久化兩種方式,我們來簡單回憶下:

AOF 持久化是命令在 redis 實例執行成功後纔會記錄到 AOF 日誌中,每次操作redis 都會記錄一個日誌,這就會造成 AOF 日誌文件變大,恢復起來不容易,我們可以採用 AOF 重寫機制,來減小 AOF 文件的大小,AOF 的重寫機制就是隻記錄 key 和 value 的最終對應關係,省去中間的步驟,但是當 Redis 實例恢復的時候,需要一條一條執行恢復起來會特別麻煩,這個時候引入了 RDB 快照。他就是針對當前 redis 實例的內存數據拍攝快照,是一個二進制文件。

我們雖然保證數據持久化了,但是當實際生產環境中如果只有一個 redis 實例,那麼他如果不可用了,還是會造成服務中斷,所以我們下面來講解下 redis 集羣模式;

Redis 主從模式

redis 實際上給我們提供了主從模式的數據庫,主從庫之間是採用的讀寫分離模式,一切數據都是從主庫寫入,然後可以從 redis 任何一個節點讀取。

img

爲什麼要採用讀寫分離的方式?

保證集羣中的數據強一致性,我們可以試想一下,在上圖中如果主從庫都可以進行寫操作,一個直接的問題就是:如果客戶端對同一個數據(例如 k1)前後修改了三次,每一次的修改請求都發送到不同的實例上,在不同的實例上執行,那麼,這個數據在這三個實例上的副本就不一致了(分別是 v1、v2 和 v3)。在讀取這個數據的時候,就可能讀取到舊的值。如果我們非要保持這個數據在三個實例上一致,就要涉及到加鎖、實例間協商是否完成修改等一系列操作,但這會帶來鉅額的開銷,當然是不太能接受的。

而主從庫模式一旦採用了讀寫分離,所有數據的修改只會在主庫上進行,不用協調三個實例。主庫有了最新的數據後,會同步給從庫,這樣,主從庫的數據就是一致的。

主從庫之間是如何進行數據同步的?

第一次數據同步

當我們啓動多個 redis 實例的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係,之後會按照三個階段完成數據的第一次同步。

例如,現在有實例 1(ip:172.16.19.3)和實例 2(ip:172.16.19.5),我們在實例 2 上執行以下這個命令後,實例 2 就變成了實例 1 的從庫,並從實例 1 上覆制數據:

replicaof  172.16.19.3  6379

他們之間就會按照下面流程來進行第一次數據同步

img

1,建立連接,協商同步

這一步主要是爲全量複製做準備,在這一步從庫和主庫建立起連接,並告訴主庫可以進行數據同步,主庫確認恢復後,主從庫間就可以開始進行數據同步了。

a, 從庫給主庫發送 psync 命令,表示要進行數據同步,主庫根據命令參數來啓動複製。

psync 主庫的 runID 複製進度Offset 

runID: 每個 redis 實例啓動的時候都會自動生成一個隨機的 ID 用來當做唯一標識,因爲第一次連接協商的時候還不知道主庫的 runID 只能用 "?" 來表示。

offset:設置爲 "-1" 代表全量複製,第一次複製

主庫收到 psync 命令後,會用 FULLRESYNC 響應命令帶上兩個參數:主庫 runID 和主庫目前的複製進度 offset,返回給從庫。從庫收到響應後,會記錄下這兩個參數。

FULLRESYNC響應表示第一次複製採用的全量複製

2,主庫給從庫同步數據

主庫會先執行bgsave命令,生成一個 RDB 文件,接着將這個文件發送給從庫。從庫接收到RDB 文件之後,會先清空當前 Redis 實例中的數據(保證主從庫數據一致),然後加載 RDB 文件,進行數據恢復;

我們肯定會想到,當主從庫數據進行同步的時候,肯定不能影響到主庫的讀寫操作,當我們在同步數據期間這些主庫上寫入的數據如何同步到從庫上呢?爲了保證主從庫的數據一致性,主庫會把在這期間的寫操作在內存中用專門的replication buffer記錄生成 RDB 文件後的寫操作。

3,主庫發送新寫命令給從庫

主庫會把第二階段執行過程中新收到的寫命令,再發送給從庫。具體的操作是,當主庫完成 RDB 文件發送後,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。

後續在進行數據同步,從庫就會向主句發送psync 1 3然後主庫就會把需要的數據發送給從庫了。

主從級聯模式分擔全量複製時的主庫壓力

如果我們一個集羣中如果有特別多的從庫 ,所有的從庫都去向主庫同步數據庫的話,那會對主庫造成不必要的壓力,我們可以採用級聯模式來分擔主庫的壓力"主-從-從"

簡單來說,我們在部署主從集羣的時候,可以手動選擇一個從庫(比如選擇內存資源配置較高的從庫),用於級聯其他的從庫。然後,我們可以再選擇一些從庫(例如三分之一的從庫),在這些從庫上執行如下命令,讓它們和剛纔所選的從庫,建立起主從關係。

replicaof 所選從庫的IP 6379

這樣一來,這些從庫就會知道,在進行同步時,不用再和主庫進行交互了,只要和級聯的從庫進行寫操作同步就行了,這就可以減輕主庫上的壓力,如下圖所示:

img

們瞭解了主從庫間通過全量複製實現數據同步的過程,以及通過“主 - 從 - 從”模式分擔主庫壓力的方式。那麼,一旦主從庫完成了全量複製,它們之間就會一直維護一個網絡連接,主庫會通過這個連接將後續陸續收到的命令操作再同步給從庫,這個過程也稱爲基於長連接的命令傳播,可以避免頻繁建立連接的開銷。

主從庫間網絡斷了怎麼辦?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網絡閃斷,那麼,從庫就會和主庫重新進行一次全量複製,開銷非常大。

從 Redis 2.8 開始,網絡斷了之後,主從庫會採用增量複製的方式繼續同步。聽名字大概就可以猜到它和全量複製的不同:全量複製是同步所有數據,而增量複製只會把主從庫網絡斷連期間主庫收到的命令,同步給從庫。

那麼,增量複製時,主從庫之間具體是怎麼保持同步的呢?這裏的奧妙就在於 repl_backlog_buffer 這個緩衝區。我們先來看下它是如何用於增量命令的同步的。

當主從庫斷連後,主庫會把斷連期間收到的寫操作命令,寫入 replication buffer,同時也會把這些操作命令也寫入 repl_backlog_buffer 這個緩衝區。(這個是從主從關係確定之後就一直存在的,當從庫斷連後,從庫重新發送psync \$master_runid \$offset,主庫才能通過​\$offset在repl_backlog_buffer中找到從庫斷開的位置,只發送$offset之後的增量數據給從庫即可。)

repl_backlog_buffer 是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置。

剛開始的時候,主庫和從庫的寫讀位置在一起,這算是它們的起始位置。隨着主庫不斷接收新的寫操作,它在緩衝區中的寫位置會逐步偏離起始位置,我們通常用偏移量來衡量這個偏移距離的大小,對主庫來說,對應的偏移量就是 master_repl_offset。主庫接收的新寫操作越多,這個值就會越大。

同樣,從庫在複製完寫操作命令後,它在緩衝區中的讀位置也開始逐步偏移剛纔的起始位置,此時,從庫已複製的偏移量 slave_repl_offset 也在不斷增加。正常情況下,這兩個偏移量基本相等。

img

主從庫的連接恢復之後,從庫首先會給主庫發送 psync 命令,並把自己當前的 slave_repl_offset 發給主庫,主庫會判斷自己的 master_repl_offset 和 slave_repl_offset 之間的差距。

在網絡斷連階段,主庫可能會收到新的寫操作命令,所以,一般來說,master_repl_offset 會大於 slave_repl_offset。此時,主庫只用把 master_repl_offset 和 slave_repl_offset 之間的命令操作同步給從庫就行。

就像剛剛示意圖的中間部分,主庫和從庫之間相差了 put d e 和 put d f 兩個操作,在增量複製時,主庫只需要把它們同步給從庫,就行了。說到這裏,我們再借助一張圖,回顧下增量複製的流程。

img

不過,有一個地方我要強調一下,因爲 repl_backlog_buffer 是一個環形緩衝區,所以在緩衝區寫滿後,主庫會繼續寫入,此時,就會覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的數據不一致。

因此,我們要想辦法避免這一情況,一般而言,我們可以調整 repl_backlog_size 這個參數。這個參數和所需的緩衝空間大小有關。緩衝空間的計算公式是:緩衝空間大小 = 主庫寫入命令速度 操作大小 - 主從庫間網絡傳輸命令速度 操作大小。在實際應用中,考慮到可能存在一些突發的請求壓力,我們通常需要把這個緩衝空間擴大一倍,即 repl_backlog_size = 緩衝空間大小 * 2,這也就是 repl_backlog_size 的最終值。

舉個例子,如果主庫每秒寫入 2000 個操作,每個操作的大小爲 2KB,網絡每秒能傳輸 1000 個操作,那麼,有 1000 個操作需要緩衝起來,這就至少需要 2MB 的緩衝空間。否則,新寫的命令就會覆蓋掉舊操作了。爲了應對可能的突發壓力,我們最終把 repl_backlog_size 設爲 4MB。

這樣一來,增量複製時主從庫的數據不一致風險就降低了。不過,如果併發請求量非常大,連兩倍的緩衝空間都存不下新操作請求的話,此時,主從庫數據仍然可能不一致。針對這種情況,一方面,你可以根據 Redis 所在服務器的內存資源再適當增加 repl_backlog_size 值,比如說設置成緩衝空間大小的 4 倍,另一方面,你可以考慮使用切片集羣來分擔單個主庫的請求壓力。

repl_backlog_buffer和replication buffer解釋

1、repl_backlog_buffer:就是上面我解釋到的,它是爲了從庫斷開之後,如何找到主從差異數據而設計的環形緩衝區,從而避免全量同步帶來的性能開銷。如果從庫斷開時間太久,repl_backlog_buffer環形緩衝區被主庫的寫命令覆蓋了,那麼從庫連上主庫後只能乖乖地進行一次全量同步,所以repl_backlog_buffer配置儘量大一些,可以降低主從斷開後全量同步的概率。而在repl_backlog_buffer中找主從差異的數據後,如何發給從庫呢?這就用到了replication buffer。

2、replication buffer:Redis和客戶端通信也好,和從庫通信也好,Redis都需要給分配一個 內存buffer進行數據交互,客戶端是一個client,從庫也是一個client,我們每個client連上Redis後,Redis都會分配一個client buffer,所有數據交互都是通過這個buffer進行的:Redis先把數據寫到這個buffer中,然後再把buffer中的數據發到client socket中再通過網絡發送出去,這樣就完成了數據交互。所以主從在增量同步時,從庫作爲一個client,也會分配一個buffer,只不過這個buffer專門用來傳播用戶的寫命令到從庫,保證主從數據一致,我們通常把它叫做replication buffer。

3、再延伸一下,既然有這個內存buffer存在,那麼這個buffer有沒有限制呢?如果主從在傳播命令時,因爲某些原因從庫處理得非常慢,那麼主庫上的這個buffer就會持續增長,消耗大量的內存資源,甚至OOM。所以Redis提供了client-output-buffer-limit參數限制這個buffer的大小,如果超過限制,主庫會強制斷開這個client的連接,也就是說從庫處理慢導致主庫內存buffer的積壓達到限制後,主庫會強制斷開從庫的連接,此時主從複製會中斷,中斷後如果從庫再次發起複製請求,那麼此時可能會導致惡性循環,引發複製風暴,這種情況需要格外注意。

主從全量同步使用RDB而不使用AOF的原因

1、RDB文件內容是經過壓縮的二進制數據(不同數據類型數據做了針對性優化),文件很小。而AOF文件記錄的是每一次寫操作的命令,寫操作越多文件會變得很大,其中還包括很多對同一個key的多次冗餘操作。在主從全量數據同步時,傳輸RDB文件可以儘量降低對主庫機器網絡帶寬的消耗,從庫在加載RDB文件時,一是文件小,讀取整個文件的速度會很快,二是因爲RDB文件存儲的都是二進制數據,從庫直接按照RDB協議解析還原數據即可,速度會非常快,而AOF需要依次重放每個寫命令,這個過程會經歷冗長的處理邏輯,恢復速度相比RDB會慢得多,所以使用RDB進行主從全量同步的成本最低。

2、假設要使用AOF做全量同步,意味着必須打開AOF功能,打開AOF就要選擇文件刷盤的策略,選擇不當會嚴重影響Redis性能。而RDB只有在需要定時備份和主從全量同步數據時纔會觸發生成一次快照。而在很多丟失數據不敏感的業務場景,其實是不需要開啓AOF的。

主庫高可用-sentinel 模式

我們上面介紹了redis 主從架構模式,但是想過沒有如果主庫發生故障,那麼整個集羣就變成只讀的了,因爲可寫節點不存在了,那些個還沒來得及同步的數據也不能正常訪問了。

img

無論是寫服務中斷,還是從庫無法進行數據同步,都是不能接受的。所以,如果主庫掛了,我們就需要運行一個新主庫,比如說把一個從庫切換爲主庫,把它當成主庫。這就涉及到三個問題:

1,主庫真的掛了嗎?

2,該選擇哪個從庫作爲主庫?

3,怎麼把新主庫的相關信息通知給從庫和客戶端呢?

想知道這個三個問題的答案,我們就要了解redis 哨兵模式的機制,

哨兵機制的基本流程

哨兵其實就是一個運行在特殊模式下的 Redis 進程,主從庫實例運行的同時,它也在運行。哨兵主要負責的就是三個任務:監控、選主(選擇主庫)和通知。

監控:其實就是對應第一個問題,主庫真的掛了嗎

選主:該選擇哪個從庫作爲主庫?

通知:怎麼把新主庫的相關信息通知給從庫和客戶端呢?

img

監控-主庫真的掛了嗎?

監控是指哨兵進程在運行時,週期性地給所有的主從庫發送 PING 命令,檢測它們是否仍然在線運行。如果從庫沒有在規定時間內響應哨兵的 PING 命令,哨兵就會把它標記爲“客觀下線”;同樣,如果主庫也沒有在規定時間內響應哨兵的 PING 命令,哨兵就會判定主庫"主觀下線",然後進行投票選舉,判斷主庫爲"客觀下線",繼續後面的操作,

主觀下線和客觀下線

主觀下線

哨兵進程會使用 PING 命令檢測它自己和主、從庫的網絡連接情況,用來判斷實例的狀態。如果哨兵發現主庫或從庫對 PING 命令的響應超時了,那麼,哨兵就會先把它標記爲“主觀下線”。

如果檢測的是從庫,那麼,哨兵簡單地把它標記爲“客觀下線”就行了,因爲從庫的下線影響一般不太大,集羣的對外服務不會間斷。

但是,如果檢測的是主庫,那麼,哨兵還不能簡單地把它標記爲“客觀下線”,開啓主從切換。因爲很有可能存在這麼一個情況:那就是哨兵誤判了,其實主庫並沒有故障。可是,一旦啓動了主從切換,後續的選主和通知操作都會帶來額外的計算和通信開銷。

爲了避免這些不必要的開銷,我們要特別注意誤判的情況。

首先,我們要知道啥叫誤判。很簡單,就是主庫實際並沒有下線,但是哨兵誤以爲它下線了。誤判一般會發生在集羣網絡壓力較大、網絡擁塞,或者是主庫本身壓力較大的情況下。

一旦哨兵判斷主庫下線了,就會開始選擇新主庫,並讓從庫和新主庫進行數據同步,這個過程本身就會有開銷,例如,哨兵要花時間選出新主庫,從庫也需要花時間和新主庫同步。而在誤判的情況下,主庫本身根本就不需要進行切換的,所以這個過程的開銷是沒有價值的。正因爲這樣,我們需要判斷是否有誤判,以及減少誤判。

那怎麼減少誤判呢?在日常生活中,當我們要對一些重要的事情做判斷的時候,經常會和家人或朋友一起商量一下,然後再做決定。

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

在判斷主庫是否下線時,不能由一個哨兵說了算,只有大多數的哨兵實例,都判斷主庫已經“主觀下線”了,主庫纔會被標記爲“客觀下線”,這個叫法也是表明主庫下線成爲一個客觀事實了。這個判斷原則就是:少數服從多數。同時,這會進一步觸發哨兵開始主從切換流程。

爲了方便你理解,我再畫一張圖展示一下這裏的邏輯。

如下圖所示,Redis 主從集羣有一個主庫、三個從庫,還有三個哨兵實例。在圖片的左邊,哨兵 2 判斷主庫爲“主觀下線”,但哨兵 1 和 3 卻判定主庫是上線狀態,此時,主庫仍然被判斷爲處於上線狀態。在圖片的右邊,哨兵 1 和 2 都判斷主庫爲“主觀下線”,此時,即使哨兵 3 仍然判斷主庫爲上線狀態,主庫也被標記爲“客觀下線”了。

img

簡單來說,“客觀下線”的標準就是,當有 N 個哨兵實例時,最好要有 N/2 + 1 個實例判斷主庫爲“主觀下線”,才能最終判定主庫爲“客觀下線”。這樣一來,就可以減少誤判的概率,也能避免誤判帶來的無謂的主從庫切換。(當然,有多少個實例做出“主觀下線”的判斷纔可以,可以由 Redis 管理員自行設定 quorum 此參數設置)。

好了,到這裏,你可以看到,藉助於多個哨兵實例的共同判斷機制,我們就可以更準確地判斷出主庫是否處於下線狀態。如果主庫的確下線了,哨兵就要開始下一個決策過程了,即從許多從庫中,選出一個從庫來做新主庫。(選主)

選主-該選擇哪個從庫作爲主庫?

一般來說,我把哨兵選擇新主庫的過程稱爲“篩選 + 打分”。簡單來說,我們在多個從庫中,先按照一定的篩選條件,把不符合條件的從庫去掉。然後,我們再按照一定的規則,給剩下的從庫逐個打分,將得分最高的從庫選爲新主庫,如下圖所示:

img

在剛剛的這段話裏,需要注意的是兩個“一定”,現在,我們要考慮這裏的“一定”具體是指什麼。首先來看篩選的條件。

一般情況下,我們肯定要先保證所選的從庫仍然在線運行。不過,在選主時從庫正常在線,這隻能表示從庫的現狀良好,並不代表它就是最適合做主庫的。

設想一下,如果在選主時,一個從庫正常運行,我們把它選爲新主庫開始使用了。可是,很快它的網絡出了故障,此時,我們就得重新選主了。這顯然不是我們期望的結果。

所以,在選主時,除了要檢查從庫的當前在線狀態,還要判斷它之前的網絡連接狀態。如果從庫總是和主庫斷連,而且斷連次數超出了一定的閾值,我們就有理由相信,這個從庫的網絡狀況並不是太好,就可以把這個從庫篩掉了。

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

接下來就要給剩餘的從庫打分了。我們可以分別按照三個規則依次進行三輪打分,這三個規則分別是從庫優先級、從庫複製進度以及從庫 ID 號。只要在某一輪中,有從庫得分最高,那麼它就是主庫了,選主過程到此結束。如果沒有出現得分最高的從庫,那麼就繼續進行下一輪。

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

用戶可以通過 slave-priority 配置項,給不同的從庫設置不同優先級。比如,你有兩個從庫,它們的內存大小不一樣,你可以手動給內存大的實例設置一個高優先級。在選主時,哨兵會給優先級高的從庫打高分,如果有一個從庫優先級最高,那麼它就是新主庫了。如果從庫的優先級都一樣,那麼哨兵開始第二輪打分

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

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

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

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

所有的從庫對比 slave_repl_offset 這個值,誰的值大,說明誰複製的數據多。那麼他的得分就高,就會變成主庫。

舊主庫的 master_repl_offset 是 1000,從庫 1、2 和 3 的 slave_repl_offset 分別是 950、990 和 900,那麼,從庫 2 就應該被選爲新主庫。

img

如果所有的都一樣,那麼會進行第三輪打分:

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

每個實例都會有一個 ID,這個 ID 就類似於這裏的從庫的編號。目前,Redis 在選主庫時,有一個默認的規定:在優先級和複製進度都相同的情況下,ID 號最小的從庫得分最高,會被選爲新主庫

到這裏,新主庫就被選出來了,“選主”這個過程就完成了

哨兵在操作主從切換的過程中,客戶端能否正常地進行請求操作?

如果客戶端使用了讀寫分離,那麼讀請求可以在從庫上正常執行,不會受到影響。但是由於此時主庫已經掛了,而且哨兵還沒有選出新的主庫,所以在這期間寫請求會失敗,失敗持續的時間 = 哨兵切換主從的時間 + 客戶端感知到新主庫 的時間。

如果不想讓業務感知到異常,客戶端只能把寫失敗的請求先緩存起來或寫入消息隊列中間件中,等哨兵切換完主從後,再把這些寫請求發給新的主庫,但這種場景只適合對寫入請求返回值不敏感的業務,而且還需要業務層做適配,另外主從切換時間過長,也會導致客戶端或消息隊列中間件緩存寫請求過多,切換完成之後重放這些請求的時間變長。

哨兵檢測主庫多久沒有響應就提升從庫爲新的主庫,這個時間是可以配置的(down-after-milliseconds參數)。配置的時間越短,哨兵越敏感,哨兵集羣認爲主庫在短時間內連不上就會發起主從切換,這種配置很可能因爲網絡擁塞但主庫正常而發生不必要的切換,當然,當主庫真正故障時,因爲切換得及時,對業務的影響最小。如果配置的時間比較長,哨兵越保守,這種情況可以減少哨兵誤判的概率,但是主庫故障發生時,業務寫失敗的時間也會比較久,緩存寫請求數據量越多。

應用程序不感知服務的中斷,還需要哨兵和客戶端做些什麼?

當哨兵完成主從切換後,客戶端需要及時感知到主庫發生了變更,然後把緩存的寫請求寫入到新庫中,保證後續寫請求不會再受到影響,具體做法如下:
哨兵提升一個從庫爲新主庫後,哨兵會把新主庫的地址寫入自己實例的pubsub(switch-master)中。客戶端需要訂閱這個pubsub,當這個pubsub有數據時,客戶端就能感知到主庫發生變更,同時可以拿到最新的主庫地址,然後把寫請求寫到這個新主庫即可,這種機制屬於哨兵主動通知客戶端。

如果客戶端因爲某些原因錯過了哨兵的通知,或者哨兵通知後客戶端處理失敗了,安全起見,客戶端也需要支持主動去獲取最新主從的地址進行訪問。

所以,客戶端需要訪問主從庫時,不能直接寫死主從庫的地址了,而是需要從哨兵集羣中獲取最新的地址(sentinel get-master-addr-by-name命令),這樣當實例異常時,哨兵切換後或者客戶端斷開重連,都可以從哨兵集羣中拿到最新的實例地址。

一般Redis的SDK都提供了通過哨兵拿到實例地址,再訪問實例的方式,我們直接使用即可,不需要自己實現這些邏輯。當然,對於只有主從實例的情況,客戶端需要和哨兵配合使用,而在分片集羣模式下,這些邏輯都可以做在proxy層,這樣客戶端也不需要關心這些邏輯了,Codis就是這麼做的。

哨兵集羣:哨兵掛了,主從庫還能切換嗎?

我們上面學習了redis哨兵機制,它可以實現主從庫的自動切換。通過部署多個實例,就形成了一個哨兵集羣。哨兵集羣中的多個實例共同判斷,可以降低對主庫下線的誤判率。

但是,我們還是要考慮一個問題:如果有哨兵實例在運行時發生了故障,主從庫還能正常切換嗎?

實際上,一旦多個實例組成了哨兵集羣,即使有哨兵實例出現故障掛掉了,其他哨兵還能繼續協作完成主從庫切換的工作,包括判定主庫是不是處於下線狀態,選擇新主庫,以及通知從庫和客戶端。

如果你部署過哨兵集羣的話就會知道,在配置哨兵的信息時,我們只需要用到下面的這個配置項,設置主庫的 IP 和端口,並沒有配置其他哨兵的連接信息。

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

這些哨兵實例既然都不知道彼此的地址,又是怎麼組成集羣的呢?要弄明白這個問題,我們就需要學習一下哨兵集羣的組成和運行機制了。

哨兵集羣的組成和運行機制

基於 pub/sub 機制的哨兵集羣組成

哨兵實例之間可以相互發現,要歸功於 Redis 提供的 pub/sub 機制,也就是發佈 / 訂閱機制。

哨兵只要和主庫建立起了連接,就可以在主庫上發佈消息了,比如說發佈它自己的連接信息(IP 和端口)。同時,它也可以從主庫上訂閱消息,獲得其他哨兵發佈的連接信息。當多個哨兵實例都在主庫上做了發佈和訂閱操作後,它們之間就能知道彼此的 IP 地址和端口。

除了哨兵實例,我們自己編寫的應用程序也可以通過 Redis 進行消息的發佈和訂閱。所以,爲了區分不同應用的消息,Redis 會以頻道的形式,對這些消息進行分門別類的管理。所謂的頻道,實際上就是消息的類別。當消息類別相同時,它們就屬於同一個頻道。反之,就屬於不同的頻道。只有訂閱了同一個頻道的應用,才能通過發佈的消息進行信息交換

在主從集羣中,主庫上有一個名爲“sentinel:hello”的頻道,不同哨兵就是通過它來相互發現,實現互相通信的。

我來舉個例子,具體說明一下。在下圖中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)發佈到“sentinel:hello”頻道上,哨兵 2 和 3 訂閱了該頻道。那麼此時,哨兵 2 和 3 就可以從這個頻道直接獲取哨兵 1 的 IP 地址和端口號。

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

img

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

哨兵是如何知道從庫的 IP 地址和端口的呢?

這是由哨兵向主庫發送 INFO 命令來完成的。就像下圖所示,哨兵 2 給主庫發送 INFO 命令,主庫接受到這個命令後,就會把從庫列表返回給哨兵。接着,哨兵就可以根據從庫列表中的連接信息,和每個從庫建立連接,並在這個連接上持續地對從庫進行監控。哨兵 1 和 3 可以通過相同的方法和從庫建立連接。

img

你看,通過 pub/sub 機制,哨兵之間可以組成集羣,同時,哨兵又通過 INFO 命令,獲得了從庫連接信息,也能和從庫建立連接,並進行監控了。但是,哨兵不能只和主、從庫連接。因爲,主從庫切換後,客戶端也需要知道新主庫的連接信息,才能向新主庫發送請求操作。所以,哨兵還需要完成把新主庫的信息告訴客戶端這個任務。

而且,在實際使用哨兵時,我們有時會遇到這樣的問題:如何在客戶端通過監控瞭解哨兵進行主從切換的過程呢?比如說,主從切換進行到哪一步了?這其實就是要求,客戶端能夠獲取到哨兵集羣在監控、選主、切換這個過程中發生的各種事件。

此時,我們仍然可以依賴 pub/sub 機制,來幫助我們完成哨兵和客戶端間的信息同步。

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

從本質上說,哨兵就是一個運行在特定模式下的 Redis 實例,只不過它並不服務請求操作,只是完成監控、選主和通知的任務。所以,每個哨兵實例也提供 pub/sub 機制,客戶端可以從哨兵訂閱消息。哨兵提供的消息訂閱頻道有很多,不同頻道包含了主從庫切換過程中的不同關鍵事件。

頻道有這麼多,一下子全部學習容易丟失重點。爲了減輕你的學習壓力,我把重要的頻道彙總在了一起,涉及幾個關鍵事件,包括主庫下線判斷、新主庫選定、從庫重新配置。

img

知道了這些頻道之後,你就可以讓客戶端從哨兵這裏訂閱消息了。具體的操作步驟是,客戶端讀取哨兵的配置文件後,可以獲得哨兵的地址和端口,和哨兵建立網絡連接。然後,我們可以在客戶端執行訂閱命令,來獲取不同的事件消息。

舉個例子,你可以執行如下命令,來訂閱“所有實例進入客觀下線狀態的事件”:

SUBSCRIBE +odown

當然,你也可以執行如下命令,訂閱所有的事件:

PSUBSCRIBE *

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

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

有了這些事件通知,客戶端不僅可以在主從切換後得到新主庫的連接信息,還可以監控到主從庫切換過程中發生的各個重要事件。這樣,客戶端就可以知道主從切換進行到哪一步了,有助於瞭解切換進度。

好了,有了 pub/sub 機制,哨兵和哨兵之間、哨兵和從庫之間、哨兵和客戶端之間就都能建立起連接了,再加上我們上節課介紹主庫下線判斷和選主依據,哨兵集羣的監控、選主和通知三個任務就基本可以正常工作了。不過,我們還需要考慮一個問題:主庫故障以後,哨兵集羣有多個實例,那怎麼確定由哪個哨兵來進行實際的主從切換呢?

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

確定由哪個哨兵執行主從切換的過程,和主庫“客觀下線”的判斷過程類似,也是一個“投票仲裁”的過程。在具體瞭解這個過程前,我們再來看下,判斷“客觀下線”的仲裁過程。

哨兵集羣要判定主庫“客觀下線”,需要有一定數量的實例都認爲該主庫已經“主觀下線”了。我在上節課向你介紹了判斷“客觀下線”的原則,接下來,我介紹下具體的判斷過程

任何一個實例只要自身判斷主庫“主觀下線”後,就會給其他實例發送 is-master-down-by-addr 命令。接着,其他實例會根據自己和主庫的連接情況,做出 Y 或 N 的響應,Y 相當於贊成票,N 相當於反對票。

img

一個哨兵獲得了仲裁所需的贊成票數後,就可以標記主庫爲“客觀下線”。這個所需的贊成票數是通過哨兵配置文件中的 quorum 配置項設定的。例如,現在有 5 個哨兵,quorum 配置的是 3,那麼,一個哨兵需要 3 張贊成票,就可以標記主庫爲“客觀下線”了。這 3 張贊成票包括哨兵自己的一張贊成票和另外兩個哨兵的贊成票。

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

在投票過程中,任何一個想成爲 Leader 的哨兵,要滿足兩個條件:第一,拿到半數以上的贊成票;第二,拿到的票數同時還需要大於等於哨兵配置文件中的 quorum 值。以 3 個哨兵爲例,假設此時的 quorum 設置爲 2,那麼,任何一個想成爲 Leader 的哨兵只要拿到 2 張贊成票,就可以了。

這麼說你可能還不太好理解,我再畫一張圖片,展示一下 3 個哨兵、quorum 爲 2 的選舉過程。

img

在 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 會開始執行選主操作,而且在選定新主庫後,會給其他從庫和客戶端通知新主庫的信息。

如果 S3 沒有拿到 2 票 Y,那麼這輪投票就不會產生 Leader。哨兵集羣會等待一段時間(也就是哨兵故障轉移超時時間的 2 倍),再重新選舉。這是因爲,哨兵集羣能夠進行成功投票,很大程度上依賴於選舉命令的正常網絡傳播。如果網絡壓力較大或有短時堵塞,就可能導致沒有一個哨兵能拿到半數以上的贊成票。所以,等到網絡擁塞好轉之後,再進行投票選舉,成功的概率就會增加。

需要注意的是,如果哨兵集羣只有 2 個實例,此時,一個哨兵要想成爲 Leader,必須獲得 2 票,而不是 1 票。所以,如果有個哨兵掛掉了,那麼,此時的集羣是無法進行主從庫切換的。因此,通常我們至少會配置 3 個哨兵實例。這一點很重要,你在實際應用時可不能忽略了。

Redis 1主4從,5個哨兵,哨兵配置quorum爲2,如果3個哨兵故障,當主庫宕機時,哨兵能否判斷主庫“客觀下線”?能否自動切換?

1、哨兵集羣可以判定主庫“主觀下線”。由於quorum=2,所以當一個哨兵判斷主庫“主觀下線”後,詢問另外一個哨兵後也會得到同樣的結果,2個哨兵都判定“主觀下線”,達到了quorum的值,因此,哨兵集羣可以判定主庫爲“客觀下線”。

2、但哨兵不能完成主從切換。哨兵標記主庫“客觀下線後”,在選舉“哨兵領導者”時,一個哨兵必須拿到超過多數的選票(5/2+1=3票)。但目前只有2個哨兵活着,無論怎麼投票,一個哨兵最多隻能拿到2票,永遠無法達到多數選票的結果。

但是投票選舉過程的細節並不是大家認爲的:每個哨兵各自1票,這個情況是不一定的。下面具體說一下:

場景a:哨兵A先判定主庫“主觀下線”,然後馬上詢問哨兵B(注意,此時哨兵B只是被動接受詢問,並沒有去詢問哨兵A,也就是它還沒有進入判定“客觀下線”的流程),哨兵B回覆主庫已“主觀下線”,達到quorum=2後哨兵A此時可以判定主庫“客觀下線”。此時,哨兵A馬上可以向其他哨兵發起成爲“哨兵領導者”的投票,哨兵B收到投票請求後,由於自己還沒有詢問哨兵A進入判定“客觀下線”的流程,所以哨兵B是可以給哨兵A投票確認的,這樣哨兵A就已經拿到2票了。等稍後哨兵B也判定“主觀下線”後想成爲領導者時,因爲它已經給別人投過票了,所以這一輪自己就不能再成爲領導者了。

場景b:哨兵A和哨兵B同時判定主庫“主觀下線”,然後同時詢問對方後都得到可以“客觀下線”的結論,此時它們各自給自己投上1票後,然後向其他哨兵發起投票請求,但是因爲各自都給自己投過票了,因此各自都拒絕了對方的投票請求,這樣2個哨兵各自持有1票。

場景a是1個哨兵拿到2票,場景b是2個哨兵各自有1票,這2種情況都不滿足大多數選票(3票)的結果,因此無法完成主從切換。

經過測試發現,場景b發生的概率非常小,只有2個哨兵同時進入判定“主觀下線”的流程時纔可以發生。我測試幾次後發現,都是復現的場景a。

哨兵實例是不是越多越好?

並不是,我們也看到了,哨兵在判定“主觀下線”和選舉“哨兵領導者”時,都需要和其他節點進行通信,交換信息,哨兵實例越多,通信的次數也就越多,而且部署多個哨兵時,會分佈在不同機器上,節點越多帶來的機器故障風險也會越大,這些問題都會影響到哨兵的通信和選舉,出問題時也就意味着選舉時間會變長,切換主從的時間變久。

調大down-after-milliseconds值,對減少誤判是不是有好處?

是有好處的,適當調大down-after-milliseconds值,當哨兵與主庫之間網絡存在短時波動時,可以降低誤判的概率。但是調大down-after-milliseconds值也意味着主從切換的時間會變長,對業務的影響時間越久,我們需要根據實際場景進行權衡,設置合理的閾值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章