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值也意味着主從切換的時間會變長,對業務的影響時間越久,我們需要根據實際場景進行權衡,設置合理的閾值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM