走進Redis:哨兵集群

為什麼需要哨兵

在 Redis 的主從庫模式中,如果從庫發生了故障,用戶的操作是可以繼續進行的,因為寫操作是只在主庫中進行的。那麼,如果主庫發生了故障,用戶的操作將會收到影響。這時候可能會需要選擇一個從庫在作為主庫繼續服務用戶的操作。Redis 提供的哨兵機制就是解決主從庫模式的 Redis 服務可用性問題的。

故障轉移的基本流程

哨兵發現主庫下線並選出新主庫的流程稱為故障轉移,這個過程需要解決三個問題:

  1. 主庫真的掛了嗎?(監控)
  2. 該選擇那個從庫作為主庫?(選主)
  3. 怎麼把新主庫的相關資訊通知給從庫和客戶端呢?(通知)

監控

哨兵在運行過程中周期性的給所有主從庫發送 PING 命令,以此檢測它們是否正常運行。如果某個實例對 PING 命令的沒有在down-after-milliseconds 應答,那麼,哨兵會把它標記為「主觀下線」。

哨兵不會對主觀下線的從庫做額外的處理,如果是主庫主觀下線,那麼哨兵會進行後續的選主和通知操作,這些操作會有額外的計算和通訊開銷。為了減少誤判,哨兵通常會採用集群部署。當一個哨兵將 master 標記為主觀下線後,會和其它哨兵實例通過命令 SENTINEL is-master-down-by-addr 交流,如果有大於 quorum 個哨兵確認 master 已下線,則該 master 會被標記為」客觀下線」;否則,會重新將 master 標記為上線狀態。

💡 quorum 通過 sentinel.conf 配置得到,和哨兵集群中實例的數量有關。例如,若共有 3 個實例,則該值可設置為 2 ,最好將該值設置為 $N/2+1$。該值越小,判斷 master 客觀下線的條件越寬鬆;反之則判斷 master 客觀下線的條件越嚴格。通常哨兵集群中實例的數量為奇數,避免出現應答下線和未下線數量相同的情況。

//raw.githubusercontent.com/LooJee/medias/master/images202208091027768.png

//raw.githubusercontent.com/LooJee/medias/master/images202208091036894.png

選主

選新主庫的過程大致可以分為以下幾個步驟:

哨兵 Leader 選舉

哨兵 Leader 是本次故障轉移的執行者,每個哨兵都有機會成為 Leader。具體流程如下:

  1. 當哨兵將 master 節點標記為客觀下線後,會將當前紀元(epoch,該值類似 raft 協議的 term 值)加一,表示開始一次新的 leader 選舉。
  2. 首先會給自己投一票,然後會再次發送 is-master-down-by-addr 給其它哨兵。這個命令的使用方式如下:sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>。這裡主要看 runid,確認主節點是否客觀下線時,該值為 *;開始參選 Leader 時,傳的參數為本節點的 runid。
  3. 收到命令的哨兵,如果沒有同意過其他節點的sentinel is-master-down-by-addr命令,將同意該請求,否則拒絕。並且同意過其它節點後,該節點也不會發起投票。
  4. 如果該哨兵發現自己的票數已經大於等於max(quorum, num(sentinels)/2+1),那麼它將成為領導者。
  5. 如果此過程沒有選舉出領導者,將進入下一次選舉。選舉超時時間可以查看配置 failover-timeout

//raw.githubusercontent.com/LooJee/medias/master/images202208101819212.png

篩選

  1. 首先會判斷從庫當前的狀態,過濾掉當前主觀下線以及斷線的節點;
  2. 5 秒內沒有回復過哨兵節點 PING 請求的節點;
  3. 與主節點斷連次數超過 10 次的節點。是否斷連是根據配置項 down-after-milliseconds 來判斷的,主從節點 down-after-milliseconds 毫秒內都沒有連接上,則會被記為一次斷連。

打分

  1. 通過配置項 slave-priority 來給不同的從庫設置不同的優先順序。可以使用命令 info Replication 來查看該配置。如果有一個從庫的優先順序最高,那麼該從庫會直接選為新主庫;否則,將會進行第二輪打分。
  2. 與舊主庫同步程度最接近的從庫得分高。在主從同步中提到過,從庫會用 slave_repl_offset 來記錄當前從 master 的複製進度,該配置通過 info Replication 可以查看。哨兵會選出複製進度值最大的從庫作為新的主庫,如果多個從庫複製進度並列第一,那麼需要進行第三輪打分。
  3. ID 號最小的從庫得分高。最後,哨兵會查看從庫的 runId ,將 runId 最小的從庫作為新主庫。runId 通過命令 info Server 可以查看。

//raw.githubusercontent.com/LooJee/medias/master/images202208101427306.png

通知

當選出新主庫之後,哨兵有三個角色需要通知:

  1. 告訴新主庫它已經稱為了新主庫。哨兵會向目標庫發送命令 slaveof no one ,收到命令後,目標庫會將自己的角色( role )提升為master
  2. 告訴其它從庫新主庫的地址。哨兵會向其它從庫發送 slaveof host port 來告訴從庫新主庫的地址,從庫會執行 replicaof 命令開始從新主庫同步。
  3. 通知客戶端主庫變化。當新主庫切換完成後,哨兵會在頻道 +switch-master 中發送消息,通知外部客戶端新主庫地址。

哨兵集群

上面討論主從切換的時候有提到哨兵集群來減少主庫客觀下線誤判的可能性。哨兵監控一個 master 節點是通過下面這個命令來完成的:

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

這個命令中並沒有指定其它哨兵的地址資訊,那麼哨兵是如何組成一個集群的呢?

基於 pub/sub 機制的哨兵集群

