首頁 >資料庫 >Redis >帶你吃透Redis中的主從複製、Sentinel、集群

帶你吃透Redis中的主從複製、Sentinel、集群

青灯夜游
青灯夜游轉載
2021-10-15 11:15:012383瀏覽

這篇文章跟大家介紹一下Redis分散式的相關知識,帶大家吃透主從複製、Sentinel、集群,讓你的Redis水平更上一層!

帶你吃透Redis中的主從複製、Sentinel、集群

一、主從複製

1、簡介

主從複製是Redis分散式的基石,也是Redis高可用的保障。在Redis中,被複製的伺服器稱為主伺服器(Master),對主伺服器進行複製的伺服器稱為從伺服器(Slave)。 【相關建議:Redis影片教學

帶你吃透Redis中的主從複製、Sentinel、集群

#主從複製的設定非常簡單,有三種方式(其中IP-主伺服器IP位址/PORT -主伺服器Redis服務連接埠):

  • 設定檔-redis.conf檔中,設定slaveof ip port

  • ##指令-進入Redis客戶端執行slaveof ip port

  • 啟動參數- ./redis-server --slaveof ip port

2、主從複製的演進

Redis的主從複製機制,並不是一開始就像6.x版本一樣完善,而是一個版本一個版本迭代而來的。它大體上經過三個版本的迭代:

  • 2.8以前

  • 2.8~4.0

  • 4.0以後

隨著版本的成長,Redis主從複製機制逐漸完善;但是他們的本質都是圍繞同步(sync)和命令傳播(command propagate)兩個操作展開:

  • 同步(sync):指的是將從伺服器的資料狀態更新至主伺服器目前的資料狀態,主要發生在初始化或後續的全量同步。

  • 指令傳播(command propagate):當主伺服器的資料狀態被修改(寫/刪除等),主從之間的資料狀態不一致時,主服務將發生資料改變的命令傳播給從伺服器,讓主從伺服器之間的狀態重回一致。

2.1 版本2.8以前

2.1.1 同步

2.8先前的版本,從伺服器對主伺服器的同步需要從伺服器向主伺服器發生sync命令來完成:

帶你吃透Redis中的主從複製、Sentinel、集群

  • 從伺服器接收到客戶端發送的slaveof ip prot命令,從伺服器根據ip:port向主伺服器創建套接字連接

  • 套接字成功連接到主伺服器後,從伺服器會為這個套接字連接關聯一個專門用於處理複製工作的檔案事件處理器,處理後續的主伺服器發送的RDB檔案和傳播的命令

  • 開始進行複製,從伺服器向主伺服器發送sync命令

  • 主伺服器接收到sync指令後,執行bgsave指令,主伺服器主程序fork的子程序會產生一個RDB文件,同時將RDB快照產生後的所有寫入操作記錄在緩衝區中

  • bgsave指令執行完成後,主伺服器將產生的RDB檔案傳送給從伺服器,從伺服器接收到RDB檔案後,首先會清除本身的全部數據,然後載入RDB文件,將自己的資料狀態更新成主伺服器的RDB檔案的資料狀態

  • 主伺服器將緩衝區的寫入指令傳送給從伺服器,從伺服器接收指令,並執行。

  • 主從複製同步步驟完成

2.1.2 指令傳播

當同步工作完成之後,主從之間需要透過命令傳播來維持資料狀態的一致性。如下圖,當前主從伺服器之間完成同步工作之後,主服務接收客戶端的DEL K6指令後刪除了K6,此時從伺服器仍然存在K6,主從資料狀態並不一致。為了維持主從伺服器狀態一致,主伺服器會將導致自己資料狀態改變的命令傳播到從伺服器執行,當從伺服器也執行了相同的命令之後,主從伺服器之間的資料狀態將會保持一致。

帶你吃透Redis中的主從複製、Sentinel、集群

2.1.3 缺陷

從上面看不出2.8以前版本的主從複製有什麼缺陷,這是因為我們還沒有考慮網路波動的情況。了解分散式的兄弟們肯定聽過CAP理論,CAP理論是分散式儲存系統的基石,在CAP理論中P(partition網路分區)必然存在,Redis主從複製也不例外。當主從伺服器之間出現網路故障,導致一段時間內從伺服器與主伺服器之間無法通信,當從伺服器重新連接上主伺服器時,如果主伺服器在這段時間內資料狀態發生了改變,那麼主從伺服器之間將出現資料狀態不一致。在Redis 2.8以前的主從複製版本中,解決這種資料狀態不一致的方式是透過重新發送sync命令來實現。雖然sync能保證主從伺服器資料狀態一致,但很明顯sync是一個非常消耗資源的操作。

sync指令執行,主從伺服器需要佔用的資源:

  • 主伺服器執行BGSAVE產生RDB文件,會佔用大量CPU、磁碟I/O和記憶體資源

  • 主伺服器將產生的RDB檔案傳送給從伺服器,會佔用大量網路頻寬,

  • 從伺服器接收RDB檔案並載入,會導致從伺服器阻塞,無法提供服務

從上面三點可以看出,sync指令不僅會導致主伺服器的回應能力下降,也會導致從伺服器在此期間拒絕對外提供服務。

2.2 版本2.8-4.0

2.2.1 改進點

針對2.8以前的版本,Redis在2.8之後對從伺服器重連後的資料狀態同步進行了改進。改進的方向是減少全量同步(full resynchronizaztion)的發生,盡可能使用增量同步(partial resynchronization)。在2.8版本之後使用psync指令取代了sync指令來執行同步操作,psync指令同時具備全量同步與增量同步的功能:

  • 全量同步與上一版本(sync)一致

  • 增量同步中對於斷線重連後的複製,會根據情況採取不同措施;如果條件允許,仍然只發送從服務缺失的部分資料。

2.2.2 psync如何實現

Redis為了實現從伺服器斷線重連後的增量同步,增加了三個輔助參數:

  • 複製偏移(replication offset)

  • 積壓緩衝區(replication backlog)

  • 伺服器運行id (run id)

2.2.2.1 複製偏移量

在主伺服器和從伺服器內都會維護一個複製偏移量

  • 主伺服器向從服務發送數據,傳播N個位元組的數據,主服務的複製偏移量增加N

  • 從伺服器接收主伺服器發送的數據,接收N個位元組的數據,從伺服器的複製偏移量增加N

正常同步的情況如下:

帶你吃透Redis中的主從複製、Sentinel、集群

透過比較主從伺服器之間的複製偏移量是否相等,能夠得知主從伺服器之間的資料狀態是否保持一致。 假設此時A/B正常傳播,C從伺服器斷線,那麼將出現如下情況:

帶你吃透Redis中的主從複製、Sentinel、集群

#很明顯有了複製偏移量之後,從伺服器C斷線重連後,主伺服器只需要傳送從伺服器缺少的100位元組資料即可。 但是主伺服器又是如何知道從伺服器缺少的是那些資料呢?

2.2.2.2 複製積壓緩衝區

複製積壓緩衝區是固定長度的佇列,預設為1MB大小。當主伺服器資料狀態改變,主伺服器將資料同步給從伺服器的同時會另存一份到複製積壓緩衝區。

帶你吃透Redis中的主從複製、Sentinel、集群

複製積壓緩衝區為了能和偏移量進行匹配,它不僅儲存了資料內容,還記錄了每個位元組對應的偏移量:

复制积压缓冲区+字节值+帶你吃透Redis中的主從複製、Sentinel、集群

當從伺服器斷線重連後,從伺服器透過psync指令將自己的複製偏移量(offset)傳送給主伺服器,主伺服器可透過這個偏移量來判斷進行增量傳播還是全量同步。

  • 如果偏移量offset 1的資料仍然在複製積壓緩衝區中,那麼進行增量同步操作

  • 反之進行全量同步操作,與sync一致

Redis的複製積壓緩衝區的大小預設為1MB,如果需要自訂應該如何設定呢? 很明顯,我們希望能盡可能的使用增量同步,但是又不希望緩衝區佔用過多的記憶體空間。那我們可以透過預估Redis從服務斷線後重連的時間T,Redis主伺服器每秒接收的寫入指令的記憶體大小M,來設定複製積壓緩衝區的大小S。

S = 2 * M * T

#注意這裡擴大2倍是為了留有一定的餘地,保證絕大部分的斷線重連都能採用增量同步。

2.2.2.3 伺服器運行ID

看到這裡是不是再想上面已經可以實現斷線重連的增量同步了,還要運行ID幹嘛?其實還有一種情況沒考慮,就是當主伺服器宕機後,某台從伺服器被選出成為新的主伺服器,這種情況我們就透過比較運行ID來區分。

  • 執行ID(run id)是伺服器啟動時自動產生的40個隨機的十六進位字串,主服務和從伺服器都會產生執行ID

  • #當從伺服器首次同步主伺服器的資料時,主伺服器會傳送自己的執行ID給從伺服器,從伺服器會儲存在RDB檔案中

  • 當從伺服器斷線重連後,從伺服器會向主伺服器發送先前儲存的主伺服器運行ID,如果伺服器執行ID匹配,則證明主伺服器未發生更改,可以嘗試進行增量同步

  • #如果伺服器運行ID不匹配,則進行全量同步

#2.2.3 完整的psync

完整的psync過程非常的複雜,在2.8-4.0的主從複製版本中已經做到了非常完善。 psync指令發送的參數如下:

psync

當沒有從伺服器複製任何主伺服器(並不是主從第一次複製,因為主伺服器可能會變化,而是從伺服器第一次全量同步),從伺服器將會發送:

psync ? -1

帶你吃透Redis中的主從複製、Sentinel、集群

一起完整的psync流程如下圖:

一次完整的帶你吃透Redis中的主從複製、Sentinel、集群

  • #從伺服器接收到SLAVEOF 127.0.0.1 6379指令

  • 從伺服器回傳OK給指令發起方(這裡是非同步操作,先回傳OK,再儲存位址和連接埠資訊)

  • 從伺服器將IP位址和連接埠資訊儲存到Master Host和Master Port

  • #從伺服器根據Master Host和Master Port主動向主伺服器發起套接字連接,同時從服務將會未這個套接字連接關聯一個專門用於文件複製工作的文件事件處理器,用於後續的RDB文件複製等工作

  • 主伺服器接收到從伺服器的套接字連接請求,為該請求創建對應的套接字連接之後,並將從伺服器看著一個客戶端(在主從複製中,主伺服器和從伺服器之間其實互為客戶端和服務端)

  • 套接字連線建立完成,從伺服器主動向主服務發送PING指令,如果在指定的逾時時間內主伺服器傳回PONG,則證明套接字連接可用,否則斷開重連

  • 如果主伺服器設定了密碼(masterauth),那麼從伺服器向主伺服器發送AUTH masterauth命令,進行身份驗證。注意,如果從伺服器發送了密碼,主服務並未設定密碼,此時主服務會發送no password is set錯誤;如果主伺服器需要密碼,而從伺服器未發送密碼,此時主伺服器會發送NOAUTH錯誤;如果密碼不匹配,主伺服器會傳送invalid password錯誤。

  • 從伺服器傳送REPLCONF listening-port xxxx(xxxx表示從伺服器的連接埠)。主伺服器接收到該指令後會將資料儲存起來,當客戶端使用INFO replication查詢主從資訊時能夠傳回資料

  • 從伺服器傳送psync指令,此步驟請檢視上圖psync的兩種情況

  • 主伺服器與從伺服器之間互為客戶端,進行資料的請求/回應

  • 主伺服器與從伺服器之間透過心跳包機制,判斷連線是否斷開。從伺服器每個1秒向主伺服器發送命令,REPLCONF ACL offset(從伺服器的複製偏移量),該機制可以保證主從之間資料的正確同步,如果偏移量不相等,主伺服器將會採取增量/全量同步措施來確保主從之間資料狀態一致(增量/全量的選擇取決於,offset 1的資料是否仍在複製積壓緩衝區中)

