天天看點

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

手把手帶你搭建單機版高可用分布式Redis叢集

  • 前言
  • Redis叢集服務
    • 主從複制
      • 配置一主兩從master-slave叢集
      • 主從複制原理分析
        • 建立連接配接
        • 同步
        • 指令傳播
        • 部分重同步
          • 同步偏移量
          • 複制積壓緩沖區
      • 主從服務的不足之處
    • 哨兵Sentinel機制
      • Sentinel原理分析
        • 主觀下線和客觀下線
        • Leader選舉
          • Raft選舉算法
          • Sentinel選舉Leader
        • 故障轉移
          • 如何選舉新的master節點
      • 配置Sentinel叢集
      • Sentinel機制日志解析
      • Sentinel機制的使用
        • Jedis使用Sentinel機制
        • SpringBoot使用Sentinel機制
      • Sentinel機制的不足之處
    • Redis分布式叢集方案
      • 用戶端實作分片
        • 用戶端分片的缺陷
      • 中間代理服務實作分片
      • Redis Cluster方案
        • 資料分片
          • 哈希後取模
          • 一緻性哈希
        • 槽(slot)
          • 如何讓相關業務資料強制落在同一個槽
        • 用戶端的重定向
        • 重新分片
          • ASK錯誤
          • ASK錯誤和MOVED錯誤
        • Redis Group
          • 故障檢測
          • 故障轉移
          • 選舉新的master節點
        • 為什麼槽定義為16384個
      • 手動配置一個Redis Cluster叢集
        • 為什麼至少需要3個maser節點
        • 手把手搭建一個3主3從Redi叢集
        • 搭建叢集常見錯誤
      • Redis Cluster叢集常用指令
      • 用戶端如何使用Redis Cluster叢集
      • Redis Cluster的不足
  • 總結

前言

Redis作為一款優秀的Nosql資料庫,使用非常廣泛,但是在有些場景下,一台Redis伺服器是不能滿足要求的,我們可能需要多台Redis伺服器來一起工作,而且如果僅僅使用一台Redis伺服器,那麼假如這一台伺服器挂了,也會給業務帶來很大的影響,嚴重的可能會導緻整個系統不可用,是以一個高可用的分布式Redis叢集是非常必要的。

Redis叢集服務

Redis當中的叢集方案實作方式可以分為三大類:主從複制叢集,基于哨兵機制實作的高可用叢集和Redis分布式Cluster方案,下面就讓我們分别來進行介紹

主從複制

主動複制,即:master-slave方案,是一個非常常見的設計模型。其中主庫用來讀寫,并且将資料同步給從庫,一旦主庫挂了,那麼從庫就可以更新為主庫。

配置一主兩從master-slave叢集

配置檔案中配置

replicaof no one

則表示目前Redis伺服器為主伺服器,然後從伺服器使用配置

replicaof host port

這樣就成為了master的從庫。配置成功之後,連接配接上主從庫,可以執行指令

replication info

指令進行檢視資訊。

  • 主庫執行

    info replication

    指令顯示資訊如下(role表示目前是一個master庫,下面的slaveX展示的就是從庫的資訊):
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
  • 從庫執行

    info replication

    指令顯示資訊如下(role表示目前是一個slave庫,下面的master_XX展示的就是主庫的相關資訊):
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
    配置主從的指令除了可以在配置檔案中配置,還可以直接在redis伺服器上執行,或者也可以在啟動的時候執行

    ./redis-server --slaveof ip port

    來指定主從伺服器。

搭建好主從之後,主伺服器上資料就會被同步到從伺服器,注意,從伺服器預設是隻讀的,可以通過配置檔案

replica-read-only no

來修改。

題外話:上面的

replicaof

指令可以替換為

slaveof

指令,但是建議還是使用

replicaof

指令,因為

slaveof

指令在國外被了解奴隸制度,是以當時因為這個指令Redis作者發起過一個投票,半數以上的人支援改名,是以後面Redis就采用了

replicaof

指令來替換

slaveof

指令。

主從複制原理分析

master-slave叢集的關鍵在于資料的同步,而在資料同步之前必須先建立連接配接。

建立連接配接

建立連接配接主要分為以下幾步:

  • 1、執行slaveof指令時候,從伺服器會在本地将主伺服器的一些資訊(如:IP和端口等資訊)儲存在

    redisServer

    内。
  • 2、建立和主伺服器的連接配接,建立連接配接之後,從伺服器就相當于主伺服器的一個用戶端。
  • 3、從伺服器向主服務發送

    ping

    指令,确認連接配接是否可用,如果Redis伺服器需要授權,這一步還會進行授權認證。
  • 4、如果從伺服器收到主伺服器的

    pong

    回複之後,表示目前連接配接可用,此時從伺服器會将自己的伺服器的端口号發送給主伺服器,主伺服器收到之後将其記錄在

    redisClient

    内。
  • 5、如果從伺服器沒收到主伺服器傳回

    pong

    ,則會發起重連。

PS:建立完連接配接之後,主從伺服器會定時(間隔1s)向對方發送

replconf of

指令來檢測對方是否正常。上圖中在主伺服器檢視從伺服器資訊中有一個

lag

屬性記錄的就是上一次發送心跳包時間。

同步

master和slave服務之間連接配接建立之後,就會開始進行資料同步,而首次同步一般用于首次建立主從連接配接的時候,這時候因為是初次建立master-slave關系,是以需要進行資料的全量同步。

首次資料全量同步主要分為以下步驟:

  • 1、從伺服器向主伺服器發送同步資料指令。
  • 2、主伺服器接收到同步資料指令之後,則會執行

    bgsave

    指令,在背景生成一個RDB檔案,并将其發送給從伺服器,此時如果有新的指令過來,主伺服器會将其記錄在緩沖區内。
  • 3、從伺服器收到主伺服器發送過來的RDB檔案之後,會首先清除自己的資料,然後載入RDB檔案來生成資料。
  • 4、當從伺服器執行完RDB檔案之後,主伺服器會将緩沖區的指令發送給從伺服器,從伺服器再依次執行。

指令傳播

執行完首次全量同步之後,這時候主伺服器會将自己接收到的改變了資料庫狀态的指令發送給從伺服器,進而使得主從伺服器資料始終保持一緻。

既然是指令傳播,那麼就不可避免的會造成資料延遲,Redis當中提供了一個參數來進行優化。

當參數設定為yes時,此時會将資料包進行合并發送,也就是降低了發送頻率(發送頻率與Linux核心配置有關);當參數設定為no時,則主伺服器每執行一個能改變資料庫狀态的指令就會立刻實時同步給從伺服器。

部分重同步

上面就是在伺服器正常情況下的同步措施,同步+指令傳播可以使得正常情況下主從伺服器資料保持一緻。然而如果說從伺服器因為停電等其他因素導緻其和主伺服器之間的連接配接中途斷開,那麼當連接配接再次恢複正常之後,如果還是重新全量同步則效率會非常低,也顯得沒有必要,是以Redis就需要支援部分重同步。

實作部分重同步最關鍵的地方就是需要記錄原先同步的偏移量,隻有這樣才能在連接配接恢複正常之後繼續實作指令傳播,而無需傳輸整個RDB檔案。

同步偏移量

主從伺服器各自都會維護一個資料複制的偏移量,這個偏移量表示的是發送指令的位元組數。

下圖就是一個剛建立連接配接的主從伺服器,預設偏移量offset都是0:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

當主伺服器向從伺服器發送100位元組之後,主從伺服器的偏移量就會變成100。

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

那麼如果master再次向slave1和slave2傳輸了一個200位元組的指令,slave1接收到了,而slave2沒有接收到,那麼就會出現以下情況:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

這時候當slave2再次和master恢複連接配接之後,此時slave2伺服器會想主伺服器發送同步指令,同步指令會帶上偏移量,這時候主伺服器收到了,發現slave2發送過來額偏移量是100,而自己已經到300了,那麼主伺服器就會把101到300之間的指令再次進行發送給slave2,進而達到了部分重同步的目的。

複制積壓緩沖區

上面的部分重同步貌似看起來能解決問題,但是這又會帶來另一個問題,那就是當主伺服器将指令發送出去之後,為了實作部分重同步還需要将指令儲存起來,否則當從伺服器的偏移量低于主伺服器時,主伺服器也無法将指令重傳播。

那麼問題就來了,這個指令要儲存多久呢?如果一直儲存下去就會占據大量的空間,為了解決這個問題,master伺服器維護了一個固定長度的FIFO隊列,即複制積壓緩沖區。

當進行指令傳播的過程中,master伺服器不僅會将指令傳播給所有的slave伺服器,同時還會将指令寫入複制積壓緩沖區。複制積壓緩沖區預設大小為1MB。

下面就是一個完整的部分重同步流程圖:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

也就是說,當master伺服器記錄的偏移量+1已經不存在與複制積壓緩沖區了,就會執行一次全量同步,即發送RDB檔案給從伺服器。

主從服務的不足之處

主從伺服器通過讀寫分離實作了資料的可靠性,但是其并未實作系統的高可用性。其主要存在以下兩個問題:

  • 1、首次同步或者部分重同步時需要執行全量同步時發送的RDB檔案如果過大,則會非常耗時。
  • 2、假如master伺服器挂了,那麼系統并不能手動切換master伺服器,仍然需要人為進行切換。

哨兵Sentinel機制

Redis的Sentinel機制主要是為了實作Redis伺服器的高可用性而設計的一種解決方案。這種方案也是為了彌補主從複制模式的不足,Sentinel機制實作了主從服務的自動切換。

Sentinel原理分析

Sentinel其本身也是一個特殊的Redis服務,在Redis的安裝包内,除了redis.conf檔案,還有一個sentinel.conf檔案,這個就是啟動sentinel服務的配置檔案,啟動指令則通過

redis-sentinel

來執行(如:

./redis-sentinel ../sentinel.conf

)或者也可以通過

redis-server

指令指定參數

sentinel

來啟動(如:

./redis-server ../sentinel.conf --sentinel

)。

Sentinel主要用來監控Redis叢集中的所有節點,當Sentinel服務發現master不可用時,可以實作master服務的自動切換。但是如果Sentinel服務自己挂了怎麼辦?是以為了實作高可用,Sentinel服務本身也是一個叢集,和Redis的master-slave模式不同的是,Sentinel叢集之間在正常情況下沒有主從關系,互相之間是平等的,隻有在需要執行故障轉移時才需要進行Leader選舉。

下圖就是一個3個Sentinel服務叢集和1主2從的Redis叢集示意圖:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

Sentinel叢集之間的服務會互相監控,然後每個Sentinel服務都會監控所有的master-slave節點,一旦發現master節點不可用,則Sentinel中通過選舉産生的Leader節點會執行故障轉移,切換master節點。

主觀下線和客觀下線

Sentinel服務預設以每秒1次的頻率向Redis服務節點發送

ping

指令(Sentinel服務之間也會發送ping指令進行檢測)。如果在指定時間内(可以由參數

down-after-milliseconds

進行控制,預設30s)沒有收到有效回複,Sentinel會将該伺服器标記為下線,即主觀下線。

down-after-milliseconds master-name milliseconds
           

當某一個Sentinel把master服務标記為主觀下線之後,會去詢問其他Sentinel節點,确認這個master是否真的下線,當達到指定數量的Sentinel服務都認為master伺服器已經主觀下線,這時候Sentinel就會将master服務标記為客觀下線,并執行故障轉移操作。

多少個Sentinel服務認定master節點主觀下線才會正式将master服務标記為客觀下線,由以下參數控制:

其中的

quorum

選項就是決定了這個數量。

需要注意的是,每個Sentinel服務的判斷主觀下線和客觀下線的配置可能不一樣,是以當Sentinel1判定master已經主觀下線或者客觀下線時,其他Sentinel服務并不一定會這麼認為,但是隻要有一個Sentinel判定master已經客觀下線,其就會執行故障轉移,但是故障轉移并不一定是由判斷為客觀下線的Sentinel服務來執行,在執行故障轉移的之前,Sentinel服務之間必須進行Leader選舉。

Leader選舉

當某一個或者多個Sentinel服務判定master服務已經下線,其會發起Leader選舉,選舉出Leader之後,由Leader節點執行故障轉移。

Raft選舉算法

Sentinel服務的Leader選舉是通過Raft算法來實作的。Raft是一個共識算法(consensus algorithm),其核心思想主要有兩點:

  • 1、先到先得
  • 2、少數服從多數

在Raft算法中,每個節點都維護了一個屬性

election timeout

,這是一個随機的時間,範圍在150ms~300ms之間,哪個節點先到達這個時間,哪個節點就可以發起選舉投票。

選舉步驟總要可以總結為以下步驟:

  • 1、發起選舉的服務首先會給自己投上一票。
  • 2、然後會向其他節點發送投票請求到其他節點,其他節點在收到請求後如果在

    election timeout

    範圍内還沒有投過票,那麼就會給發起選舉的節點投上一票,然後将

    election timeout

    重置。
  • 3、如果發起選舉的節點獲得的票數超過一半,那麼目前服務就會成為Leader節點,成為Leader節點之後就會維護一個

    heartbeat timeout

    時間屬性,在每一次到達

    heartbeat timeout

    時間時,Leader節點就會向其他Follow節點發起一個心跳檢測。
  • 4、Follow節點收到Leader節點的心跳包之後就會将

    election timeout

    清空,這樣可以防止Follow節點因為到達

    election timeout

    而發起選舉。
  • 5、假如Leader節點挂了,那麼Follow節點的

    election timeout

    将不會被清空,誰先到達,誰就會再次發起選舉。

PS:因為

election timeout

是一個随機值,雖然機率小,但也可能出現兩個節點同時發起投票選舉,這種情況就可能出現一次選舉并不能選出Leader(比如總共4個節點,每個節點都得了2票),此時就會等待下一次首先到達

election timeout

的節點再次發起投票選舉。

如果對Raft算法感興趣的,可以點選這裡觀看示範。

Sentinel選舉Leader

Sentinel中的選舉雖然是源于Raft算法,但是也做了以下改進:

  • 1、觸發選舉并不是由

    election timeout

    時間決定,而是由誰先判定master下線來決定的。
  • 2、Sentinel節點并沒有維護

    election timeout

    屬性,而是維護了一個配置紀元

    configuration epoch

    屬性,配置紀元是一個計數器(預設0),每一個Sentinel節點的同一個配置紀元隻能投票1次(先到先得),每次投票前會将配置紀元自增+1。
  • 3、選舉出Leader之後,Leader并不會向Follow節點發送心跳包告訴其他Follow節點自己成為了Leader。

故障轉移

當Sentinel選舉出Leader之後,Leader就會開始執行故障轉移,執行故障轉移主要分為一下三步:

  • 1、在已判定客觀下線的master伺服器的slave伺服器中找到一個合格的slave伺服器,向其發送

    replicaof no one

    指令,使其轉換為master服務。
  • 2、向其他從伺服器發送

    replicaof ip port

    指令,使其成為新master服務的slave節點。
  • 3、将已下線的master服務也設定為新的master服務的slave節點。
如何選舉新的master節點

新的master選舉條件主要需要參考4個因素:

  • 1、斷開連接配接時長:首先将所有于已下線master節點斷開連接配接時間超過

    down-after-milliseconds * 10

    的slave節點删除掉,確定salve節點的資料都是比較新的。
  • 2、slave節點的優先級排序:将所有的salve節點按照優先級進行排序,選出優先級最高的slave節點作為新的master節點(優先級由配置檔案參數

    replica-priority

    決定,預設100)。
  • 3、複制偏移量:如果有多個優先級相同的slave節點,則選出複制偏移量最大的的slave節點作為新的master節點。
  • 4、程序id:如果還是沒選出新的master節點,那麼會再次選擇程序id最小的slave節點作為新的master節點。

配置Sentinel叢集

配置sentinel需要修改sentinel.conf配置檔案,主要涉及到以下的一些配置屬性:

protected-mode no
port 26380
pidfile /xxx/redis-sentinel-26380.pid
logfile "/xxx/sentinel.log"
dir  "/xxx"
sentinel monitor mymaster xx.xxx.xxx.xxx 6370 2  //ip不要設定成127.0.0.1或者localhost
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
           
參數 說明
protected-mode 是否允許外部網絡通路
port sentinel端口
pidfile pid檔案
logfile 日志檔案
dir sentinel工作主目錄
sentinel monitor 監控的master服務名稱,ip和端口,其中ip建議使用真實ip取代本機ip
sentinel down-after-milliseconds master下線多少毫秒才會被Sentinel 判定為主觀下線
sentinel parallel-syncs 切換新的master-slave時,slave需要從新的master同步資料,這個數字表示允許多少個slave同時複制資料
sentinel failover-timeout 故障轉移逾時時間,這個時間有4個含義

注意上面的master服務名稱可以取值,但是在同一個Sentinel叢集中需要保持一緻,否則會無法正确監控。

故障轉移逾時時間用在了以下4個地方:

  • 1、同一個sentinel對同一個master兩次failover之間的間隔時間。
  • 2、從檢測到master伺服器故障開始,到被強制切換到新的master伺服器并開始複制資料為止的時間。
  • 3、取消已經在進行故障轉移(沒有産生任何配置更改的故障轉移)所需的時間。
  • 4、将所有salve配置新的master節點所需要的時間。超過這個時間如果仍然沒有完成還是會繼續進行,但是不一定會按照配置

    parallel-syncs

    所指定的并行數來進行。

Sentinel機制日志解析

下圖就是一個Sentine故障轉移的日志,最開始6371為被Sentinel監控的master伺服器:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
  • 1-7行表示Sentinel啟動完畢,正在監控6371端口的master伺服器。
  • 8-9行表示Sentinel發現了master服務有兩個slave節點,端口為6370和6372。
  • 10行表示Sentinel判定6371的master伺服器已經主觀下線。
  • 11行的1/1表示已經滿足客觀下線條件,是以Sentinel将master判定為客觀下線。
  • 12行表示将配置紀元自增1。
  • 13行表示要開始準備故障轉移,但是需要先進性Leader選舉。
  • 14行表示自己給自己投了1票,因為這裡隻配置了一個Sentinel,是以他成為了Leader,由它來執行故障轉移。
  • 15-17行表示經過一些列條件判定之後,6370被推選為新的master。
  • 18-21行表示Sentinel像6370服務發送

    slaveof no one

    指令,使其成為新的master節點,并等待這個過程完成之後修改其配置檔案。
  • 22-25行表示6372服務開始同步新的master節點資料。
  • 26行正式将master伺服器的位址切換到6370(因為這裡隻有1主2從,挂了1個還有2個,如果還有其他從伺服器,也需要依次來複制資料)。切換master位址之後用戶端就可以擷取到新的master服務位址。
  • 27行表示将6372伺服器添加到新的master伺服器的slave清單。
  • 28-29行表示将舊的master-6371伺服器設定為新的master伺服器的slave節點,這樣當6371再次啟動之後,會成為新的master伺服器的slave節點。

Sentinel機制的使用

Sentinel機制下,用戶端應該怎麼連接配接上master服務呢?因為master是可能改變的,是以在Sentinel機制下,用戶端需要連接配接上Sentinel服務,然後從Sentinel服務獲得master的位址進行連接配接。如下圖所示:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

Jedis使用Sentinel機制

下面就是一個使用Jedis用戶端使用Sentinel機制的例子。

1、引入

pom

依賴:

<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
   <scope>compile</scope>
</dependency>
           

2、建立一個測試類

TestJedisSentinel

進行測試:

package com.lonelyWolf.redis.sentinel;

import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;

public class TestJedisSentinel {
    private static JedisSentinelPool pool;

    private static JedisSentinelPool initJedisSentinelPool() {
        // master的名字是sentinel.conf配置檔案裡面的名稱
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("xx.xxx.xxx.xxx:26380");
//        sentinels.add("xx.xxx.xxx.xxx:26381");
//        sentinels.add("xx.xxx.xxx.xxx:26382");
        pool = new JedisSentinelPool(masterName, sentinels);
        return pool;
    }

    public static void main(String[] args) {
        JedisSentinelPool pool = initJedisSentinelPool();
        pool.getResource().set("name", "longly_wolf");
        System.out.println(pool.getResource().get("name"));
    }
}
           

連接配接時需要把所有Sentinel的連接配接建立并放入池内,然後用戶端會周遊其中所有服務,找到第一個可用的Sentinel服務,并擷取到master伺服器的位址,然後建立連接配接。

SpringBoot使用Sentinel機制

如果使用SpringBoot,則需加入以下兩個配置:

spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=ip:port,ip:port,ip:port
           

第一個參數sentinel.conf中自定義的名字,第二個參數需要配置所有的Sentinel伺服器ip和端口資訊。

Sentinel機制的不足之處

哨兵機制雖然實作了高可用性,但是仍然存在以下不足:

  • 1、主從切換的過程中會丢失資料,因為隻有一個 master,是以在切換過程中服務是不可用的。
  • 2、哨兵機制其本質還是master-slave叢集,即:1主N從。也就是master伺服器依然隻有1個,并沒有實作水準擴充。

Redis分布式叢集方案

要實作一個Redis水準擴充,需要實作分片來進行資料共享,可以有三種思路:

  • 1、在用戶端實作相關的邏輯,由用戶端實作分片決定路由到哪台伺服器。
  • 2、将分片處理的邏輯運作一個獨立的中間服務,用戶端連接配接到這個中間服務,然後由中間服務做請求的轉發。
  • 3、基于服務端實作。

用戶端實作分片

用戶端實作分片的話,Jedis提供了這個分片(Sharding)功能:

package com.lonelyWolf.redis.cluster.client;

import redis.clients.jedis.*;
import java.util.Arrays;
import java.util.List;

/**
 * Jedis用戶端實作叢集分片功能
 */
public class TestJedisSharding {
    public static void main(String[] args) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        //建立所有的連接配接服務分片
        JedisShardInfo shardInfo1 = new JedisShardInfo("xx.xxx.xxx.xxx", 6370);
        JedisShardInfo shardInfo2 = new JedisShardInfo("xx.xxx.xxx.xxx", 6371);

        //将所有連接配接加入連接配接池
        List<JedisShardInfo> shardInfoList = Arrays.asList(shardInfo1, shardInfo2);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, shardInfoList);

        //建立10個key值存入伺服器
        ShardedJedis jedis = jedisPool.getResource();//擷取連接配接
        for (int i=1;i<=10;i++){
            jedis.set("name" + i,"lonely_wolf" + i);
        }
        //取出key和其所在伺服器資訊
        for (int i=1;i<=10;i++){
            String key = "name" + i;
            Client client = jedis.getShard("name"+i).getClient();
            System.out.println("key值:" + jedis.get(key) + ",存在于伺服器的端口為:" + client.getPort());
        }
    }
}
           

輸出結果如下:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

然後我們分别去伺服器上看一下可以看到是完全比對的:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

用戶端分片的缺陷

用戶端實作分片的好處就是配置簡單,而且分片規則都是由用戶端來實作,但是卻存在以下缺點:

  • 1、用戶端需要支援分片,假如代碼移植到其他項目,而其他項目使用的Redis用戶端不支援分片,那就會造成叢集不可用。
  • 2、用戶端分片模式下不能很好地實作伺服器動态增減。

中間代理服務實作分片

中間代理服務實作分片其實就是将用戶端的分片邏輯進行抽取,然後單獨部署成一個服務,這樣做的好處是用戶端不需要處理分片邏輯,而且也不用關心伺服器的增減。

中間代理服務分片的方案有兩個使用比較廣泛,那就是(這兩種方案如果有興趣的可以點選對應的連結進去github擷取到對應的源碼進行了解):

  • 1、Twemproxy(Twitter開源)。
  • 2、Codis(豌豆莢開源)

使用中間服務的最大缺陷就是其本身是獨立的服務,而為了保證高可用,也需要對這個中間服務進行高可用的叢集配置,是以會導緻整個系統架構更加複雜。

Redis Cluster方案

Redis Cluster是Redis3.0版本正式推出的,實作了高可用的分布式叢集部署。

一個Cluster叢集由多個節點組成,Redis當中通過配置檔案

cluster-enabled

是否為

yes

來決定目前Redis服務是否開啟叢集模式,隻有開啟了叢集模式,才可以使用叢集相關的指令,與之相反的,如果沒有開啟叢集的Redis我們稱之為單機Redis。

資料分片

水準叢集的最關鍵一個問題就是資料應該如何配置設定,主流的有哈希後取模和一緻性哈希兩種資料分片方式。

哈希後取模

哈希後再取模這種方式比較簡單,就是将key值進行哈希運算之後再來除以節點數,即:hash(key)%N,然後根據餘數來決定落在哪個節點。這種方式資料分布會比較均勻,但是這種方式同時也是一種靜态的資料分片方式,一旦節點數發生變化,需要重新計算然後将資料進行重新分布。

一緻性哈希

一緻性哈希的原理就是把所有的哈希值組織成一個虛拟的哈希圓環,其中起點0和終點232-1位置重疊,如下圖所示:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

上圖中綠色表示目前存在的節點,黃色表示資料落點處,如果資料沒有落在節點上,則會落到順時針找到第一個節點,如上圖中黃色的資料最終會落到Node1節點上。

這種方式的好處就是如果新增或者删除了節點,那麼最多也隻會影響相關的1個節點的資料,而不需要将所有的資料全部進行重新分片。

下圖就是新增了一個Node5節點,那麼其影響的就是Node2到Node5之間的這部分資料需要由原來落在Node3節點的資料改到Node5節點,對其他節點資料沒有任何影響。

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

一緻性哈希有一個問題就是當節點數比較少的情況下會導緻資料分布不均勻。如下圖所示:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

這裡面隻有2個節點,但是有3條資料落在了Node2而Node1隻有1條資料,分布不均勻,為了解決這種問題,一緻性哈希又引入了虛拟節點。當節點過少的時候,在哈西環上建立一些虛拟節點,然後按照範圍指派給實際節點。

如下圖,藍色的就是虛拟節點,其中Node1-1的資料會分到Node1上,而Node2-1會分到Nde2上,這樣就可以使得資料較為平均:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

槽(slot)

Redis當中的資料分布并沒有采用以上兩種資料分片方式,而是另外引入了一個槽(slot)的概念。

Redis将整個資料庫劃分為16384個槽(slot),然後根據目前資料庫的節點數來劃分,每個節點負責一部分槽。如下圖所示:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

槽數組也是一個位圖數組(bit array),每個對象配置設定到哪個槽是根據CRC16算法得到的,需要注意的是:一個key落在哪一個槽是不會改變的,但是每個Redis Group(Redis主從服務)負責的槽可能會發生變化。

如何讓相關業務資料強制落在同一個槽

有時候我們同一個業務需要緩存一些資料,假如這些資料落在不同的槽歸屬于不同的伺服器負責,那麼在有些時候是會帶來不便的,比如

multi

開啟事務就不能跨節點,是以我們應該如何讓相關業務資料強制落在同一個槽呢?

Redis提供了一種

{}

機制,當我們的key裡面帶有

{}

的時候,那麼Redis隻會通過計算

{}

裡面的字元來進行哈希,是以我們可以通過這種方式來強制同一種業務資料落在同一個槽。

如下所示,我們可以将所有使用者相關資訊的key都帶上

{user_info}

,當然get的時候是需要帶上

{}

這個完整的key,帶上

{}

隻是影響slot的計算,其他并不影響:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

用戶端的重定向

Redis叢集中伺服器的資料分布用戶端是不可知的,是以假如在一個用戶端擷取key,然後這個key不存在目前伺服器那麼伺服器會根據背景自己存儲的資訊判斷出目前key所在槽歸屬于哪台伺服器負責,會傳回一個

MOVED

指令,帶上伺服器的ip和端口來告訴用戶端目前key所在的伺服器,用戶端接收到

MOVED

指令之後會進行重定向,然後擷取key值。

這種方式用戶端擷取一個key可能會需要連接配接2次伺服器。Jedis等用戶端會在本地維護一份 slot和node的映射關系,是以大部分時候不需要重定向,這種用戶端也稱之為smart jedis。但是這種特性并不是所有的用戶端都支援的。

重新分片

Redis叢集中,可以将已經指派給某個節點的任意數量的槽重新指派給另一個節點,且重新指派的槽所屬的鍵值對也會被移動到新的目标節點,利用這個特性就可以實作新增節點或者删除節點。

重新分片的操作是可以線上進行的,也就是說在重新分片的過程中,叢集不需要下線,并且參與重新分片的節點也可以繼續處理指令請求。

ASK錯誤

在重新分片的過程中會涉及到鍵值對的遷移,那麼可能會出現這樣一種情況:被遷移槽的一部分鍵值對儲存在目标節點裡面,而還有一部分鍵值對仍然在原節點還沒來得及前遷移。

而假如這時候用戶端來通路原節點時發現本來應該在這個節點的鍵值對已經被遷移到目标節點了,那麼這時候就會傳回一個

ASK

錯誤來引導用戶端到目标節點去通路,這時候用戶端并不能直接通路去通路目标節點,因為目标節點在所有鍵值對被遷移完成前是無法直接通過正常明星通路得到的。

目标節點在接收其他節點指派過來的槽所對應鍵值對時,會通過一個臨時數組屬性

importing_slots_from[16384]

來存儲,是以用戶端在接收到傳回的

ASK

錯誤之後,用戶端會先向目标節點發送一個

ASKING

指令,之後再發送原本想要執行的指令,這樣目标節點就知道目前用戶端通路的key是正在遷移過來的,知道去哪裡取這個資料。

需要注意的時,

ASKING

指令的作用是用戶端會打上一個标記,而這個标記是臨時性的,當伺服器執行過一個帶有

ASKING

标記的指令之後就會将該标記清除。

下圖就是一個特殊情況下的執行流程圖:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
ASK錯誤和MOVED錯誤
  • ASK錯誤可以認為是一種過渡時期的特殊錯誤,隻有在發生槽遷移的過程中,發現原本屬于node1管理的槽被指派給了node2,而資料又還沒有遷移完成的情況下,因為這是一種特殊場景,是以用戶端收到

    ASK

    錯誤之後不能直接連到目标節點執行指令(這時候直接連過去目标節點會傳回

    MOVED

    指令指向node1),用戶端收到

    ASK

    錯誤之後需要先向node2節點發送一個

    ASKING

    指令給自己打上标記才能真正發送用戶端想要執行的指令。
  • MOVED錯誤是在正常情況或者說槽已經完成重新分片的情況下傳回的錯誤,這種情況伺服器發現目前key所在槽不歸自己管,那麼就會直接

    MOVED

    錯誤和負責管理該槽的伺服器資訊,用戶端收到

    MOVED

    錯誤之後就會再次連接配接目标節點執行指令。

Redis Group

Redis叢集中的節點并不是隻有一台伺服器,而是一個由master-slave組成的叢集,稱之為Redis Group,那麼既然是主從就會涉及到主從的資料複制,複制的原理和我們前面講述的是一樣的,但是故障檢測以及故障轉移和前面講述的Sentinel模式下有點差別。

故障檢測

叢集中的各個節點都會定期向叢集中的其他節點發送

PING

消息來檢測對方是否線上,如果接收

PING

消息的節點沒有在規定時間内傳回

PONG

,那麼發送

PING

消息的節點就會将其标記為疑似下線(probable fail,PFAIL)。

PFAIL類似于Sentinel機制下的主觀下線,不同的是叢集中并不是發現PFAIL之後才會去詢問其他節點,而是定期通過消息的方式來交換狀态。

在Redis叢集中,各個節點會通過互相發送消息(

PING

)的方式來交換叢集中各個節點的狀态資訊,一旦某一個主節點A發現半數以上的主節點都将某一個主節點B标記為PFAIL,這時候主節點A就會将主節點B标記為已下線(FAIL),然後主節點A會向叢集中發一條主節點B已下線的

FAIL

消息的廣播,所有收到這條廣播消息的節點會立即将主節點B标記為已下線。

注意:如果廣播之後,發現這個master節點所在的Redis Group中,所有的slave節點也挂了,那麼這時候就會将叢集标記為下線狀态,整個叢集将會不可用。

故障轉移

當一個從節點發現自己的主節點已下線時,從節點将會開始進行故障轉移,執行故障轉移的從節點需要經過選舉,如何選舉在後面講,故障轉移的步驟主要分為以下三步:

  • 1、被選中的從節點會執行

    slave no one

    指令,使得自己成為新的主節點。
  • 2、新的主節點會将已下線的主節點負責的槽全部指派給自己。
  • 3、新的主節點會向叢集發一條

    PONG

    消息的廣播,收到這條消息的其他節點就會知道這個節點已經成為了新的master節點,并且将舊節點的槽進行了接管。
選舉新的master節點

一個從節點發現主節點下線後,會發起選舉,選舉步驟如下:

  • 1、該從節點會将叢集的配置紀元自增1(和Sentinel機制一樣,配置紀元預設值也是0)。
  • 2、該從節點會向叢集發一條

    CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST

    的消息廣播。
  • 3、其他節點收到廣播後,master節點會判斷合法性,如果一個主節點具有投票權(正在負責處理槽),那麼就會傳回一個

    CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK

    消息給他投1票(同樣的,一個配置紀元内,一個master節點最多隻能投票1次)。
  • 4、如果同時有多個從節點發起投票,那麼每個從節點都會統計自己所得票數,然後進行統計,隻有得到了大于參與投票的主節點數的一半的從節點,就會成為新的master節點。
  • 5、如果一個配置紀元内沒有一個從節點達到要求,那麼叢集會把配置紀元再次自增,并再次進行選舉,直到選出新的master。

為什麼槽定義為16384個

前面我們提到Redis叢集中,判斷一個key值落在哪個槽上是通過CRC16算法來計算的,CRC16算法産生的hash值有16bit,也就是可以産生216(即:65536)個值。

Redis中每秒都在發送

PING

消息,發送

PING

消息的節點會帶上自己負責處理的槽資訊,如果建立65536個槽(0-65535),那麼就需要65536大小的位圖(Bitmap)來存儲,也就是需要:65535/8/1024=8k的位圖數組空間,這對于頻繁發送的心跳包來說太大了,而如果使用16384那麼隻需要16384/8/1024=2kb的位圖數組空間,這是原因之一。

另一個原因是Redis叢集中的節點數官方建議不要超過1000個,那麼對于最大的1000個節點來說,16384個槽是比較合适的,因為16384/1000=16,也就是極端情況下每個節點負責16個slot,這是比較合适的,槽如果太小了(即

slot/N

不宜過大)會影響到位圖的壓縮。

下面截圖就是Redis作者的回複:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

手動配置一個Redis Cluster叢集

配置一個Redis Cluster至少需要3個master節點,是以為了高可用,每個master節點又至少需要配置一個slave節點,也就是最低配版的高可用Redis Cluster服務是需要3主3從。

為什麼至少需要3個maser節點

為什麼至少需要3個master節點的原因是如果一個master挂了,至少3個節點才能執行後面的故障轉移等操作。1個master就不用說了,挂了就沒了;如果是2個master節點就可能出現這種情況A發現B挂了,但是A自己隻有1個,也就是認為B挂了的主節點剛好等于所有主節點的一半,沒大于一半,是以他就沒辦法斷定節點B就是挂了,是以2個master也不行。

手把手搭建一個3主3從Redi叢集

  • 1、首先需要把配置檔案

    cluster-enabled

    參數改為

    yes

    ,這樣才能啟動叢集模式。
  • 2、還需要把配置檔案

    cluster-config-file

    修改一下,這個是節點檔案,由每個節點自己管理,但是我們需要配置好檔案名和路徑,主要的配置檔案如下所示:
port 6370 //端口号
daemonize yes  //是否背景運作
protected-mode no  //網絡是否允許對外通路 no表示允許
dir /usr/local/redis-6370/  //redis工作主目錄
cluster-enabled yes  //是否開啟叢集模式
cluster-config-file /usr/local/redis-6370/nodes-6370.conf //node檔案
cluster-node-timeout 5000 //叢集逾時時間
appendonly yes  //是否開啟aop持久化
pidfile /var/run/redis_6370.pid  //pid檔案
           
  • 3、完成好配置之後,依次啟動6個服務。
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
  • 4、登入任意一個節點,執行

    cluster info

    指令會發現目前叢集的狀态是下線狀态(fail)
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
  • 5、執行下面的建立叢集指令,注意:ip要使用真實的ip,而不要使用127或者localhost等本機ip。