哨兵成功和主節點建立連接之後,會在主節點上創建一個名為 「sentinel:hello」 的頻道,然後會將自己的地址埠等資訊發布到該頻道,其它哨兵就可以從這個頻道獲取監控同一個主節點的哨兵資訊,互相建立網路連接,形成一個集群。

💡 我們可以用 redis-cli 連接上主節點,然後用命令psubscribe * 監聽主節點上的所有頻道,會看到哨兵不斷在 __sentienl__:hello 頻道發送自己的資訊。

//raw.githubusercontent.com/LooJee/medias/master/images202208111100489.png

獲取從庫資訊

redis 主庫會保存從庫的資訊,哨兵會向主庫發送 INFO 命令來獲取從庫列表。哨兵就可以根據從庫列表的地址資訊和每個從庫建立連接,然後監控從庫的狀態。

//raw.githubusercontent.com/LooJee/medias/master/images202208111107427.png

哨兵獲取從庫資訊主要是INFO 命令查看主庫的 replication 資訊,典型的一個 replication 資訊如下所示:

127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.26.0.3,port=6379,state=online,offset=217874,lag=0
slave1:ip=172.26.0.2,port=6379,state=online,offset=217874,lag=0
master_failover_state:no-failover
master_replid:51567b12a9c1ba1d82846a8fb8fd84404b60eaf8
master_replid2:0baa276d98298739b2e0755640fd5b50d2828b26
master_repl_offset:217874
second_repl_offset:17993
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:429
repl_backlog_histlen:217446

從資訊里可以看到主庫的 role 為 master ,已連接的從庫數量(connected_slaves)為 2,然後 slave0 和 slave1 就是從庫的資訊。

哨兵會定期給主庫和從庫發送 PING 命令檢測網路狀態,如果超時未應答就會標記為主觀下線,如果主觀下線的是主庫,就會開啟故障轉移流程。

客戶端訪問 redis-server

客戶端如何訪問哨兵集群監控的 redis-server 呢?主要有以下幾個步驟:

  1. 連接哨兵集群。客戶端初始化時,需要提供哨兵的地址,客戶端會連接到第一個可用的哨兵地址。
  2. 連接主庫。客戶端給哨兵發送 SENTINEL get-master-addr-by-name <master-name> 獲取主庫地址,然後和主庫建立連接。為了確認連接的確實是主庫,客戶端還需要發送 info replication 命令來查看 role 是否是 master,如果不是的話就要從第一步重新開始。
  3. 連接從庫。客戶端給哨兵發送 SENTINEL replicas <master-name>來獲取從庫資訊。相應的,客戶端也可以通過 info 命令來確認目標的角色。
  4. 監聽主庫遷移。哨兵通過 pub/sub 來通知客戶端集群中的事件。哨兵提供的訂閱頻道很多,具體可以查看//redis.io/docs/manual/sentinel/#pubsub-messages 。客戶端使用命令 psubscribe * 來訂閱所有的頻道,來了解主從切換的進度。當頻道 switch-master 有新消息時,表示新主庫已經切換完成。

//raw.githubusercontent.com/LooJee/medias/master/images202208111154890.png

實驗

可以使用以下 docker-compose 來簡單模擬故障遷移的實驗:

version: "3"
services:
  redis-master:
    image: redis:7
    ports:
      - "16379:6379"
    container_name: "redis-master"
    command: redis-server
    networks:
      - sentinel-network
  redis-slave-1:
    image: redis:7
    ports:
      - "6380:6379"
    container_name: "redis-slave-1"
    command: redis-server --replicaof redis-master 6379
    depends_on:
      - redis-master
    networks:
      - sentinel-network
  redis-slave-2:
    image: redis:7
    ports:
      - "6381:6379"
    container_name: "redis-slave-2"
    command: redis-server --replicaof redis-master 6379
    depends_on:
      - redis-master
    networks:
      - sentinel-network
  redis-sentinel:
    image: bitnami/redis-sentinel:latest
    environment:
      - REDIS_MASTER_HOST=redis-master
      - REDIS_SENTINEL_QUORUM=2
      - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000
    depends_on:
      - redis-master
      - redis-slave-1
    ports:
      - '26379-26381:26379'
    networks:
      - sentinel-network
networks:
  sentinel-network:

使用命令 docker-compose up --scale redis-sentinel=3 來啟動 docker-compose。啟動之後會打出如下的日誌:

redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 * Sentinel new configuration saved on disk
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 # Sentinel ID is 787487fa6116b997caa0a44b011793b58e265a54
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 # +monitor master mymaster 172.27.0.2 6379 quorum 2
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.827 * +slave slave 172.27.0.3:6379 172.27.0.3 6379 @ mymaster 172.27.0.2 6379
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.840 * Sentinel new configuration saved on disk
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.840 * +slave slave 172.27.0.4:6379 172.27.0.4 6379 @ mymaster 172.27.0.2 6379

日誌表示這樣一個流程:哨兵初始化完成 ⇒ 監控主庫 ⇒ 監控從庫。

此時,如果連接上主庫,然後訂閱 __sentinel__:hello 頻道,可以看到如下的消息:

//raw.githubusercontent.com/LooJee/medias/master/images202208111349300.png

集群內的哨兵都在往 __sentinel__:hello 發送自己的服務資訊。

可以使用命令 docker stop redis-master 來模擬主庫下線的情況,在執行這個命令之前可以先連到哨兵節點 redis-cli -p 26379 ,然後用命令 psubscribe * 來訂閱哨兵的頻道,這樣模擬主庫下線時就能看到哨兵通過頻道通知客戶端的消息了:

//raw.githubusercontent.com/LooJee/medias/master/images202208111358072.png

結語

Redis 哨兵的相關原理暫時告一段落,歡迎大家交流。

Tags: