1. 程式人生 > >07Redis入門指南筆記(主從複製、哨兵)

07Redis入門指南筆記(主從複製、哨兵)

        現實專案中通常需要若干臺Redis伺服器的支援:

結構上,單個 Redis 伺服器會發生單點故障,而且一臺伺服器需要承受所有的請求負載。這就需要為資料生成多個副本並分配在不同的伺服器上;

容量上,單個 Redis 伺服器的記憶體非常容易成為儲存瓶頸,所以需要進行資料分片。

同時擁有多個 Redis 伺服器後就會面臨如何管理叢集的問題,包括如何增加節點、故障恢復等操作。

一:複製(replication)

為了避免單點故障,通常的做法是將資料庫複製多個副本以部署在不同的伺服器上,這樣即使有一臺伺服器出現故障,其他伺服器依然可以繼續提供服務。為此,Redis 提供了“複製”功能,當一臺資料庫中的資料更新後,自動將更新的資料同步到其他資料庫上。

1:配置

在複製的概念中,資料庫分為兩類,一類是主庫,另一類是從庫。主庫可以進行讀寫操作,當寫操作導致資料變化時,會自動將資料同步給從庫。從庫一般是隻讀的,並接受主庫同步過來的資料。

一個主庫可以擁有多個從庫,而一個從庫只能擁有一個主庫,如下圖所示:

在 Redis 中使用複製功能非常容易, 只需要在從庫的配置檔案中加入:”slaveof  masterip masterport”即可,而主庫無需進行任何配置。

下面實現一個最簡化的複製系統:在一臺伺服器上啟動兩個 Redis 例項,監聽不同埠,其中一個作為主庫,另一個為從庫。

首先不加任何引數來啟動一個Redis例項作為主庫,該例項預設監聽6379埠。然後啟動另一個Redis例項,加上”slaveof”引數作為從庫,並讓其監聽6380埠:

# redis-server --port 6380 --slaveof 127.0.0.1 6379

此時在主庫中的任何資料變化都會自動地同步到從庫中。開啟終端A執行redis-cli連線到主庫:

[email protected]:~# redis-cli
127.0.0.1:6379> 

再開啟終端B執行redis-cli連線到從庫:

[email protected]:~# redis-cli -p 6380 
127.0.0.1:6380> 

使用”info”命令來分別在終端A和終端B中獲取Replication節點的相關資訊,下面是終端A得到的資訊:

127.0.0.1:6379>info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=379,lag=1
...

可見,得到的角色是master,即主庫,同時已連線的從庫的個數為1。下面是終端B得到的資訊:

127.0.0.1:6380>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
...

可見,得到的角色是slave,即從庫,同時其主庫的IP地址為127.0.0.1,埠為 6379。

在終端A中使用set命令設定一個鍵的值:

127.0.0.1:6379> set foo heheh
OK

此時在終端B中就可以獲得該值了:

127.0.0.1:6380> get foo
"heheh"

預設情況下,從庫是隻讀的, 如果直接修改從庫的資料會出現錯誤:

127.0.0.1:6380> set foo hi
(error) READONLY You can't write against a read only slave.

可以通過設定從庫的配置檔案中的“slave-read-only”為”no”,使從庫可寫,但是對從庫的任何更改都不會同步給任何其他資料庫,並且一旦主庫中更新了對應的資料就會覆蓋從庫中的改動,所以通常的場景下,不應該設定從庫可寫。

配置多臺從庫的方法也一樣,在所有從庫的配置檔案中都加上”slaveof”引數指向同一個主庫即可。

除了通過配置檔案或命令列引數設定”slaveof”引數,還可以在執行時使用”slaveof”命令修改:

127.0.0.1:6380>info replication
# Replication
role:master
connected_slaves:0
...
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
...

如果該資料庫已經是其他主庫的從庫了,則slaveof命令會停止和原來資料庫的同步,轉而和新資料庫進行同步。此外對於從庫來說,還可以使用”slave  no  one”命令來使當前資料庫停止接收其他資料庫的同步並轉換成為主庫。

