緩存學習(九):分佈式Redis之副本、哨兵

目錄

1.配置主從節點

2.複製原理

2.1 複製流程

2.1.1 全量複製

2.1.2 斷點續傳

2.2 PSYNC協議

2.3 心跳機制

3 哨兵Sentinel

3.1 簡介

3.2 配置

3.3 搭建

3.4 命令

3.5 哨兵實現原理

3.5.1 Pub/Sub

3.5.2 Leader與Epoch

3.5.3 監控和從節點選取

3.6 Jedis對哨兵的支持


1.配置主從節點

主從關係的配置有三種方式:

需要注意的是,如果主節點配置了requirepass,從節點需要配置materauth才能連接,下面是一個連接示例(6379端口實例爲主節點,此處啓動的6380端口實例爲從節點):

root@Yhc-Surface:~# redis-server --port 6380 --slaveof localhost 6379
...
24:S 28 Apr 2019 14:31:19.041 * Connecting to MASTER localhost:6379
24:S 28 Apr 2019 14:31:19.046 * MASTER <-> REPLICA sync started
24:S 28 Apr 2019 14:31:19.048 * Non blocking connect for SYNC fired the event.
24:S 28 Apr 2019 14:31:19.049 * Master replied to PING, replication can continue...
24:S 28 Apr 2019 14:31:19.050 * Partial resynchronization not possible (no cached master)
24:S 28 Apr 2019 14:31:19.056 * Full resync from master: 1197e315344ccb5e97581f126ca28388080feba1:0
24:S 28 Apr 2019 14:31:19.082 * MASTER <-> REPLICA sync: receiving 1070 bytes from master
24:S 28 Apr 2019 14:31:19.084 * MASTER <-> REPLICA sync: Flushing old data
24:S 28 Apr 2019 14:31:19.084 * MASTER <-> REPLICA sync: Loading DB in memory
24:S 28 Apr 2019 14:31:19.086 * MASTER <-> REPLICA sync: Finished with success

可以看到,6380節點啓動後自動連接主節點並進行了一次全量複製。 此時在6379插入一條數據,在6380進行查詢:

127.0.0.1:6379> set hello world
OK

127.0.0.1:6380> get hello
"world"

斷開連接只需要在從節點執行slaveof no one命令即可,同理,如果要更換主節點,也只需要使用slaveof命令即可,但是更換主節點後,原本的數據會丟失(斷開與主節點的連接則不會清空數據):

127.0.0.1:6380> slaveof localhost 6381
OK
127.0.0.1:6380> get hello
(nil)

在Redis副本機制中,會在初次連接以及長時間斷開連接時進行全量複製,之後每次執行寫入操作都會自動複製,如果短期斷開連接,則從會在重新連接後,進行增量同步,無論哪種複製方式,都默認是異步的。在默認情況下,從節點只處理讀請求。

Redis副本有三種結構:

最簡單的是一主一從結構,這種方式可以幫助主節點分擔至少一半的讀請求,此外還可以在主節點處關閉持久化,將持久化的壓力轉移到從節點上,不過這樣做的一個後果是,如果主節點下線後又立即重啓,會因爲沒有日誌或快照,導致主節點數據清空,經過同步後把從節點的數據也清空了。這種時候,需要先斷開主從節點之間的連接,然後重啓主節點,再倒轉主從關係。

第二種結構是星型結構,即一主多從,這種結構適合讀多寫少的場景,缺點是初次連接時容易造成主節點擁塞,一個改進思路是讓從節點分批上線。

第三種結構是樹狀結構,即某些從節點作爲其他從節點的主節點,實現級聯複製,這樣一來可以避免多寫對主節點造成的壓力,二來可以實現指數級的數據擴散速度,缺點就是比較難管理。

Redis副本的相關信息可以通過info replication或role命令查看,示例如下:

127.0.0.1:6379> role
1) "master"
2) (integer) 8637
3) 1) 1) "127.0.0.1"
      2) "6380"
      3) "8637"
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=8637,lag=1
master_replid:32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:8637
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:8637

對於role命令,其在主節點上輸出有三部分,1)是其角色,有master和slave、sentinel三種,2)是主節點複製偏移量,3)是連接的從節點。在從節點上輸出有5部分,分別是:角色(slave)、主節點IP、主節點端口、複製狀態(connected、connecting、sync三種)、一共從主節點接收了多少數據。sentinel的下面再介紹。

對於info replication,內容比較長,但是自描述性較好就不做介紹了。

2.複製原理

2.1 複製流程

2.1.1 全量複製

首先看一下主從雙方的日誌,首先是主節點;

192:M 28 Apr 2019 15:25:47.935 * Replica 127.0.0.1:6380 asks for synchronization
192:M 28 Apr 2019 15:25:47.936 * Full resync requested by replica 127.0.0.1:6380
192:M 28 Apr 2019 15:25:47.936 * Starting BGSAVE for SYNC with target: disk
192:M 28 Apr 2019 15:25:47.948 * Background saving started by pid 200
200:C 28 Apr 2019 15:25:47.959 * DB saved on disk
192:M 28 Apr 2019 15:25:48.050 * Background saving terminated with success
192:M 28 Apr 2019 15:25:48.052 * Synchronization with replica 127.0.0.1:6380 succeeded

然後是從節點:

196:S 28 Apr 2019 15:25:47.922 * Connecting to MASTER localhost:6379
196:S 28 Apr 2019 15:25:47.931 * MASTER <-> REPLICA sync started
196:S 28 Apr 2019 15:25:47.931 * Non blocking connect for SYNC fired the event.
196:S 28 Apr 2019 15:25:47.933 * Master replied to PING, replication can continue...
196:S 28 Apr 2019 15:25:47.933 * Partial resynchronization not possible (no cached master)
196:S 28 Apr 2019 15:25:47.950 * Full resync from master: 32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2:0
196:S 28 Apr 2019 15:25:48.051 * MASTER <-> REPLICA sync: receiving 175 bytes from master
196:S 28 Apr 2019 15:25:48.053 * MASTER <-> REPLICA sync: Flushing old data
196:S 28 Apr 2019 15:25:48.056 * MASTER <-> REPLICA sync: Loading DB in memory
196:S 28 Apr 2019 15:25:48.057 * MASTER <-> REPLICA sync: Finished with success

根據以上內容,可以推斷出如下複製流程:

  • 首先,從節點連接主節點,由於是初次連接,因此,連接成功後需要進行全量複製,爲了確保可以進行復制,首先進行了一次PING
  • 如果PING成功,則由從節點發出SYNC信號,發起全量同步事件
  • 主節點收到SYNC後,開始BGSAVE操作,將當前數據保存爲快照
  • 快照保存完畢後,開始發送給從節點,從節點清空舊數據並載入主節點發來的新數據

SYNC是Redis的同步協議,不過目前已經改用PSYNC,後者支持部分同步,如果直接向6379端口發送SYNC,會有如下輸出:

root@Yhc-Surface:~# echo 'SYNC' | nc localhost 6379
$410
REDIS0009       redis-ver5.0.4
redis-bits@ctime¨]used-memB repl-stream-db repl-id(32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2repl-offsetMaof-pre:hobbymalet@K@__   K   programming
sexualage redisgood helloworld testmyteststream  jb        <<     nameagesexual   zhangsan  male  jb  dO*1
$4
PING
*1
$4
PING
*1
$4
PING
... //每隔幾秒輸出一次 *1\r\n$4\r\nPING\r\n

SYNC實際就是傳輸RDB的內容。PSYNC會在下面介紹。

可以看到,當進行全量同步時,有如下日誌:

Full resync from master: 32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2:0

這裏的master被一個字符串標記了,以冒號爲分隔,由兩部分組成,前面較長的一串被稱爲副本ID,是一個僞隨機串,用來標記數據集,後面的0是偏移量,用來標記從數據集的哪一部分開始複製,這裏由於是全量複製,所以肯定是從0開始。

該流程建立在沒有開啓 repl-diskless-sync 的情況下,該選項開啓後不會先保存RDB再發送,而是直接發送數據

以上兩段日誌未能體現的是:1)如果在複製期間有寫請求到達主節點,這些請求正常響應後,都會存入backlog隊列,等到快照發送完畢後再發給從節點 2)從節點如果開啓了AOF,則會再同步完畢後立刻執行一次rewrite

2.1.2 斷點續傳

上面的複製流程是全量複製的,當主從節點斷開一段時間後(從節點關閉,不能使用slave no one,後者會導致從節點進入主節點模式,導致副本ID變化,使得每次重連都會進行全量同步),如果重新進行連接,顯然不需要完整複製,這種方式成爲斷點續傳。首先還是看主節點日誌:

192:M 28 Apr 2019 15:47:47.750 * Synchronization with replica 127.0.0.1:6380 succeeded
192:M 28 Apr 2019 15:48:38.363 # Connection with replica 127.0.0.1:6380 lost.
192:M 28 Apr 2019 15:48:40.331 * Replica 127.0.0.1:6380 asks for synchronization
192:M 28 Apr 2019 15:48:40.332 * Partial resynchronization request from 127.0.0.1:6380 accepted. Sending 70 bytes of backlog starting from offset 1897.

然後是從節點日誌:

212:S 28 Apr 2019 15:48:40.323 * Connecting to MASTER localhost:6379
212:S 28 Apr 2019 15:48:40.328 * MASTER <-> REPLICA sync started
212:S 28 Apr 2019 15:48:40.328 * Non blocking connect for SYNC fired the event.
212:S 28 Apr 2019 15:48:40.329 * Master replied to PING, replication can continue...
212:S 28 Apr 2019 15:48:40.330 * Trying a partial resynchronization (request 32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2:1897).
212:S 28 Apr 2019 15:48:40.332 * Successful partial resynchronization with master.
212:S 28 Apr 2019 15:48:40.334 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

可以看到,這次從節點要求從偏移量爲1897處開始複製,主節點確認副本ID後,直接發送了70字節數據,而非全量複製

2.2 PSYNC協議

在2.1.1小節,我們嘗試直接向6379端口發送SYNC,獲得了RDB內容,但是如果直接發送PSYNC,會提示參數不足:

root@Yhc-Surface:~# echo 'PSYNC' | nc localhost 6379
-ERR wrong number of arguments for 'psync' command

PSYNC命令的參數就是副本ID和偏移量,現在我們再試一次:

root@Yhc-Surface:~# echo 'PSYNC 32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2 1897' | nc localhost 6379
+CONTINUE
... //大量的 *1\r\n$4\r\nPING\r\n
*2
$6
SELECT
$1
0
*3
$3
set
$5
redis
$4
good
... //大量的 *1\r\n$4\r\nPING\r\n
*9
$4
xadd
$12
myteststream
$15
1556437967499-0
$4
name
$8
zhangsan
$3
age
$2
18
$6
sexual
$4
male
... //下略

全量複製時,副本ID是問號"?",偏移量是-1,效果跟發送SYNC類似:

root@Yhc-Surface:~# echo 'PSYNC ? -1' | nc localhost 6379
+FULLRESYNC 32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2 4423
$410
REDIS0009       redis-ver5.0.4
redis-bits@ctimeGaused-memXC repl-stream-db repl-id(32e1f0aa4381c3fd906a21b7a748ff1fa93be2b2repl-offsetGaof-pre:hobbymalet@K@__   K   programming
sexualage redisgood helloworld testmyteststream  jb        <<     nameagesexual   zhangsan  male  jb  A|

從以上兩個例子來看,如果是全量複製,則返回內容的第一行是:+FULLRESYNC replicationId offset,如果是部分複製,則是+CONTINUE,無法識別的命令例子裏沒有體現,實際返回+ERR。

2.3 心跳機制

從主從節點中還可以看到的是大量的PING報文,這就是Redis副本機制中的心跳機制。心跳機制中,主從雙方各模擬爲對方的客戶端,主節點每隔repl-ping-slave-period時間PING一次從節點,而從節點每秒發送一次replconf ack offset給主節點。

心跳機制主要作用有:

  • 探測主、從節點的網絡狀態
  • 從節點通過replconf上報自己的複製偏移量
  • 可以用來控制從節點數量和延遲

3 哨兵Sentinel

3.1 簡介

一個一主多從的Redis集羣本身還是沒有高可用性,主要表現在:用戶無法及時判斷節點不可達,從節點晉升需要人工干預,還需要對所有涉及到的應用進行修改,工作量很大。一個思路是使用代理,對外提供統一的訪問地址,可以減少節點替換對應用端的影響,不過另外兩個問題無法解決。

人工干預即通過編碼或手動操作實現failover邏輯,即主節點因故下線後,從節點晉升爲新的主節點對外提供服務。一種可行方案是編寫程序定時與各節點通信,一旦從節點下線,就將其從節點列表中暫時移除,直到其恢復正常重新加入集羣;一旦主節點下線,就從從節點中挑選一個成爲新的主節點。

該方案的問題主要有兩個:

1)如何選擇合適的從節點晉升?新的主節點應該有以下特性:性能儘可能好(包括處理能力強、網絡延遲低等),該節點上的數據版本儘可能新,不過符合以上要求的節點可能不止一個,究竟該如何選擇?

2)監控程序自身的可用性如何保證?如果只有一個節點,則仍不夠健壯,如果是多個節點的集羣,它們又該如何管理?難道再加一個監控程序?這樣下去就是無盡的死循環。

Sentinel(哨兵)是Redis提供的副本監控、異常告警和故障轉移機制,能夠解決以上問題。

Sentinel本身沒有主/從節點之分,是完全去中心化的,統稱Sentinel節點,其集羣稱爲Sentinel節點集合。Redis的主/從節點被稱爲數據節點。Sentinel節點集合與數據節點組合爲Redis Sentinel。

下面分析以下幾種拓撲下,哨兵的工作情況:

1)一主一從,每一個數據節點和一個哨兵節點處於同一分區:

+----+         +----+
| M1 |---------| R1 |
| S1 |         | S2 |
+----+         +----+

Configuration: quorum = 1

假設此時兩個分區之間網絡中斷,那麼哨兵也無法互相感知,從而導致不可用

2) 一主多從,每個數據節點都和一個哨兵節點處於同一分區

       +----+
       | M1 |
       | S1 |
       +----+
          |
+----+    |    +----+
| R2 |----+----| R3 |
| S2 |         | S3 |
+----+         +----+

Configuration: quorum = 2

這種情況下,即便發生網絡分區,使得M1、S1都斷開,也可以保證有足夠多的哨兵節點達成一致,選出新的主節點

3)哨兵與客戶端處於同一分區

            +----+         +----+
            | M1 |----+----| R1 |
            |    |    |    |    |
            +----+    |    +----+
                      |
         +------------+------------+
         |            |            |
         |            |            |
      +----+        +----+      +----+
      | C1 |        | C2 |      | C3 |
      | S1 |        | S2 |      | S3 |
      +----+        +----+      +----+

      Configuration: quorum = 2

這種結構也具有良好的可用性,M1斷開後,可以順利選舉R1爲主節點

4)客戶端少於2個的情況

此時需要在服務器節點處也部署哨兵,也可以完成故障轉移

            +----+         +----+
            | M1 |----+----| R1 |
            | S1 |    |    | S2 |
            +----+    |    +----+
                      |
               +------+-----+
               |            |  
               |            |
            +----+        +----+
            | C1 |        | C2 |
            | S3 |        | S4 |
            +----+        +----+

      Configuration: quorum = 3

Sentinel節點可以通過redis-server的--sentinel選項運行,類似於數據節點,它也有配置文件,默認爲sentinel.conf。

3.2 配置

Sentinel節點的許多配置和數據節點的類似,可參照緩存學習(五):Redis安裝、配置

1)網絡配置

  • bind:含義和數據節點的一致,但是默認不配置,官方認爲哨兵節點不應該被外界訪問,至少應該處於足夠的保護下
  • protected-mode:含義與數據節點一致,默認不配置
  • port:哨兵監聽的端口,默認26379
  • sentinel announce-ip、sentinel announce-port:作用和slave-announce-ip、slave-announce-port類似

2)基本配置

  • daemonize:是否後臺運行,默認no
  • pidfile、logfile:和數據節點作用一致
  • dir:工作目錄
  • sentinel rename-command :和數據節點的rename-command作用一樣