2.3 版本4.0

Redis 2.8-4.0版本仍有一些改進的空間,當主伺服器切換時,是否也能進行增量同步呢?因此Redis 4.0版本針對這個問題做了最佳化處理,psync升級為psync2.0。 psync2.0 拋棄了伺服器運行ID,採用了replid和replid2來代替,其中replid儲存的是目前主伺服器的運行ID,replid2保存的是上一個主伺服器運行ID。

  • 複製偏移(replication offset)

  • #積壓緩衝區(replication backlog)

  • #主伺服器運行id(replid)

  • 上主伺服器運行id(replid2)

透過replid和replid2我們可以解決主伺服器切換時,增量同步的問題:

  • 如果replid等於目前主伺服器的運行id,那麼判斷同步方式增量/全量同步

  • 如果replid不相等,則判斷replid2是否相等(是否同屬於上一個主伺服器的從伺服器),如果相等,仍然可以選擇增量/全量同步,如果不相等則只能進行全量同步。

二、Sentinel

1、簡介

主從複製奠定了Redis分散式的基礎,但是普通的主從複製並不能達到高可用的狀態。在普通的主從複製模式下,如果主伺服器宕機,就只能透過維運人員手動切換主伺服器,很顯然這種方案並不可取。針對上述情況,Redis官方推出了可抵抗節點故障的高可用方案-Redis Sentinel(哨兵)。 Redis Sentinel(哨兵):由一個或多個Sentinel實例組成的Sentinel系統,它可以監視任意多個主從伺服器,當監視的主伺服器宕機時,自動離線主伺服器,並且擇優選取從伺服器升級為新的主伺服器。

如下範例:當舊Master下線時長超過使用者設定的下線時長上限,Sentinel系統就會對舊Master執行故障轉移操作,故障轉移操作包含三個步驟:

  • 在Slave中選擇資料最新的作為新的Master

  • #向其他Slave發送新的複製指令,讓其他從伺服器成為新的Master的Slave

  • 繼續監視舊Master,如果其上線則將舊Master設定為新Master的Slave

帶你吃透Redis中的主從複製、Sentinel、集群

本文基於以下資源清單進行進行:

6379/26379##192.168.211.106Redis Slave/ Sentinel
#IP位址 節點角色
#192.168.211.104 # Redis Master/ Sentinel 6379/26379
192.168.211.105 192.168.211.105 Redis Slave/ Sentinel
##192.168.211.106
###6379/26379################

2、Sentinel初始化與網路連線

Sentinel並沒有什麼特別神奇的地方,它就是一個更簡單的Redis伺服器,在Sentinel啟動的時候它會載入不同的命令表和設定文件,因此從本質上來講Sentinel就是一個擁有較少命令和部分特殊功能的Redis服務。當一個Sentinel啟動時它需要經歷以下步驟:

  • 初始化Sentinel伺服器

  • 取代普通Redis程式碼為Sentinel的專用程式碼

  • 初始化Sentinel狀態

  • 根據使用者給定的Sentinel設定文件,初始化Sentinel監視的主伺服器清單

  • 建立連接主伺服器的網路連線

  • 根據主服務取得從伺服器信息,建立連線從伺服器的網路連線

  • ##根據發布/訂閱取得Sentinel訊息,建立Sentinel之間的網路連線

2.1 初始化Sentinel伺服器

Sentinel基本上就是一個Redis伺服器,因此啟動Sentinel需要啟動一個Redis伺服器,但是Sentinel並不需要讀取RDB/AOF檔來還原資料狀態。

2.2 取代普通Redis程式碼為Sentinel的專用程式碼

Sentinel用於較少的Redis指令,大部分指令在Sentinel客戶端都不支持,並且Sentinel擁有一些特殊的功能,這些需要Sentinel在啟動時將Redis伺服器使用的程式碼替換為Sentinel的專用程式碼。在此期間Sentinel會載入與普通Redis伺服器不同的命令表。 Sentinel不支援SET、DBSIZE等指令;保留支援PING、PSUBSCRIBE、SUBSCRIBE、UNSUBSCRIBE、INFO等指令;這些指令在Sentinel工作中提供了保障。

2.3 初始化Sentinel狀態

裝載Sentinel的特有程式碼之後,Sentinel會初始化sentinelState結構,該結構用於儲存Sentinel相關的狀態訊息,其中最重要的就是masters字典。

struct sentinelState {
   
    //当前纪元,故障转移使用
 uint64_t current_epoch; 
  
    // Sentinel监视的主服务器信息 
    // key -> 主服务器名称 
    // value -> 指向sentinelRedisInstance指针
    dict *masters; 
    // ...
} sentinel;

2.4 初始化Sentinel監視的主伺服器清單

Sentinel監視的主伺服器清單保存在sentinelState的masters字典中,當sentinelState創建之後,開始對Sentinel監視的主伺服器清單進行初始化。

  • masters的key是主服務的名字

  • masters的value是指指向sentinelRedisInstance指標

#主伺服器的名字由我們sentinel.conf設定檔指定,如下主伺服器名字為redis-master(我這裡是一主二從的配置):

daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-6.2.4/sentinel-tmp"
sentinel monitor redis-master 192.168.211.104 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1

sentinelRedisInstance實例保存了Redis伺服器的信息(主伺服器、從伺服器、Sentinel資訊都保存在這個實例中)。

typedef struct sentinelRedisInstance {
 
    // 标识值,标识当前实例的类型和状态。如SRI_MASTER、SRI_SLVAE、SRI_SENTINEL
    int flags;
    
    // 实例名称 主服务器为用户配置实例名称、从服务器和Sentinel为ip:port
    char *name;
    
    // 服务器运行ID
    char *runid;
    
    //配置纪元,故障转移使用
 uint64_t config_epoch; 
    
    // 实例地址
    sentinelAddr *addr;
    
    // 实例判断为主观下线的时长 sentinel down-after-milliseconds redis-master 30000
    mstime_t down_after_period; 
    
    // 实例判断为客观下线所需支持的投票数 sentinel monitor redis-master 192.168.211.104 6379 2
    int quorum;
    
    // 执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 sentinel parallel-syncs redis-master 1
    int parallel-syncs;
    
    // 刷新故障迁移状态的最大时限 sentinel failover-timeout redis-master 180000
 mstime_t failover_timeout;
    
    // ...
} sentinelRedisInstance;

根據上面的一主二從配置將會得到如下實例結構:

帶你吃透Redis中的主從複製、Sentinel、集群

#2.5 建立連接主伺服器的網路連線

當實例結構初始化完成之後,Sentinel將會開始建立連接Master的網路連接,這一步Sentinel將成為Master的客戶端。 Sentinel與Master之間會建立一個指令連線和一個訂閱連線:

  • 指令連線用於取得主從資訊

  • ##訂閱連線用於Sentinel之間進行資訊廣播,每個Sentinel和自己監視的主從伺服器之間會訂閱_sentinel_:hello頻道(注意Sentinel之間不會建立訂閱連接,它們透過訂閱_sentinel_:hello頻道來取得其他Sentinel的初始訊息)

帶你吃透Redis中的主從複製、Sentinel、集群Sentinel在建立指令連線完成之後,每隔10秒鐘向Master發送INFO指令,透過Master的回覆訊息可以獲得兩方面的知識:

    Master本身的資訊
  • Master下的Slave資訊

帶你吃透Redis中的主從複製、Sentinel、集群2.6 建立連接從伺服器的網路連接

根據主服務取得從伺服器訊息,Sentinel可以建立到Slave的網路連接,Sentinel和Slave之間也會建立命令連接和訂閱連接。

Slave帶你吃透Redis中的主從複製、Sentinel、集群

当Sentinel和Slave之间创建网络连接之后,Sentinel成为了Slave的客户端,Sentinel也会每隔10秒钟通过INFO指令请求Slave获取服务器信息。 到这一步Sentinel获取到了Master和Slave的相关服务器数据。这其中比较重要的信息如下:

  • 服务器ip和port

  • 服务器运行id run id

  • 服务器角色role

  • 服务器连接状态mater_link_status

  • Slave复制偏移量slave_repl_offset(故障转移中选举新的Master需要使用)

  • Slave优先级slave_priority

此时实例结构信息如下所示:

帶你吃透Redis中的主從複製、Sentinel、集群

2.7 创建Sentinel之间的网络连接

此时是不是还有疑问,Sentinel之间是怎么互相发现对方并且相互通信的,这个就和上面Sentinel与自己监视的主从之间订阅_sentinel_:hello频道有关了。 Sentinel会与自己监视的所有Master和Slave之间订阅_sentinel_:hello频道,并且Sentinel每隔2秒钟向_sentinel_:hello频道发送一条消息,消息内容如下:

PUBLISH sentinel:hello ",,,,,,,"

其中s代码Sentinel,m代表Master;ip表示IP地址,port表示端口、runid表示运行id、epoch表示配置纪元。

多个Sentinel在配置文件中会配置相同的主服务器ip和端口信息,因此多个Sentinel均会订阅_sentinel_:hello频道,通过频道接收到的信息就可获取到其他Sentinel的ip和port,其中有如下两点需要注意:

  • 如果获取到的runid与Sentinel自己的runid相同,说明消息是自己发布的,直接丢弃

  • 如果不相同,则说明接收到的消息是其他Sentinel发布的,此时需要根据ip和port去更新或新增Sentinel实例数据

Sentinel之间不会创建订阅连接,它们只会创建命令连接:

帶你吃透Redis中的主從複製、Sentinel、集群

此时实例结构信息如下所示:

帶你吃透Redis中的主從複製、Sentinel、集群

3、Sentinel工作

Sentinel最主要的工作就是监视Redis服务器,当Master实例超出预设的时限后切换新的Master实例。这其中有很多细节工作,大致分为检测Master是否主观下线、检测Master是否客观下线、选举领头Sentinel、故障转移四个步骤。

3.1 检测Master是否主观下线

Sentinel每隔1秒钟,向sentinelRedisInstance实例中的所有Master、Slave、Sentinel发送PING命令,通过其他服务器的回复来判断其是否仍然在线。

sentinel down-after-milliseconds redis-master 30000

在Sentinel的配置文件中,当Sentinel PING的实例在连续down-after-milliseconds配置的时间内返回无效命令,则当前Sentinel认为其主观下线。Sentinel的配置文件中配置的down-after-milliseconds将会对其sentinelRedisInstance实例中的所有Master、Slave、Sentinel都适应。

无效指令指的是+PONG、-LOADING、-MASTERDOWN之外的其他指令,包括无响应

如果当前Sentinel检测到Master处于主观下线状态,那么它将会修改其sentinelRedisInstance的flags为SRI_S_DOWN

帶你吃透Redis中的主從複製、Sentinel、集群

3.2 检测Master是否客观下线

当前Sentinel认为其下线只能处于主观下线状态,要想判断当前Master是否客观下线,还需要询问其他Sentinel,并且所有认为Master主观下线或者客观下线的总和需要达到quorum配置的值,当前Sentinel才会将Master标志为客观下线。

帶你吃透Redis中的主從複製、Sentinel、集群

当前Sentinel向sentinelRedisInstance实例中的其他Sentinel发送如下命令:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
  • ip:被判断为主观下线的Master的IP地址

  • port:被判断为主观下线的Master的端口

  • current_epoch:当前sentinel的配置纪元

  • runid:当前sentinel的运行id,runid

current_epoch和runid均用于Sentinel的选举,Master下线之后,需要选举一个领头Sentinel来选举一个新的Master,current_epoch和runid在其中发挥着重要作用,这个后续讲解。

接收到命令的Sentinel,会根据命令中的参数检查主服务器是否下线,检查完成后会返回如下三个参数:

  • down_state:检查结果1代表已下线、0代表未下线

  • leader_runid:返回*代表判断是否下线,返回runid代表选举领头Sentinel

  • leader_epoch:当leader_runid返回runid时,配置纪元会有值,否则一直返回0

  • 当Sentinel检测到Master处于主观下线时,询问其他Sentinel时会发送current_epoch和runid,此时current_epoch=0,runid=*

  • 接收到命令的Sentinel返回其判断Master是否下线时down_state = 1/0,leader_runid = *,leader_epoch=0

帶你吃透Redis中的主從複製、Sentinel、集群

3.3 选举领头Sentinel

down_state返回1,证明接收is-master-down-by-addr命令的Sentinel认为该Master也主观下线了,如果down_state返回1的数量(包括本身)大于等于quorum(配置文件中配置的值),那么Master正式被当前Sentinel标记为客观下线。 此时,Sentinel会再次发送如下指令:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

此时的runid将不再是0,而是Sentinel自己的运行id(runid)的值,表示当前Sentinel希望接收到is-master-down-by-addr命令的其他Sentinel将其设置为领头Sentinel。这个设置是先到先得的,Sentinel先接收到谁的设置请求,就将谁设置为领头Sentinel。 发送命令的Sentinel会根据其他Sentinel回复的结果来判断自己是否被该Sentinel设置为领头Sentinel,如果Sentinel被其他Sentinel设置为领头Sentinel的数量超过半数Sentinel(这个数量在sentinelRedisInstance的sentinel字典中可以获取),那么Sentinel会认为自己已经成为领头Sentinel,并开始后续故障转移工作(由于需要半数,且每个Sentinel只会设置一个领头Sentinel,那么只会出现一个领头Sentinel,如果没有一个达到领头Sentinel的要求,Sentinel将会重新选举直到领头Sentinel产生为止)。

3.4 故障转移

故障转移将会交给领头sentinel全权负责,领头sentinel需要做如下事情:

  • 从原先master的slave中,选择最佳的slave作为新的master

  • 让其他slave成为新的master的slave

  • 继续监听旧master,如果其上线,则将其设置为新的master的slave

这其中最难的一步是如果选择最佳的新Master,领头Sentinel会做如下清洗和排序工作:

  • 判断slave是否有下线的,如果有从slave列表中移除

  • 删除5秒内未响应sentinel的INFO命令的slave

  • 删除与下线主服务器断线时间超过down_after_milliseconds * 10 的所有从服务器

  • 根据slave优先级slave_priority,选择优先级最高的slave作为新master

  • 如果优先级相同,根据slave复制偏移量slave_repl_offset,选择偏移量最大的slave作为新master

  • 如果偏移量相同,根据slave服务器运行id run id排序,选择run id最小的slave作为新master

新的Master产生后,领头sentinel会向已下线主服务器的其他从服务器(不包括新Master)发送SLAVEOF ip port命令,使其成为新master的slave。

到这里Sentinel的的工作流程就算是结束了,如果新master下线,则循环流程即可!

三、集群

1、简介

Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)进行数据共享,Redis集群主要实现了以下目标:

  • 在1000个节点的时候仍能表现得很好并且可扩展性是线性的。

  • 没有合并操作(多个节点不存在相同的键),这样在 Redis 的数据模型中最典型的大数据值中也能有很好的表现。

  • 寫入安全,那些與大多數節點相連的客戶端所做的寫入操作,系統嘗試全部都保存下來。但Redis無法保證資料完全不遺失,非同步同步的主從複製無論如何都會存在資料遺失的情況。

  • 可用性,主節點不可用,從節點能替換主節點工作。

關於Redis集群的學習,如果沒有任何經驗的弟兄們建議先看下這三篇文章(中文系列): Redis集群教程

REDIS cluster-tutorial -- Redis中文資料站-- Redis中國使用者群組(CRUG)

Redis叢集規格

REDIS cluster-spec -- Redis中文資料站-- Redis中國用戶組(CRUG)

Redis3主3從偽集群部署

CentOS 7單機安裝Redis Cluster(3主3從偽集群),僅需簡單五步驟_李子捌的博客-CSDN博客

#下文內容依賴下圖三主三從結構開展:

帶你吃透Redis中的主從複製、Sentinel、集群

資源清單:

##Master[1]192.168.211.107:6329Slots 5461 - 10922Master[2]192.168.211.107:6339Slots 10923 - 16383# Slave[0]192.168.211.107:6369#Slave[1]Slave[2]

帶你吃透Redis中的主從複製、Sentinel、集群

帶你吃透Redis中的主從複製、Sentinel、集群

2、集群内部

Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,这种结构很容易添加或者删除节点。集群的每个节点负责一部分hash槽,比如上面资源清单的集群有3个节点,其槽分配如下所示:

  • 节点 Master[0] 包含 0 到 5460 号哈希槽

  • 节点 Master[1] 包含5461 到 10922 号哈希槽

  • 节点 Master[2] 包含10923到 16383 号哈希槽

深入学习Redis集群之前,需要了解集群中Redis实例的内部结构。当某个Redis服务节点通过cluster_enabled配置为yes开启集群模式之后,Redis服务节点不仅会继续使用单机模式下的服务器组件,还会增加custerState、clusterNode、custerLink等结构用于存储集群模式下的特殊数据。

如下三个数据承载对象一定要认真看,尤其是结构中的注释,看完之后集群大体上怎么工作的,心里就有数了,嘿嘿嘿;

2.1 clsuterNode

clsuterNode用于存储节点信息,比如节点的名字、IP地址、端口信息和配置纪元等等,以下代码列出部分非常重要的属性:

typedef struct clsuterNode {

    // 创建时间
    mstime_t ctime;
    
    // 节点名字,由40位随机16进制的字符组成(与sentinel中讲的服务器运行id相同)
    char name[REDIS_CLUSTER_NAMELEN];
    
    // 节点标识,可以标识节点的角色和状态
    // 角色 -> 主节点或从节点 例如:REDIS_NODE_MASTER(主节点) REDIS_NODE_SLAVE(从节点)
    // 状态 -> 在线或下线 例如:REDIS_NODE_PFAIL(疑似下线) REDIS_NODE_FAIL(下线) 
    int flags;
    
    // 节点配置纪元,用于故障转移,与sentinel中用法类似
    // clusterState中的代表集群的配置纪元
    unit64_t configEpoch;
    
    // 节点IP地址
    char ip[REDIS_IP_STR_LEN];
    
    // 节点端口
    int port;
    
    // 连接节点的信息
    clusterLink *link;
    
    // 一个2048字节的二进制位数组
    // 位数组索引值可能为0或1
    // 数组索引i位置值为0,代表节点不负责处理槽i
    // 数组索引i位置值为1,代表节点负责处理槽i
    unsigned char slots[16384/8];
    
    // 记录当前节点处理槽的数量总和
    int numslots;
    
    // 如果当前节点是从节点
    // 指向当前从节点的主节点
    struct clusterNode *slaveof;
    
    // 如果当前节点是主节点
    // 正在复制当前主节点的从节点数量
    int numslaves;
    
    // 数组——记录正在复制当前主节点的所有从节点
    struct clusterNode **slaves;
    
} clsuterNode;

上述代码中可能不太好理解的是slots[16384/8],其实可以简单的理解为一个16384大小的数组,数组索引下标处如果为1表示当前槽属于当前clusterNode处理,如果为0表示不属于当前clusterNode处理。clusterNode能够通过slots来识别,当前节点处理负责处理哪些槽。 初始clsuterNode或者未分配槽的集群中的clsuterNode的slots如下所示:

帶你吃透Redis中的主從複製、Sentinel、集群

假设集群如上面我给出的资源清单,此时代表Master[0]的clusterNode的slots如下所示:

帶你吃透Redis中的主從複製、Sentinel、集群

clusterLink是clsuterNode中的一个属性,用于存储连接节点所需的相关信息,比如套接字描述符、输入输出缓冲区等待,以下代码列出部分非常重要的属性:

typedef struct clusterState {

    // 连接创建时间
    mstime_t ctime;
   
    // TCP 套接字描述符
    int fd;
    
    // 输出缓冲区,需要发送给其他节点的消息缓存在这里
    sds sndbuf;
    
    // 输入缓冲区,接收打其他节点的消息缓存在这里
    sds rcvbuf;
    
    // 与当前clsuterNode节点代表的节点建立连接的其他节点保存在这里
    struct clusterNode *node;
} clusterState;

2.3 custerState

每个节点都会有一个custerState结构,这个结构中存储了当前集群的全部数据,比如集群状态、集群中的所有节点信息(主节点、从节点)等等,以下代码列出部分非常重要的属性:

typedef struct clusterState {

    // 当前节点指针,指向一个clusterNode
    clusterNode *myself;
    
    // 集群当前配置纪元,用于故障转移,与sentinel中用法类似
    unit64_t currentEpoch;
    
    // 集群状态 在线/下线
    int state;
    
    // 集群中处理着槽的节点数量总和
    int size;
    
    // 集群节点字典,所有clusterNode包括自己
    dict *node;
    
    // 集群中所有槽的指派信息
    clsuterNode *slots[16384];
    
    // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽
    clusterNode *importing_slots_from[16384];
    
    // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽
    clusterNode *migrating_slots_to[16384];
    
    // ...
    
} clusterState;

在custerState有三个结构需要认真了解的,第一个是slots数组,clusterState中的slots数组与clsuterNode中的slots数组是不一样的,在clusterNode中slots数组记录的是当前clusterNode所负责的槽,而clusterState中的slots数组记录的是整个集群的每个槽由哪个clsuterNode负责,因此集群正常工作的时候clusterState的slots数组每个索引指向负责该槽的clusterNode,集群槽未分配之前指向null。

如图展示资源清单中的集群clusterState中的slots数组与clsuterNode中的slots数组:

帶你吃透Redis中的主從複製、Sentinel、集群

Redis集群中使用两个slots数组的原因是出于性能的考虑:

  • 当我们需要获取整个集群中clusterNode分别负责什么槽时,只需要查询clusterState中的slots数组即可。如果没有clusterState的slots数组,则需要遍历所有的clusterNode结构,这样显然要慢一些

  • 此外clusterNode中的slots数组也有存在的必要,因为集群中任意一个节点之间需要知道彼此负责的槽,此时节点之间只需要互相传输clusterNode中的slots数组结构就行。

第二个需要认真了解的结构是node字典,该结构虽然简单,但是node字典中存储了所有的clusterNode,这也是Redis集群中的单个节点获取其他主节点、从节点信息的主要位置,因此我们也需要注意一下。 第三个需要认真了解的结构是importing_slots_from[16384]数组和migrating_slots_to[16384],这两个数组在集群重新分片时需要使用,需要重点了解,后面再说吧,这里说的话顺序不太对。

3、叢集工作

3.1 槽(slot)如何指派?

Redis集群一共16384個槽,如上資源清單我們在三主三從的集群中,每個主節點負責自己對應的槽,而在上面的三主三從部署的過程中並未看到我指定槽給對應的主節點,這是因為Redis叢集自己內部給我們劃分了槽,但是如果我們想自己指派槽該如何整呢?我們可以向節點發送如下命令,將一個或多個槽指派給當前節點負責:

CLUSTER ADDSLOTS

例如我們想把0和1槽指派給Master[0],我們只需要想Master[0]節點發送以下命令:

#CLUSTER ADDSLOTS 0 1

當節點被指派了槽後,會將clusterNode的slots數組更新,節點會將自己負責處理的槽也就是slots數組通過訊息發送給集群中的其他節點,其他節點在接收當消息後會更新對應clusterNode的slots數組以及clusterState的solts數組。

3.2 ADDSLOTS 在Redis叢集內部是如何實現的呢?

這個其實也比較簡單,當我們向Redis叢集中的某個節點發送CLUSTER ADDSLOTS指令時,目前節點會先透過clusterState中的slots陣列來確認指派給目前節點的插槽是否沒有指派給其他節點,如果已經指派了,那麼會直接拋出異常,回傳錯誤給指派的客戶端。如果指派給目前節點的所有槽都未指派給其他節點,那麼目前節點會將這些槽指派給自己。指派主要有三個步驟:

  • 更新clusterState的slots數組,將指定槽slots[i]指向目前clusterNode

  • 更新clusterNode的slots數組,將指定槽slots[i]處的值更新為1

  • 向叢集中的其他節點發送訊息,將clusterNode的slots數組傳送給其他節點,其他節點接收到訊息後面也更新對應的clusterState的slots陣列和clusterNode的slots陣列

3.3 叢集這麼多節點,客戶端怎麼知道請求哪個節點?

在了解這個問題之前先要知道一個點,Redis集群是怎麼計算當前這個鍵屬於哪個槽的呢?根據官網的介紹,Redis其實並未使用一致性hash演算法,而是將每個請求的key透過CRC16校驗後對16384取模來決定放置到哪個槽中。

HASH_SLOT = CRC16(key) mod 16384

此時,當客戶端連線向某個節點發送請求時,目前接收到命令的節點首先會透過演算法計算出當前key所屬的槽i,計算完後當前節點會判斷clusterState的槽i是否由自己負責,如果恰好由自己負責那麼當前節點就會之間響應客戶端的請求,如果不由目前節點負責,則會經歷以下步驟:

  • 節點向客戶端傳回MOVED重定向錯誤,MOVED重定向錯誤中會將運算好的正確處理該key的clusterNode的ip和port傳回給客戶端

  • 客戶端接收到節點回傳的MOVED重定向錯誤時,會根據ip和port將指令轉送給正確的節點,整個處理過程對程式設計師來說透明,由Redis叢集的服務端和客戶端共同負責完成。

3.4 如果我想將已經指派給A節點的槽重新指派給B節點,怎麼整?

這個問題其實涵括了很多問題,例如移除Redis叢集中的某些節點,增加節點等都可以概括為把哈希槽從一個節點移動到另外一個節點。而Redis叢集非常牛逼的一點也在這裡,它支援線上(不停機)的分配,也就是官方說叢集在線重配置(live reconfiguration )。

在將實作之前先來看下CLUSTER的指令,指令會了操作就會了:

  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]

  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]

  • CLUSTER SETSLOT slot NODE node

  • CLUSTER SETSLOT slot MIGRATING node

  • CLUSTER SETSLOT slot IMPORTING node

CLUSTER 用于槽分配的指令主要有如上这些,ADDSLOTS 和DELSLOTS主要用于槽的快速指派和快速删除,通常我们在集群刚刚建立的时候进行快速分配的时候才使用。CLUSTER SETSLOT slot NODE node也用于直接给指定的节点指派槽。如果集群已经建立我们通常使用最后两个来重分配,其代表的含义如下所示:

  • 当一个槽被设置为 MIGRATING,原来持有该哈希槽的节点仍会接受所有跟这个哈希槽有关的请求,但只有当查询的键还存在原节点时,原节点会处理该请求,否则这个查询会通过一个 -ASK 重定向(-ASK redirection)转发到迁移的目标节点。

  • 当一个槽被设置为 IMPORTING,只有在接受到 ASKING 命令之后节点才会接受所有查询这个哈希槽的请求。如果客户端一直没有发送 ASKING 命令,那么查询都会通过 -MOVED 重定向错误转发到真正处理这个哈希槽的节点那里。

上面这两句话是不是感觉不太看的懂,这是官方的描述,不太懂的话我来给你通俗的描述,整个流程大致如下步骤:

  • redis-trib(集群管理软件redis-trib会负责Redis集群的槽分配工作),向目标节点(槽导入节点)发送CLUSTER SETSLOT slot IMPORTING node命令,目标节点会做好从源节点(槽导出节点)导入槽的准备工作。

  • redis-trib随即向源节点发送CLUSTER SETSLOT slot MIGRATING node命令,源节点会做好槽导出准备工作

  • redis-trib随即向源节点发送CLUSTER GETKEYSINSLOT slot count命令,源节点接收命令后会返回属于槽slot的键,最多返回count个键

  • redis-trib会根据源节点返回的键向源节点依次发送MIGRATE ip port key 0 timeout命令,如果key在源节点中,将会迁移至目标节点。

  • 迁移完成之后,redis-trib会向集群中的某个节点发送CLUSTER SETSLOT slot NODE node命令,节点接收到命令后会更新clusterNode和clusterState结构,然后节点通过消息传播槽的指派信息,至此集群槽迁移工作完成,且集群中的其他节点也更新了新的槽分配信息。