2:原理

當一個從庫啟動後,會向主庫傳送”sync”命令。主庫收到該命令後,開始在後臺儲存快照(即RDB持久化的過程),並將儲存快照期間接收到的命令快取起來。當快照完成後,Redis會將快照檔案和所有快取的命令傳送給從庫。從庫收到後,會載入快照檔案並執行收到的快取命令。以上過程稱為複製初始化。

複製初始化結束後,主庫每當收到寫命令時就會將命令同步給從庫,從而保證主從庫資料一致。

當主從庫之間的連線斷開重連後,Redis2.6及之前的版本會重新進行復制初始化(即主庫重新儲存快照並傳送給從庫),即使從庫可能僅有幾條命令沒有收到,主庫也必須要將資料庫裡的所有資料重新傳送給從庫。這使得主從庫斷線重連後的資料恢復過程效率很低下,在網路環境不好的時候這一問題尤其明顯。

Redis 2.8版的一個重要改進就是斷線重連後,支援有條件的增量資料傳輸,當從庫重新連線上主庫後,主庫只需要將斷線期間執行的命令傳送給從庫,從而大大提高Redis複製的實用性。後續會詳細介紹增量複製的實現原理以及應用條件。

下面從具體協議角度,詳細介紹複製初始化的過程。Redis伺服器使用 TCP協議通訊,可以使用 telnet 工具偽裝成一個從庫來與主庫通訊。首先在命令列中連線主庫:

[email protected]:~# telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

作為從庫,先要傳送”ping”命令確認主庫是否可以連線:

ping
+PONG

而後向主庫傳送”replconf”命令說明自己的埠號:

replconf listening-port 6381
+OK

這時就可以開始同步的過程了,向主庫傳送”sync”命令開始同步,此時主庫傳送回快照檔案和快取的命令。目前主庫中只有一個foo鍵,所以收到的內容如下(快照檔案是二進位制格式,從第三行開始):

sync
$29 
REDIS0006?foobar?6_?"

從庫將收到的內容寫入到硬碟上的臨時檔案中,當寫入完成後從庫會用該臨時檔案替換RDB快照檔案,之後的操作就和RDB持久化時,啟動恢復的過程一樣了。在同步的過程中從庫並不會阻塞,而是可以繼續處理客戶端發來的命令。

預設情況下,從庫會用同步前的資料對命令進行響應。可以配置”slaveserve-stale-data”引數為”no”來使從庫在同步完成前對所有命令(除了info和slaveof)都回復錯誤:

”SYNC  with  master in progress.“

複製初始化階段結束後,主庫執行的任何導致資料變化的命令都會非同步地傳送給從庫,這一過程為“複製同步階段”。同步的內容和Redis通訊協議一樣。複製同步階段會貫穿整個主從同步過程的始終,直到主從關係終止為止。

在複製的過程中,快照無論在主庫還是從庫中都起了很大的作用,只要執行復制就會進行快照,即使關閉了RDB方式的持久化(通過刪除所有save引數)。Redis 2.8.18 之後支援了無硬碟複製,會在下面介紹。

Redis採用了樂觀複製(optimistic replication)的複製策略,容忍在一定時間內主從庫的內容是不同的,但是兩者的資料會最終同步。具體來說,Redis在主從庫之間複製資料的過程本身是非同步的,這意味著,主庫執行完客戶端請求的命令後會立即將命令在主庫的執行結果返回給客戶端,並非同步地將命令同步給從庫,而不會等待從庫接收到該命令後再返回給客戶端。

這一特性保證了啟用複製後主庫的效能不會受到影響,但另一方面也會產生一個主從庫資料不一致的時間視窗,當主庫執行了一條寫命令後,主庫的資料已經發生的變動,然而在主庫將該命令傳送給從庫之前,如果兩個資料庫之間的網路連線斷開了,此時二者之間的資料就會是不一致的。

從這個角度來看,主庫是無法得知某個命令最終同步給了多少個從庫的,不過 Redis 提供了兩個配置選項,來限制只有當資料至少同步給指定數量的從庫時,主庫才是可寫的:

min-slaves-to-write 3
min-slaves-max-lag 10

“min-slaves-to-write”表示只有當3個(或以上)的從庫連線到主庫時,主庫才是可寫的,否則會返回錯誤:

“NOREPLICAS  Not  enough  good  slaves to  write.”

“min-slaves-max-lag”表示允許從庫最長失去連線的時間,如果從庫最後與主庫聯絡(即傳送“replconf  ack”命令)的時間小於這個值,則認為從庫還在保持與主庫的連線。

舉個例子,按上面的配置,假設主庫與3個從庫相連,其中一個從庫上一次與主庫聯絡是 9 秒前,這時主庫可以正常接受寫入,一旦1秒過後這臺從庫依舊沒有活動,則主庫則認為目前連線的從庫只有2個,從而拒絕寫入。這一特性預設是關閉的,在分散式系統中,開啟併合理配置該選項後可以降低主從架構中因為網路分割槽導致的資料不一致的問題。

從庫不僅可以接收主庫的同步資料,自己也可以同時作為主庫存在,形成類似下圖的結構:

資料庫A的資料會同步到B和C中, 而B中的資料會同步到D和E中。向B中寫入資料不會同步到A或C中,只會同步到 D和E中。

3:讀寫分離與一致性

通過複製可以實現讀寫分離,以提高伺服器的負載能力。在常見的場景中(如電子商務網站),讀的頻率大於寫,當單機的Redis無法應付大量的讀請求時(尤其是較耗資源的請求,如sort命令等),可以通過複製功能建立多個從庫節點,主庫只進行寫操作,而從庫負責讀操作。

這種一主多從的結構很適合讀多寫少的場景,而當單個主庫不能夠滿足需求時,就需要使用Redis 3.0 推出的叢集功能,後續會詳細介紹。

4:從庫持久化

另一個相對耗時的操作是持久化,為了提高效能,可以通過複製功能建立一個(或若干個)從庫,並在從庫中啟用持久化,同時在主庫禁用持久化。當從庫崩潰重啟後,主庫會自動將資料同步過來,所以無需擔心資料丟失。然而當主庫崩潰時,情況就稍顯複雜了。

手工通過從庫資料恢復主庫資料時,需要嚴格按照以下兩步進行:

a:在從庫中使用 “slaveof  no  one”命令將從庫提升成主庫繼續服務;

b:啟動之前崩潰的主庫,然後使用”slaveof”命令將其設定成新主庫的從庫,即可將資料同步回來。

注意,當開啟複製且主庫關閉持久化功能時,一定不要使用 Supervisor 以及類似的程序管理工具令主庫崩潰後自動重啟。同樣當主庫所在的伺服器因故關閉時,也要避免直接重新啟動。這是因為當主庫重新啟動後,因為沒有開啟持久化功能,所以資料庫中所有資料都被清空,這時從庫依然會從主庫中接收資料,使得所有從庫也被清空,導致從庫的持久化失去意義。

無論哪種情況,手工維護從庫或主庫的重啟以及資料恢復都相對麻煩,好在Redis提供了一種自動化方案:“哨兵”來實現這一過程,避免了手工維護的麻煩和容易出錯的問題,後續會詳細介紹“哨兵”。

5:無硬碟複製

介紹Redis複製的工作原理時,介紹了複製是基於RDB方式的持久化實現的,即主庫端在後臺儲存 RDB 快照,從庫端則接收並載入快照檔案。這樣的實現優點是可以顯著地簡化邏輯,複用已有的程式碼,但是缺點也很明顯:

a:當主庫禁用RDB快照時(即刪除了所有的配置檔案中的save語句),如果執行了複製初始化操作,Redis依然會生成RDB快照,所以下次啟動後主庫會以該快照恢復資料。因為複製發生的時間不能確定,這使得恢復的資料可能是任何時間點的。