3)監控配置

  • sentinel monitor <master-name> <ip> <redis-port> <quorum>:指定監控的主節點,quorum的意思是,如果有quorum個節點認爲主節點已下線(主觀下線),則判定它客觀下線,默認爲2,一般設置爲節點總數的一半加1
  • sentinel auth-pass <master-name> <password>:和master-auth配置作用一致
  • sentinel down-after-milliseconds <master-name> <milliseconds>:設置經過多少毫秒後認爲主節點邏輯下線,默認30000
  • sentinel parallel-syncs <master-name> <numreplicas>:用來限制新的主節點選舉出來之後,同時最多有多少節點可以執行復制,默認1,即順序複製。該值可以設置低一些,因爲RDB加載過程是阻塞的,如果同時複製的節點太多,一方面可能對新主節點造成較大壓力,另一方面可能造成一段時間無法響應讀請求
  • sentinel failover-timeout <master-name> <milliseconds>:故障轉移的超時時間,默認3分鐘,有四個作用,以T代指該配置的值:
    • 如果對某個節點failover失敗,需要過2*T時間才能再對同一節點發起failover
    • 假如從節點晉升(即slaveof no one)的時候出現錯誤,即新主節點也下線了,則最多嘗試T時間,然後認爲failover失敗
    • 假如slave no one執行沒有失敗,但是在T時間內仍然無法通過info命令確認晉升結果,則認爲failover失敗
    • 假如其他從節點在重新進行全量複製時,持續時間超過T,則認爲failover失敗

4)通知配置:

  • sentinel notification-script  <master-name> <script-path>:指定告警腳本,包括客觀下線sdown和主觀下限odown事件,這裏的腳本是shell腳本,不是lua腳本,腳本最多運行60秒,超時則結束並重試(腳本返回值爲1),最多重試10次,如果腳本返回值大於等於2,則不會重試
  • sentinel client-reconfig-script <master-name> <script-path>:用於在failover後通知客戶端修改訪問地址,這裏的腳本是shell腳本,不是lua腳本,傳入的參數有:<master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>,其中state是”failover“,role要麼是”leader“,要麼是”observer“
  • sentinel deny-scripts-reconfig:是否允許在運行時修改上面兩個腳本,默認yes,代表不允許修改

以上配置都可以在Redis Shell中用sentinel set命令修改,並且會立即刷新到配置文件中。

3.3 搭建

這裏有一篇偏向生產環境的文章可供參考:https://blog.51cto.com/dengaosky/2091877,這裏的以3個Redis節點、3個Sentinel節點爲例,Redis使用6379、6380、6381端口,主節點爲6379,哨兵使用26379,、26380、26381端口。在生產環境中,最好分散在不同物理機上部署3個以上的奇數個Sentinel節點。

首先啓動三個Redis節點:

root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/redis.conf
root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/redis.conf --port 6380 --replicaof localhost 6379
root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/redis.conf --port 6381 --replicaof localhost 6379

然後啓動三個哨兵節點:

root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/sentinel.conf --sentinel
339:X 28 Apr 2019 21:11:08.393 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
339:X 28 Apr 2019 21:11:08.393 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=339, just started
339:X 28 Apr 2019 21:11:08.395 # Configuration loaded
root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/sentinel-26380.conf --sentinel
343:X 28 Apr 2019 21:11:31.790 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
343:X 28 Apr 2019 21:11:31.791 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=343, just started
343:X 28 Apr 2019 21:11:31.791 # Configuration loaded
root@Yhc-Surface:~/redis-5.0.4# redis-server ~/redis-5.0.4/sentinel-26381.conf --sentinel
345:X 28 Apr 2019 21:11:35.856 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
345:X 28 Apr 2019 21:11:35.856 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=345, just started
345:X 28 Apr 2019 21:11:35.857 # Configuration loaded

然後驗證搭建成功,首先進入主節點(6379),使用role命令:

127.0.0.1:6379> role
1) "master"
2) (integer) 100921
3) 1) 1) "127.0.0.1"
      2) "6381"
      3) "100522"
   2) 1) "127.0.0.1"
      2) "6380"
      3) "100522"

現在還看不到哨兵節點的信息,不過不要緊,我們登入26379節點,執行sentinel masters和info sentinel命令:

127.0.0.1:26379> sentinel masters
1)  1) "name"
    2) "mymaster"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "6379"
    ... //下略

127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

可見不但主節點識別成功,連哨兵節點的數目也成功識別出來了。實際上,哨兵是以客戶端的形式註冊到主節點上的(如果使用info clients,可以看到clients:3的字樣),然後訂閱指定channel,新加入的sentinel節點向channel中發佈一條包含自己信息的消息,這樣其他哨兵就可以感知到新成員。但是刪除就會麻煩一些,除了需要關閉目標哨兵外,還需要連接剩下的每一個哨兵節點,執行 sentinel reset * 命令,官方要求每次執行至少間隔30秒。