執行之後會有如下提示,也就是系統自動幫我們配置設定好了槽而且将主從确定了:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

-6 輸入

yes

同意系統的配置設定方案,然後等待配置成功。

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結
  • 7、配置成功之後納入任意一個節點執行

    cluster info

    指令檢視叢集資訊
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

搭建叢集常見錯誤

搭建叢集過程中可能會出現以下錯誤:

  • 1、[ERR] Node 47.107.155.197:6370 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

這個錯誤有2個原因:一個就是redis當中有資料,這個執行

flushall

指令或者把rdb和aof兩個持久化檔案删除掉如果有的話,再重新開機即可;另一個原因就是可能初始化叢集失敗過1次,那麼這時候需要把當時配置好的nodes.conf檔案删掉,并重新開機Redis服務就可以解決。

  • 2、初始化叢集輸入yes之後提示Waiting for the cluster to join,但是卻遲遲等不到成功。

這個原因可能是防火牆引起的,除了正常的資料連接配接端口,如6370等,還有另一個端口需要用資料端口固定加上10000得到16370。是以在上面的案例中,需要確定以下12個端口都是通的(10000以下的是正常Redis工作接口):

這個10000的偏移量是固定的,加上10000偏移量後得到的端口是叢集内部用來執行故障檢測、配置更新、故障轉移授權等操作的,用戶端不能連接配接這個端口。

  • 3、[ERR] Not all 16384 slots are covered by nodes.
    【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

這個原因和上面第1種的錯誤原因類似,可能是建立叢集失敗過而沒有清空資料導緻第二次建立失敗,這時候清空資料并删除對應的node.conf檔案即可解決。

Redis Cluster叢集常用指令

  • cluster info:列印目前叢集的資訊。
  • cluster nodes:列印出叢集中node及其資訊。
  • cluster meet :将指定ip和port的node添加到目前執行指令節點所在的叢集中。
  • cluster forget <node_id>:從叢集中移除某一個節點。
  • cluster replicate <node_id>:将目前節點設定指定的節點的從節點。
  • cluster saveconfig:将目前節點的配置檔案儲存到硬碟裡面。
  • cluster keyslot :計算鍵key應該被放置在哪個槽上。
  • cluster countkeysinslot :傳回cao目前包含的鍵值對數量。
  • cluster getkeysinslot :傳回count個slot槽中的鍵。
  • cluster addslots [slot…]:将一個或多個槽( slot)指派給目前節點。
  • cluster delslots [slot…] : 移除一個或多個槽對目前節點的指派。
  • cluster flushslots : 移除目前節點的所有槽。
  • cluster setslot node <node_id>:将槽slot指派給指定的節點, 如果槽slot已經指派給另一個節點,則會先删除再指派。
  • cluster setslot migrating <node_id>:将本節點的槽slot遷移到指定的節點中。
  • cluster setslot importing <node_id> :從指定的節點中導入槽slot到目前節點。
  • cluster setslot stable:取消對槽slot的導入( import)或者遷移( migrate)。

用戶端如何使用Redis Cluster叢集

如果直接在shell上操作,那麼當發現一個槽不在目前節點,我們必須根據傳回的

MOVED

錯誤自己手動去傳回的節點中操作,是以Redis Cluster叢集要求用戶端必須要實作自動重定向。

在Jedis中,隻要連接配接任意一個或者多個節點,且不論是master還是slave節點,Jedis就可以自動實作重定向連接配接到叢集中任意節點。

下面我們就以Jedis用戶端為例來看看如何操作Redis Cluster:

package com.lonelyWolf.redis.cluster.server;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class TestRedisCluster {
    public static void main(String[] args) throws IOException {
        HostAndPort hp1 = new HostAndPort("xx.xxx.xxx.xxx",6370);
        Set nodes = new HashSet<HostAndPort>();
        nodes.add(hp1);
        JedisCluster cluster = new JedisCluster(nodes);
        cluster.set("name", "lonely_wolf");
        System.out.println(cluster.get("name") + cluster.);
        cluster.close();
    }
}
           

可以看到我這邊隻添加了6370一個節點,但是實際上

name

這個key應該是歸6371管的:

【Redis系列9】手把手帶你搭建單機版高可用分布式Redis叢集(Cluster)前言Redis叢集服務總結

注意,這裡如果Jedis的版本額Redis的版本不比對,可能會報錯,上面示例中因為Redis使用的是5.0.5版本,而假如Jedis使用的是低版本,如2.8。那麼就會報一個類似于下面這個異常:

Exception in thread "main" java.lang.NumberFormatException: For input string: "[email protected]"

Redis Cluster的不足

  • 1、用戶端需要能實作重定向或者實作smart client特性。
  • 2、資料的複制是通過異步複制的,不保證資料的強一緻性。

總結

本文從最簡單的master-slave架構入手,逐漸分析了Redis中的master-slave架構,Sentinel架構的實作原理,緊急着介紹了目前運用最廣泛的Redis Cluster高可用分布式架構的原理,最後還和大家一起手把手的分别搭建了master-slave叢集,Sentinel叢集和Redis Cluster叢集,相信通過本文,大家對Redis中的三種叢集方案會有深入的認識。

請關注我,和孤狼一起學習進步。

繼續閱讀