b:因為複製初始化時需要在硬碟中建立RDB快照檔案,所以如果硬碟效能很慢,則這一過程會對效能產生影響。

因此從2.8.18版本開始,Redis引入了“無硬碟複製”選項,開啟該選項時,Redis 在與從庫進行復制初始化時將不會將快照內容儲存到硬碟上,而是直接通過網路傳送給從庫,避免了硬碟的效能瓶頸。

目前無硬碟複製的功能還在試驗階段,可以在配置檔案中使用如下配置來開啟該功能:

repl-diskless-sync yes

6:增量複製

在介紹複製原理時提到,當主從庫連線斷開後,從庫會發送sync命令來重新進行一次完整複製操作。這樣即使斷開期間資料庫的變化很小(甚至沒有),也需要將資料庫中的所有資料重新快照並傳送一次。這種實現方式顯然不太理想。

Redis 2.8版相對2.6版的最重要的更新之一,就是實現了主從斷線重連情況下的增量複製功能。增量複製是基於如下3點實現的:

a:從庫會儲存主庫的執行ID。每個Redis 執行例項均會擁有一個唯一的執行ID,每當例項重啟後,就會自動生成一個新的執行ID。

b:在複製同步階段,主庫每將一個命令傳送給從庫時,都會同時把該命令存放到一個積壓佇列(backlog)中,並記錄下當前積壓佇列中,存放的命令的偏移量範圍。

c:同時,從庫接收到主庫傳來的命令時,會記錄下該命令的偏移量。

注意,主庫和所有從庫都記錄了命令的偏移量。以上3點是實現增量複製的基礎。當主從連線準備就緒後,從庫會發送一條sync命令來告訴主庫可以開始把所有資料同步過來了。而2.8版之後,不再發送sync命令,取而代之的是傳送psync,格式為“psync  主庫的執行ID  斷開前最新的命令偏移量”。

主庫收到psync命令後,會執行以下判斷來決定此次重連是否可以執行增量複製:

a:首先主庫會判斷從庫傳送來的執行ID是否和自己的執行ID相同。這一步驟的意義在於確保從庫之前確實是和自己同步的,以免從庫拿到錯誤的資料(比如主庫在斷線期間重啟過,會造成資料的不一致);

b:然後判斷從庫最後同步成功的命令偏移量是否在積壓佇列中,如果在,則可以執行增量複製,並將積壓佇列中相應的命令傳送給從庫。如果此次重連不滿足增量複製的條件,主庫會進行一次全部同步(即與Redis 2.6的過程相同)。

大部分情況下,增量複製的過程對開發者來說是完全透明的,開發者不需要關心增量複製的具體細節。2.8 版本的主庫也可以正常地和舊版本的從庫同步(通過接收sync命令),同樣 2.8 版本的從庫也可以與舊版本的主庫同步(通過傳送sync命令)。

唯一需要開發者設定的就是積壓佇列的大小了。積壓佇列本質上是一個固定長度的迴圈佇列,預設情況下積壓佇列的大小為 1 MB,可以通過配置檔案的” repl-backlog-size”選項來調整。積壓佇列越大,其允許的主從庫斷線的時間就越長。

根據主從庫之間的網路狀態,設定一個合理的積壓佇列很重要。因為積壓佇列儲存的內容是命令本身,如”SET  foo  bar”,所以估算積壓佇列的大小隻需要估計主從庫斷線的時間中,主庫可能執行的命令的大小即可。

與積壓佇列相關的另一個配置選項是”repl-backlog-ttl”,即當所有從庫與主庫斷開連線後,經過多久時間可以釋放積壓佇列的記憶體空間。預設時間是1小時。

二:哨兵

在一個典型的一主多從的Redis 系統中,從庫在整個系統中起到了資料冗餘備份和讀寫分離的作用。當主庫遇到異常中斷服務後,開發者可以通過手動的方式選擇一個從庫來升級為主庫,以使得系統能繼續提供服務。

然而整個過程相對麻煩且需要人工介入,難以實現自動化。為此,Redis 2.8中提供了“哨兵”工具來實現自動化的系統監控和故障恢復功能。

注意 Redis 2.6 版也提供了哨兵工具,但此時的哨兵是1.0版,存在非常多的問題,任何情況下都不應該使用這個版本的哨兵。本章介紹的哨兵都是Redis 2.8提供的哨兵

1:什麼是哨兵

哨兵的作用就是監控 Redis系統的執行狀況。它的功能包括:

a:監控主庫和從庫是否正常執行;

b:主庫出現故障時自動將從庫轉換為主庫;

哨兵是一個獨立的程序,使用哨兵的一個典型架構如下圖所示,其中虛線表示主從複製關係,實線表示哨兵的監控路徑:


在一主多從的Redis系統中,可以使用多個哨兵進行監控,以保證系統足夠穩健,此時,不僅哨兵會監控主庫和從庫,哨兵之間也會互相監控。如下圖所示:


2:實踐

在理解哨兵的原理前,首先實際使用一下哨兵,來了解哨兵是如何工作的。為了簡單起見,在同一個主機上建立3個Redis例項,包括一個主庫和兩個從庫。主庫的埠為6379,兩個從庫的埠分別為6380和6381。使用Redis-cli獲取複製狀態,以保證複製配置正確。首先是主庫:

127.0.0.1:6379>info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=57,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=57,lag=0
master_repl_offset:57
...


可見其連線了兩個從庫。然後用同樣的方法檢視兩個從庫的配置:

127.0.0.1:6380>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
...
127.0.0.1:6381>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
...

接下來開始配置哨兵。建立一個配置檔案sentinel.conf,內容為:

sentinel monitor mymaster 127.0.0.1 6379 1

其中mymaster表示要監控的主庫的名字,可以自己定義。這個名字必須僅由大小寫字母、數字和”.-_”這3個字元組成。後兩個引數表示主庫的IP地址和埠號。最後的1表示最低通過票數,後面會介紹。

接下來啟動 Sentinel程序,並將上述配置檔案的路徑傳遞給哨兵:

# redis-sentinel /root/sentinel.conf 

配置哨兵監控一個系統時,只需要配置其監控主庫即可,哨兵會自動發現所有複製該主庫的從庫,具體原理後面會詳細介紹。

啟動哨兵後,哨兵輸出如下內容:

...
2112:X 06 Dec 08:28:12.550 # Sentinel runid is ba22decd98aff96e61faca44d7bfd1b4a911cc26
2112:X 06 Dec 08:28:12.550 # +monitor master mymaster 127.0.0.1 6379 quorum 1
2112:X 06 Dec 08:28:13.550 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2112:X 06 Dec 08:28:13.568 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379

其中”+slave”表示新發現了從庫,可見哨兵成功地發現了兩個從庫。

現在哨兵已經在監控這3個Redis例項了,這時將主庫關閉(殺死程序或使用 shutdown 命令),等待指定時間後(可配置,預設為 30 秒),哨兵會輸出如下內容:

2112:X 06 Dec 08:33:40.709 # +sdown master mymaster 127.0.0.1 6379
2112:X 06 Dec 08:33:40.711 # +odown master mymaster 127.0.0.1 6379 #quorum 1/1
...

其中”+sdown”表示哨兵主觀認為主庫停止服務了,而”+odown”則表示哨兵客觀認為主庫停止服務了,關於主觀和客觀的區別後文會詳細介紹。此時哨兵開始執行故障恢復,即挑選一個從庫,將其升級為主庫。輸出如下內容:

2112:X 06 Dec 08:33:40.713 # +try-failover master mymaster 127.0.0.1 6379
...
2112:X 06 Dec 08:33:42.861 # +failover-end master mymaster 127.0.0.1 6379
2112:X 06 Dec 08:33:42.862 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6381
2112:X 06 Dec 08:33:42.864 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381
2112:X 06 Dec 08:33:42.864 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
2112:X 06 Dec 08:34:12.932 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381

“+try-failover”表示哨兵開始進行故障恢復,”+failover-end”表示哨兵完成故障恢復,期間涉及的內容比較複雜,包括領頭哨兵的選舉、備選從庫的選擇等,放到後面介紹,此處只需要關注最後4條輸出。”+switch-master”表示主庫從6379埠遷移到6381埠,即6381埠的從庫升級為主庫,兩個”+slave”則列出了新主庫的兩個從庫,埠分別為6380和6379。其中6379就是之前停止服務的主庫。

哨兵並沒有徹底清除停止服務例項的資訊,這是因為停止服務的例項可能會在之後的某個時間恢復服務,這時哨兵會讓其重新加入進來,所以當例項停止服務後,哨兵會更新該例項的資訊,使得當其重新加入後可以按照當前資訊繼續對外提供服務。此例中6379埠的主庫例項停止服務了,而6381 埠的從庫已經升級為主庫,當6379埠的例項恢復服務後,會轉變為6381埠例項的從庫來執行,所以哨兵將6379埠例項的資訊修改成了 6381埠例項的從庫。

故障恢復完成後,可以使用”info  replication”重新檢查6380和6381兩個埠上的例項的複製資訊:

127.0.0.1:6380>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:up
...
127.0.0.1:6381>info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=59391,lag=0

可見6381埠上的例項已經確實升級為主庫了,同時6380埠上的例項是其從庫。整個故障恢復過程就此完成。

如果此時將6379埠上的例項重新啟動,會發生什麼情況呢?首先哨兵會監控到這一變化,並輸出:

2112:X 06 Dec 08:57:17.572 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
2112:X 06 Dec 08:57:27.546 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381

“-sdown”表示例項6379已經恢復了服務(與+sdown相反),同時”+convert-to-slave”表示將6379埠的例項設定為6381埠例項的從庫。這時使用”info replication”檢視6379埠例項的複製資訊為:

127.0.0.1:6379>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:up

同時6381埠例項的複製資訊為:

127.0.0.1:6381>info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=105334,lag=1
slave1:ip=127.0.0.1,port=6379,state=online,offset=105334,lag=1

正如預期一樣,6381埠例項的從庫變為了兩個,6379成功恢復服務。

3:實現原理

哨兵程序啟動時讀取配置檔案的內容,通過如下的配置找出需要監控的主庫:

sentinel monitor master-name ip redis-port quorum

master-name 是主庫的名字,因為考慮到故障恢復後當前監控的系統的主庫的地址和埠會產生變化,所以哨兵提供了命令可以通過主庫的名字獲取當前系統的主庫的IP地址和埠號。

ip表示當前系統中主庫的地址,redis-port則表示埠號。

quorum表示執行故障恢復操作前,至少需要幾個哨兵節點同意,後文會詳細介紹。

一個哨兵節點可以同時監控多個Redis主從系統,只需要提供多個”sentinel monitor”配置即可,例如:

sentinel monitor mymaster 127.0.0.1 6379 2 
sentinel monitor othermaster 192.168.1.3 6380 4 

多個哨兵節點也可以同時監控同一個Redis 主從系統,從而形成網狀結構。

配置檔案中還可以定義其他監控相關的引數,每個配置選項都包含主庫的名字使得監控不同主庫時可以使用不同的配置引數。例如:

sentinel down-after-milliseconds mymaster 60000 
sentinel down-after-milliseconds othermaster 10000 

上面的兩行配置分別配置了mymaster 和othermaster的”down-after-milliseconds”選項,分別為60000和10000。

哨兵啟動後,會與要監控的主庫建立兩條連線,這兩個連線的建立方式與普通的Redis客戶端無異。其中一條用來訂閱該主庫的”__sentinel__:hello”頻道,以獲取其他同樣監控該資料庫的哨兵節點的資訊。