3.5 如果客户端访问的key所属的槽正在迁移怎么办?

优秀的你总会想到这种并发情况,牛皮呀!大佬们!

帶你吃透Redis中的主從複製、Sentinel、集群

这个问题官方也考虑了,还记得我们在聊clusterState结构的时候么?importing_slots_from和migrating_slots_to就是用来处理这个问题的。

typedef struct clusterState {

    // ...
    
    // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽
    clusterNode *importing_slots_from[16384];
    
    // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽
    clusterNode *migrating_slots_to[16384];
    
    // ...
    
} clusterState;
  • 当节点正在导出某个槽,则会在clusterState中的migrating_slots_to数组对应的下标处设置其指向对应的clusterNode,这个clusterNode会指向导入的节点。

  • 当节点正在导入某个槽,则会在clusterState中的importing_slots_from数组对应的下标处设置其指向对应的clusterNode,这个clusterNode会指向导出的节点。

有了上述两个相互数组,就能判断当前槽是否在迁移了,而且从哪里迁移来,要迁移到哪里去?搞笑不就是这么简单……

此时,回到问题中,如果客户端请求的key刚好属于正在迁移的槽。那么接收到命令的节点首先会尝试在自己的数据库中查找键key,如果这个槽还没迁移完成,且当前key刚好也还没迁移完成,那就直接响应客户端的请求就行。如果该key已经不在了,此时节点会去查询migrating_slots_to数组对应的索引槽,如果索引处的值不为null,而是指向了某个clusterNode结构,那说明这个key已经被迁移到这个clusterNode了。这个时候节点不会继续在处理指令,而是返回ASKING命令,这个命令也会携带导入槽clusterNode对应的ip和port。客户端在接收到ASKING命令之后就需要将请求转向正确的节点了,不过这里有一点需要注意的地方**(因此我放个表情包在这里,方便读者注意)。**

帶你吃透Redis中的主從複製、Sentinel、集群

前面說了,當節點發現目前槽不屬於自己處理時會回傳MOVED指令,那麼在遷移中的槽時怎麼處理的呢?這個Redis集群是這個玩的。節點發現槽正在遷移則向客戶端傳回ASKING指令,客戶端會接收到ASKING指令,其中包含了槽遷入的clusterNode的節點ip和port。那麼客戶端首先會向遷入的clusterNode發送一條ASKING命令,這個命令必須要發目的是告訴當前節點,你要破例處理這次請求,因為這個槽已經遷移到你這裡了,你不能直接拒絕我(因此如果Redis未接收到ASKING命令,會直接查詢節點的clusterState,而正在遷移中的槽還沒有更新到clusterState中,那麼只能直接返回MOVED,這樣不就會一直循環很多次…),接收到ASKING指令的節點會強制執行一次這個請求(只執行一次,下次再來需要重新提前發送ASKING指令)。

4、叢集故障

Redis叢集故障比較簡單,這個和sentinel中主節點宕機或在指定最長時間內未回應,重新在從節點中選出新的主節點的方式其實差不多。當然前提是Redis叢集中的每個主節點,我們事先設定了從節點,要不要就嘿嘿嘿…沒戲。其大致步驟如下:

  • 正常工作的集群,每個節點之間會定期向其他節點發送PING命令,如果接收命令的節點未在規定時間內返回PONG訊息,目前節點會將接收指令的節點的clusterNode的flags設定為REDIS_NODE_PFAIL,PFAIL並不是下線,而是疑似下線。

  • 叢集節點會透過傳送訊息的方式告知其他節點,叢集中各節點的狀態資訊

  • 如果叢集中半數以上負責處理槽的主節點都會將某個主節點設定為疑似下線,那麼這個節點將會被標記位元下線狀態,節點會將接收指令的節點的clusterNode的flags設定為REDIS_NODE_FAIL,FAIL表示已下線

  • 叢集節點透過發送訊息的方式告知其他節點,叢集中各個節點的狀態訊息,此時下線節點的從節點在發現自己的主節點已經被標記為下線狀態了,那麼是時候挺身而出了

  • 下線主節點的從節點,會選出一個從節點作為最新的主節點,執行被選中的節點指向SLAVEOF no one成為新的主節點

  • 新的主節點會撤銷掉原主節點的槽指派,並將這些槽指派修改為自己,也就是修改clusterNode結構和clusterState結構

  • 新的主節點向叢集廣播一條PONG指令,其他節點將會知道有新的主節點產生,並更新clusterNode結構和clusterState結構

  • 新的主節點如果會向原主節點剩餘的從節點發送新的SLAVEOF指令,使其成為自己的從節點

  • ##最後新的主節點將會負責原主節點的槽的回應工作

這裡我寫得非常模糊,如果需要細緻挖掘的一定要看這篇文章:

REDIS cluster-spec - - Redis中文資料站-- Redis中國用戶組(CRUG)

http://redis.cn/topics/cluster-spec.html

或可看黃健宏老師的《Redis設計與實現》這本書寫得挺好,我也參考了許多內容。

更多程式相關知識,請造訪:

程式設計影片! !

# IP 槽(slot)範圍
#Master[0] 192.168 .211.107:6319 Slots 0 - 5460

#192.168.211.107: 6349
192.168.211.107:6359

以上是帶你吃透Redis中的主從複製、Sentinel、集群的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除