如果我們此時關閉6379實例,可以看到哨兵日誌出現如下內容:

84:X 29 Apr 2019 13:48:22.961 # +sdown master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.047 # +odown master mymaster 127.0.0.1 6379 #quorum 2/2
84:X 29 Apr 2019 13:48:23.048 # +new-epoch 1
84:X 29 Apr 2019 13:48:23.048 # +try-failover master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.057 # +vote-for-leader 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.072 # ad9a9c0fb810571249f4711cbe13408ff06258d7 voted for 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.074 # fcdc259825dd36869af4f2a3168a925186fd20b0 voted for 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.115 # +elected-leader master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.116 # +failover-state-select-slave master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.175 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.176 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.236 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.650 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.650 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:23.733 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:24.195 # -odown master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:24.682 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:24.682 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:24.783 # +failover-end master mymaster 127.0.0.1 6379
84:X 29 Apr 2019 13:48:24.783 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
84:X 29 Apr 2019 13:48:24.785 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380

從而得到整個failover的流程:

首先,當哨兵節點檢測到主數據節點不可達時,會將其設爲sdown,即主觀下線,然後收集其他哨兵的意見

當有quorum個哨兵都認爲主節點不可達後,開始執行客觀下線,選舉產生新的主節點(本例中爲6380),並執行晉升

然後對剩下的6381節點進行配置修改,使之成爲6380的從節點,到此完成6379的客觀下線。

3.4 命令

  • sentinel masters:這個命令上面出現過,可以用來顯示所有被監控的主節點的信息
  • sentinel master master-name:顯示指定主節點的信息
  • sentinel slaves master-name:顯示指定主節點的從節點信息
  • sentinel sentinels master-name:顯示監控指定主節點的哨兵節點信息
  • sentinel get-master-addr-by-name master-name:顯示指定主節點的IP、端口信息

  • sentinel reset pattern:重置所有名稱符合匹配條件的主節點,清除其狀態,重置其從節點列表等

  • sentinel failover master-name:對指定主節點強制故障轉移

  • sentinel ckquorum master-name:檢查監控主節點的哨兵個數是否達到要求

  • sentinel flushconfig:將當前哨兵節點的配置寫入磁盤

  • sentinel remove master-name:當前哨兵節點不再監控指定主節點

  • sentinel monitor master-name host port quorum:令當前哨兵節點監控指定主節點

  • sentinel set master-name key value:修改指定主節點的配置,相當於在主節點上使用config set

  • sentinel is-master-down-by-addr host port epoch replication-Id:詢問其他哨兵節點,指定的主節點是否下線

    • 如果replication-Id爲“*”,表示哨兵節點交換對主節點下線的決定

    • 如果replication-Id爲當前哨兵節點的id,表示本節點希望稱爲Leader

    • 返回結果有三部分,1)下線狀態,1代表下線,0代表在線;2)Leader節點id,*代表結果的內容是主節點是否在線,如果是某個節點的id,則代表該節點是Leader;3)Leader節點版本。關於Leader節點,會在原理部分介紹。

3.5 哨兵實現原理

3.5.1 Pub/Sub

上面提到,哨兵機制的實質就是連接到主節點的發佈/訂閱客戶端:

127.0.0.1:6379> pubsub channels *
1) "__sentinel__:hello"

如果訂閱該頻道,就能看到,每隔2秒,各哨兵節點之間就會交流對主節點的判斷,哨兵節點間的自動發現機制也是通過該頻道實現的。消息格式如下:

哨兵節點IP,哨兵節點端口,哨兵節點ID,哨兵節點紀元,主節點名,主節點IP,主節點端口,主節點紀元

所謂紀元,就是當前正在進行的選舉次數,每次執行選舉,無論成敗都會將紀元加1。

而當我們在哨兵節點處訂閱時,會發現更多頻道:

127.0.0.1:26380> psubscribe *
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "*"
3) (integer) 1
1) "pmessage"
2) "*"
3) "+new-epoch"
4) "11"
1) "pmessage"
2) "*"
3) "+config-update-from"
4) "sentinel 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 127.0.0.1 26379 @ mymaster 127.0.0.1 6379"
1) "pmessage"
2) "*"
3) "+switch-master"
4) "mymaster 127.0.0.1 6379 127.0.0.1 6381"
...

當哨兵遇到某些事件時,就會向對應頻道發送消息,有如下頻道:

  • +reset-master:表示主節點已重置
  • +slave:檢測到一個新從節點
  • +failover-state-reconf-slaves:故障轉移狀態已更改爲reconf-slaves
  • +failover-detected:檢測到故障轉移
  • +slave-reconf-sent:從節點收到了Leader哨兵發來的slaveof命令
  • +slave-reconf-inprog:從節點已經更換了主節點
  • +slave-reconf-done:從節點開始與新的主節點同步
  • -dup-sentinel:主服務器上的哨兵節點由於重複被刪除
  • +sentinel:檢測到新哨兵節點加入
  • +sdown:認定主節點主觀下線
  • -sdown:撤銷主觀下線
  • +odown:認定主節點客觀下線
  • -odown:撤銷客觀下線
  • +new-epoch:進入新紀元
  • +try-failover:開始新的故障轉移
  • +elected-leader:節點被選舉爲Leader,可以發起故障轉移
  • +failover-state-select-slave:故障轉移狀態已更改爲select-slave,開始尋找合適的從節點
  • no-good-slave:沒有可以晉升的從節點
  • selected-slave:已經找到可以晉升的從節點
  • failover-state-send-slaveof-noone:正在執行晉升
  • failover-end-for-timeout:由於超時導致故障轉移失敗,已經在執行slaveof的從節點最終還是會指向新的主節點
  • failover-end :故障轉移成功結束
  • switch-master :顯示新舊主節點的IP、端口
  • +tilt :進入Tilt模式
  • -tilt :推出Tilt模式

3.5.2 Leader與Epoch

在哨兵的日誌中,可以看到如下內容:

84:X 29 Apr 2019 13:48:23.057 # +vote-for-leader 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.072 # ad9a9c0fb810571249f4711cbe13408ff06258d7 voted for 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.074 # fcdc259825dd36869af4f2a3168a925186fd20b0 voted for 7e7d8782d2e284bbaaad3ab6915e96bc71e7cf23 1
84:X 29 Apr 2019 13:48:23.115 # +elected-leader master mymaster 127.0.0.1 6379

這就是Leader選舉的過程。Leader選舉通過Raft算法進行,我們先考慮一下沒有Leader時會有怎樣的問題,假設有3個從節點和3個哨兵,在極端情況下,每次選舉都正好有一個哨兵支持一個從節點晉升。此時,永遠無法完成故障轉移。

如果採用Leader機制,只需要選舉出Leader,然後由Leader指定一個從節點晉升即可。仍然考慮上面的例子,3個哨兵節點進行選舉,雖然也有可能出現每個節點僅支持自己成爲Leader的可能,但是這種情況基本不會發生,原因是選舉發生在主觀下線後,即誰先發起下線,一般就由誰擔任Leader。

而在Leader選舉之前,我們可以看到更新Epoch的操作,epoch(紀元)的作用是唯一標記故障轉移版本。現在考慮一個極端情況,假如Leader節點自己也遇到故障下線了,那麼此時剩下的哨兵肯定會重新選出Leader,重新發起一次故障轉移,如果沒有紀元,當原先的Leader恢復上線後,就會繼續工作,挑選一個節點進行晉升,從而導致集羣出現兩個主節點,引入紀元後,由於每次故障轉移都必須更新紀元,如果出現上述情形,當舊Leader上線併發起晉升,會發現自己的紀元版本比較低,從而放棄晉升。

紀元的另一個應用是配置傳播,當故障轉移完成後,需要將配置發佈到__sentinel__:hello頻道,各個哨兵節點收到消息後,會檢查新配置和本地配置的紀元值,如果新配置更大,則覆蓋掉本地的配置。

3.5.3 監控和從節點選取

每隔10秒,哨兵節點就會對主從節點發送INFO命令,獲取拓撲結構;每隔1秒,哨兵節點就會檢測主節點、從節點和其他哨兵節點是否存活,並對失效節點執行下線操作。

新主節點的選擇,需要遵循以下標準:

  • 過濾掉不夠健康的節點:例如主觀下線節點、5秒內沒有回覆過PING的節點、與主節點失聯超過10倍故障轉移時間的節點
  • 優先選擇slave-priority值更小的節點(優先級更高)
  • 選擇複製偏移量最大的節點
  • 選擇replication-Id最小的節點

3.6 Jedis對哨兵的支持

雖然Redis官方不建議讓外界訪問到哨兵,但是還是會有相關需求,Jedis也提供了對哨兵的支持:JedisSentinelPool。

以下是使用示例:

public static void main(String[] args) {
    Set<String> sentinels=new HashSet<>();
    sentinels.add("localhost:26379");
    sentinels.add("localhost:26380");
    sentinels.add("localhost:26381");
    JedisSentinelPool pool=new JedisSentinelPool("mymaster",sentinels);
    Jedis sentinel=pool.getResource();
    System.out.println(sentinel.sentinelMasters());
}

執行後卻報出了錯誤,無法執行sentinel masters命令:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: ERR unknown command `SENTINEL`, with args beginning with: `masters`, 
	at redis.clients.jedis.Protocol.processError(Protocol.java:132)
	at redis.clients.jedis.Protocol.process(Protocol.java:166)
	at redis.clients.jedis.Protocol.read(Protocol.java:220)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:309)
	at redis.clients.jedis.Connection.getRawObjectMultiBulkReply(Connection.java:276)
	at redis.clients.jedis.Connection.getObjectMultiBulkReply(Connection.java:280)
	at redis.clients.jedis.Jedis.sentinelMasters(Jedis.java:2985)
	at SentinelTest.main(SentinelTest.java:15)

看一下源碼來研究這個問題,跟蹤JedisSentinelPool構造方法可以發現,它調用了initSentinel方法:

    private HostAndPort initSentinels(Set<String> sentinels, String masterName) {
        ...
        HostAndPort hap;
        while(var5.hasNext()) {
            sentinel = (String)var5.next();
            hap = HostAndPort.parseString(sentinel);
            this.log.debug("Connecting to Sentinel {}", hap);
            Jedis jedis = null;

            try {
                jedis = new Jedis(hap);
                List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
                sentinelAvailable = true;
                if (masterAddr != null && masterAddr.size() == 2) {
                    master = this.toHostAndPort(masterAddr);
                    break;
                }
            } catch (JedisException var13) {
                
            } finally {
                if (jedis != null) {
                    jedis.close();
                }

            }
        }
        ...
        var5 = sentinels.iterator();
        while(var5.hasNext()) {
            sentinel = (String)var5.next();
            hap = HostAndPort.parseString(sentinel);
            JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
            masterListener.setDaemon(true);
            this.masterListeners.add(masterListener);
            masterListener.start();
        }
        return master;
}

這裏做了兩件事:1)尋找主節點,讓連接池實際連接到主節點上(這一步在initPool方法完成);2)添加MasterListener,以處理故障轉移相關事件

由於連接池實際是指向主節點的,所以不能執行sentinel XX命令。而MasterListener實際就是訂閱了+switch-master頻道,以獲得新主節點的信息,然後再調用initPool重新初始化連接池,連接到新的主節點上。

this.j.subscribe(new JedisPubSub() {
    public void onMessage(String channel, String message) {
        JedisSentinelPool.this.log.debug("Sentinel {}:{} published: {}.", new Object[]{MasterListener.this.host, MasterListener.this.port, message});
        String[] switchMasterMsg = message.split(" ");
        if (switchMasterMsg.length > 3) {
            if (MasterListener.this.masterName.equals(switchMasterMsg[0])) {
                JedisSentinelPool.this.initPool(JedisSentinelPool.this.toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
            } else {
                JedisSentinelPool.this.log.debug("Ignoring message on +switch-master for master name {}, our master name is {}", switchMasterMsg[0], MasterListener.this.masterName);
            }
        } else {
            JedisSentinelPool.this.log.error("Invalid message received on Sentinel {}:{} on channel +switch-master: {}", new Object[]{MasterListener.this.host, MasterListener.this.port, message});
        }
    }
}, new String[]{"+switch-master"});

也就是說,我們如果通過JedisSentinelPool獲取Jedis實例,可以保證一直獲取到集羣的主節點,而不必擔心節點下線等問題。

如果要連接到哨兵節點,還是需要使用JedisPool。

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