另外哨兵也需要定期向主庫傳送info等命令來獲取主庫本身的資訊,之前介紹過當客戶端的連線進入訂閱模式時就不能再執行其他命令了,所以這時哨兵會使用另外一條連線來發送這些命令。

和主庫的連線建立完成後,哨兵會定時執行下面3個操作。

a:每10秒向主庫和從庫傳送info命令;

b:每2秒向主庫和從庫的”__sentinel__:hello”頻道傳送自己的資訊,也就是說哨兵不但訂閱了該頻道,而且還會向該頻道釋出資訊,以使其他哨兵得到自己的資訊;

c:每1秒向主庫、從庫和其他哨兵節點發送ping命令。

這3個操作貫穿哨兵程序的整個生命週期中,非常重要。下面分別詳細介紹。

首先,傳送info命令使哨兵可以獲得當前資料庫的相關資訊(包括執行ID、複製資訊等)從而實現新節點的自動發現。配置哨兵監控 Redis 主從系統時只需要指定主庫的資訊即可,因為哨兵正是藉助info命令來獲取所有複製該主庫的從庫資訊的。

啟動後,哨兵向主庫傳送info命令得到從庫列表,而後對每個從庫同樣建立兩個連線,兩個連線的作用和與主庫建立的兩個連線完全一致。在此之後,哨兵會每 10 秒定時向已知的所有主從庫傳送info命令來獲取更新資訊,並進行相應操作。比如對新增的從庫建立連線並加入監控列表,對主從庫的角色變化(由故障恢復操作引起)進行資訊更新等。

接下來哨兵向主從庫的”__sentinel__:hello”頻道傳送資訊來與同樣監控該資料庫的哨兵分享自己的資訊。傳送的訊息內容為:

<哨兵的地址>,<哨兵的埠>, <哨兵執行ID>, <哨兵的配置版本>, <主庫的名字>, <主庫的地址>, <主庫的埠>, <主庫的配置版本>

哨兵會訂閱每個其監控的資料庫的”__sentinel__:hello”頻道,所以當其他哨兵收到訊息後,會判斷髮訊息的哨兵是不是新發現的哨兵。如果是,則將其加入已發現的哨兵列表中並建立一個到其的連線(與資料庫不同,哨兵與哨兵之間只會建立一條連線用來發送ping命令,而不需要建立另外一條連線來訂閱頻道,因為哨兵只需要訂閱資料庫的頻道即可實現自動發現其他哨兵)。

同時,哨兵會判斷資訊中主庫的配置版本,如果該版本比當前記錄的主庫的版本高,則更新主庫的資料。配置版本的作用會在後面詳細介紹。

實現了自動發現從庫和其他哨兵節點後,哨兵要做的就是定時監控這些資料庫和節點有沒有停止服務。這是通過每隔一定時間向這些節點發送ping命令實現的。

傳送ping的時間間隔與”down-after-milliseconds”選項有關,最長間隔為1秒。當”down-after-milliseconds”的值小於1秒時,哨兵會每隔”down-after-milliseconds”指定的時間傳送一次ping命令,當down-after-milliseconds的值大於1秒時,哨兵會每隔1秒傳送一次ping命令。

如果超過”down-after-milliseconds”指定時間後,被ping的節點仍未回覆,則哨兵認為其主觀下線(subjectively  down)。主觀下線表示,從當前的哨兵程序看來,該節點已經下線。如果該節點是主庫,則哨兵會進一步判斷是否需要對其進行故障恢復:哨兵傳送”SENTINEL is-master-down-by-addr”命令詢問其他哨兵節點以瞭解他們是否也認為該主庫主觀下線,如 果達到指定數量時,哨兵會認為其客觀下線(objectively  down),並選舉領頭的哨兵節點發起故障恢復。這個指定數量即為前文介紹的”quorum”引數。例如,下面的配置:

sentinel monitor mymaster 127.0.0.1 6379 2 

該配置表示只有當至少兩個哨兵節點(包括當前節點)認為該主庫主觀下線時,當前哨兵節點才會認為該主庫客觀下線。

接下來開始進行領頭哨兵的選舉。雖然當前哨兵節點發現了主庫客觀下線,需要故障恢復,但是故障恢復需要由領頭的哨兵來完成,這樣可以保證同一時間只有一個哨兵節點來執行故障恢復。選舉領頭哨兵的過程使用了 Raft演算法,具體過程如下。

a:發現主庫客觀下線的哨兵節點(下面稱作A)向每個哨兵節點發送命令,要求對方選自己成為領頭哨兵。

b:如果目標哨兵節點沒有選過其他人,則會同意將A設定成領頭哨兵。

c: 如果A發現有超過半數且超過quorum引數值的哨兵節點同意選自己成為領頭哨兵,則A成功成為領頭哨兵。

d:若有多個哨兵節點同時參選領頭哨兵,則會出現沒有任何節點當選的可能。此時每個參選節點將等待一個隨機時間重新發起參選請求,進行下一輪選舉,直到選舉成功。

具體過程可以參考Raft演算法的過程http://raftconsensus.github.io/。因為要成為領頭哨兵必須有超過半數的哨兵節點支援,所以每次選舉最多隻會選出一個領頭哨兵。

選出領頭哨兵後,領頭哨兵開始對主庫進行故障恢復。故障恢復的過程相對簡單,具體如下:

首先領頭哨兵將從停止服務的主庫的從庫中挑選一個來充當新的主庫。挑選的依據如下:

a:所有線上的從庫中,選擇優先順序最高的從庫。優先順序可以通過”slave-priority”選項來設定;

b:如果有多個最高優先順序的從庫,則複製的命令偏移量越大(即複製越完整)越優先;

c:如果以上條件都一樣,則選擇執行ID較小的從庫。

選出一個從庫後,領頭哨兵將向從庫傳送”slaveof  no  one”命令,使其升級為主庫。而後領頭哨兵向其他從庫傳送slaveof命令來使其成為新主庫的從庫。

最後一步則是更新內部的記錄,將已經停止服務的,舊的主庫更新為新的主庫的從庫,使得當其恢復服務時自動以從庫的身份繼續服務。

4:哨兵的部署

哨兵以獨立程序的方式對一個主從系統進行監控,監控的效果好壞取決於哨兵的視角是否有代表性。如果一個主從系統中配置的哨兵較少,哨兵對整個系統的判斷的可靠性就會降低。極端情況下,當只有一個哨兵時,哨兵本身就可能會發生單點故障。整體來講,相對穩妥的哨兵部署方案是使得哨兵的視角儘可能地與每個節點的視角一致,即:

a:為每個節點(無論是主庫還是從庫)部署一個哨兵;

b:使每個哨兵與其對應的節點的網路環境相同或相近;

這樣的部署方案可以保證哨兵的視角擁有較高的代表性和可靠性。舉個例子:當網路分割槽後,如果哨兵認為某個分割槽是主要分割槽,即意味著從每個節點觀察,該分割槽均為主分割槽。

同時設定 quorum 的值為 N/2 + 1(其中N為哨兵節點數量),這樣使得只有當大部分哨兵節點同意後才會進行故障恢復。

當系統中的節點較多時,考慮到每個哨兵都會和系統中的所有節點建立連線,為每個節點分配一個哨兵會產生較多連線,尤其是當進行客戶端分片時使用多個哨兵節點監控多個主庫,會因為 Redis 不支援連線複用而產生大量冗餘連線,具體可以見此issue: https://github.com/antirez/redis/issues/2257;同時如果Redis節點負載較高,會在一定程度上影響其對哨兵的回覆以及與其同機的哨兵與其他節點的通訊。所以配置哨兵時還需要根據實際的生產環境情況進行選擇。