您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“Redis中主從復制、Sentinel、集群有什么用”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“Redis中主從復制、Sentinel、集群有什么用”這篇文章吧。
主從復制是Redis分布式的基石,也是Redis高可用的保障。在Redis中,被復制的服務器稱為主服務器(Master),對主服務器進行復制的服務器稱為從服務器(Slave)。
主從復制的配置非常簡單,有三種方式(其中IP-主服務器IP地址/PORT-主服務器Redis服務端口):
配置文件——redis.conf文件中,配置slaveof ip port
命令——進入Redis客戶端執行slaveof ip port
啟動參數—— ./redis-server --slaveof ip port
Redis的主從復制機制,并不是一開始就像6.x版本一樣完善,而是一個版本一個版本迭代而來的。它大體上經過三個版本的迭代:
2.8以前
2.8~4.0
4.0以后
隨著版本的增長,Redis主從復制機制逐漸完善;但是他們的本質都是圍繞同步(sync)和命令傳播(command propagate)兩個操作展開:
同步(sync):指的是將從服務器的數據狀態更新至主服務器當前的數據狀態,主要發生在初始化或后續的全量同步。
命令傳播(command propagate):當主服務器的數據狀態被修改(寫/刪除等),主從之間的數據狀態不一致時,主服務將發生數據改變的命令傳播給從服務器,讓主從服務器之間的狀態重回一致。
2.1.1 同步
2.8以前的版本,從服務器對主服務器的同步需要從服務器向主服務器發生sync命令來完成:
從服務器接收到客戶端發送的slaveof ip prot命令,從服務器根據ip:port向主服務器創建套接字連接
套接字成功連接到主服務器后,從服務器會為這個套接字連接關聯一個專門用于處理復制工作的文件事件處理器,處理后續的主服務器發送的RDB文件和傳播的命令
開始進行復制,從服務器向主服務器發送sync命令
主服務器接收到sync命令后,執行bgsave命令,主服務器主進程fork的子進程會生成一個RDB文件,同時將RDB快照產生后的所有寫操作記錄在緩沖區中
bgsave命令執行完成后,主服務器將生成的RDB文件發送給從服務器,從服務器接收到RDB文件后,首先會清除本身的全部數據,然后載入RDB文件,將自己的數據狀態更新成主服務器的RDB文件的數據狀態
主服務器將緩沖區的寫命令發送給從服務器,從服務器接收命令,并執行。
主從復制同步步驟完成
2.1.2 命令傳播
當同步工作完成之后,主從之間需要通過命令傳播來維持數據狀態的一致性。 如下圖,當前主從服務器之間完成同步工作之后,主服務接收客戶端的DEL K6指令后刪除了K6,此時從服務器仍然存在K6,主從數據狀態并不一致。為了維持主從服務器狀態一致,主服務器會將導致自己數據狀態發生改變的命令傳播到從服務器執行,當從服務器也執行了相同的命令之后,主從服務器之間的數據狀態將會保持一致。
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.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
正常同步的情況如下:
通過對比主從服務器之間的復制偏移量是否相等,能夠得知主從服務器之間的數據狀態是否保持一致。 假設此時A/B正常傳播,C從服務器斷線,那么將出現如下情況:
很明顯有了復制偏移量之后,從服務器C斷線重連后,主服務器只需要發送從服務器缺少的100字節數據即可。但是主服務器又是如何知道從服務器缺少的是那些數據呢?
2.2.2.2 復制積壓緩沖區
復制積壓緩沖區是一個固定長度的隊列,默認為1MB大小。當主服務器數據狀態發生改變,主服務器將數據同步給從服務器的同時會另存一份到復制積壓緩沖區中。
復制積壓緩沖區為了能和偏移量進行匹配,它不僅存儲了數據內容,還記錄了每個字節對應的偏移量:
當從服務器斷線重連后,從服務器通過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
一起完整的psync流程如下圖:
從服務器接收到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的數據是否仍在復制積壓緩沖區中)
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是否相等(是否同屬于上一個主服務器的從服務器),如果相等,仍然可以選擇增量/全量同步,如果不相等則只能進行全量同步。
主從復制奠定了Redis分布式的基礎,但是普通的主從復制并不能達到高可用的狀態。在普通的主從復制模式下,如果主服務器宕機,就只能通過運維人員手動切換主服務器,很顯然這種方案并不可取。 針對上述情況,Redis官方推出了可抵抗節點故障的高可用方案——Redis Sentinel(哨兵)。Redis Sentinel(哨兵):由一個或多個Sentinel實例組成的Sentinel系統,它可以監視任意多個主從服務器,當監視的主服務器宕機時,自動下線主服務器,并且擇優選取從服務器升級為新的主服務器。
如下示例:當舊Master下線時長超過用戶設定的下線時長上限,Sentinel系統就會對舊Master執行故障轉移操作,故障轉移操作包含三個步驟:
在Slave中選擇數據最新的作為新的Master
向其他Slave發送新的復制指令,讓其他從服務器成為新的Master的Slave
繼續監視舊Master,如果其上線則將舊Master設置為新Master的Slave
本文基于如下資源清單進行開展:
IP地址 | 節點角色 | 端口 |
---|---|---|
192.168.211.104 | Redis Master/ Sentinel | 6379/26379 |
192.168.211.105 | Redis Slave/ Sentinel | 6379/26379 |
192.168.211.106 | Redis Slave/ Sentinel | 6379/26379 |
Sentinel并沒有什么特別神奇的地方,它就是一個更加簡單的Redis服務器,在Sentinel啟動的時候它會加載不同的命令表和配置文件,因此從本質上來講Sentinel就是一個擁有較少命令和部分特殊功能的Redis服務。當一個Sentinel啟動時它需要經歷如下步驟:
初始化Sentinel服務器
替換普通Redis代碼為Sentinel的專用代碼
初始化Sentinel狀態
根據用戶給定的Sentinel配置文件,初始化Sentinel監視的主服務器列表
創建連接主服務器的網絡連接
根據主服務獲取從服務器信息,創建連接從服務器的網絡連接
根據發布/訂閱獲取Sentinel信息,創建Sentinel之間的網絡連接
Sentinel本質上就是一個Redis服務器,因此啟動Sentinel需要啟動一個Redis服務器,但是Sentinel并不需要讀取RDB/AOF文件來還原數據狀態。
Sentinel用于較少的Redis命令,大部分命令在Sentinel客戶端都不支持,并且Sentinel擁有一些特殊的功能,這些需要Sentinel在啟動時將Redis服務器使用的代碼替換為Sentinel的專用代碼。在此期間Sentinel會載入與普通Redis服務器不同的命令表。 Sentinel不支持SET、DBSIZE等命令;保留支持PING、PSUBSCRIBE、SUBSCRIBE、UNSUBSCRIBE、INFO等指令;這些指令在Sentinel工作中提供了保障。
裝載Sentinel的特有代碼之后,Sentinel會初始化sentinelState結構,該結構用于存儲Sentinel相關的狀態信息,其中最重要的就是masters字典。
struct sentinelState { //當前紀元,故障轉移使用 uint64_t current_epoch; // Sentinel監視的主服務器信息 // key -> 主服務器名稱 // value -> 指向sentinelRedisInstance指針 dict *masters; // ... } 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;
根據上面的一主二從配置將會得到如下實例結構:
當實例結構初始化完成之后,Sentinel將會開始創建連接Master的網絡連接,這一步Sentinel將成為Master的客戶端。 Sentinel和Master之間會創建一個命令連接和一個訂閱連接:
命令連接用于獲取主從信息
訂閱連接用于Sentinel之間進行信息廣播,每個Sentinel和自己監視的主從服務器之間會訂閱_sentinel_:hello頻道(注意Sentinel之間不會創建訂閱連接,它們通過訂閱_sentinel_:hello頻道來獲取其他Sentinel的初始信息)
Sentinel在創建命令連接完成之后,每隔10秒鐘向Master發送一次INFO指令,通過Master的回復信息可以獲得兩方面的知識:
Master本身的信息
Master下的Slave信息
根據主服務獲取從服務器信息,Sentinel可以創建到Slave的網絡連接,Sentinel和Slave之間也會創建命令連接和訂閱連接。
當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
此時實例結構信息如下所示:
此時是不是還有疑問,Sentinel之間是怎么互相發現對方并且相互通信的,這個就和上面Sentinel與自己監視的主從之間訂閱_sentinel_:hello頻道有關了。 Sentinel會與自己監視的所有Master和Slave之間訂閱_sentinel_:hello頻道,并且Sentinel每隔2秒鐘向_sentinel_:hello頻道發送一條消息,消息內容如下:
PUBLISH sentinel:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_ip>,<m_port>,<m_runid>,<m_epoch>"
其中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之間不會創建訂閱連接,它們只會創建命令連接:
此時實例結構信息如下所示:
Sentinel最主要的工作就是監視Redis服務器,當Master實例超出預設的時限后切換新的Master實例。這其中有很多細節工作,大致分為檢測Master是否主觀下線、檢測Master是否客觀下線、選舉領頭Sentinel、故障轉移四個步驟。
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
當前Sentinel認為其下線只能處于主觀下線狀態,要想判斷當前Master是否客觀下線,還需要詢問其他Sentinel,并且所有認為Master主觀下線或者客觀下線的總和需要達到quorum配置的值,當前Sentinel才會將Master標志為客觀下線。
當前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
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產生為止)。
故障轉移將會交給領頭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下線,則循環流程即可!
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博客
下文內容依賴下圖三主三從結構開展:
資源清單:
節點 | IP | 槽(slot)范圍 |
---|---|---|
Master[0] | 192.168.211.107:6319 | Slots 0 - 5460 |
Master[1] | 192.168.211.107:6329 | Slots 5461 - 10922 |
Master[2] | 192.168.211.107:6339 | Slots 10923 - 16383 |
Slave[0] | 192.168.211.107:6369 | |
Slave[1] | 192.168.211.107:6349 | |
Slave[2] | 192.168.211.107:6359 |
Redis集群.png
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等結構用于存儲集群模式下的特殊數據。
如下三個數據承載對象一定要認真看,尤其是結構中的注釋,看完之后集群大體上怎么工作的,心里就有數了,嘿嘿嘿;
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如下所示:
假設集群如上面我給出的資源清單,此時代表Master[0]的clusterNode的slots如下所示:
clusterLink是clsuterNode中的一個屬性,用于存儲連接節點所需的相關信息,比如套接字描述符、輸入輸出緩沖區等待,以下代碼列出部分非常重要的屬性:
typedef struct clusterState { // 連接創建時間 mstime_t ctime; // TCP 套接字描述符 int fd; // 輸出緩沖區,需要發送給其他節點的消息緩存在這里 sds sndbuf; // 輸入緩沖區,接收打其他節點的消息緩存在這里 sds rcvbuf; // 與當前clsuterNode節點代表的節點建立連接的其他節點保存在這里 struct clusterNode *node; } clusterState;
每個節點都會有一個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集群中使用兩個slots數組的原因是出于性能的考慮:
當我們需要獲取整個集群中clusterNode分別負責什么槽時,只需要查詢clusterState中的slots數組即可。如果沒有clusterState的slots數組,則需要遍歷所有的clusterNode結構,這樣顯然要慢一些
此外clusterNode中的slots數組也有存在的必要,因為集群中任意一個節點之間需要知道彼此負責的槽,此時節點之間只需要互相傳輸clusterNode中的slots數組結構就行。
第二個需要認真了解的結構是node字典,該結構雖然簡單,但是node字典中存儲了所有的clusterNode,這也是Redis集群中的單個節點獲取其他主節點、從節點信息的主要位置,因此我們也需要注意一下。 第三個需要認真了解的結構是importing_slots_from[16384]數組和migrating_slots_to[16384],這兩個數組在集群重新分片時需要使用,需要重點了解,后面再說吧,這里說的話順序不太對。
Redis集群一共16384個槽,如上資源清單我們在三主三從的集群中,每個主節點負責自己相應的槽,而在上面的三主三從部署的過程中并未看到我指定槽給對應的主節點,這是因為Redis集群自己內部給我們劃分了槽,但是如果我們想自己指派槽該如何整呢? 我們可以向節點發送如下命令,將一個或多個槽指派給當前節點負責:
CLUSTER ADDSLOTS
比如我們想把0和1槽指派給Master[0],我們只需要想Master[0]節點發送如下命令即可:
CLUSTER ADDSLOTS 0 1
當節點被指派了槽后,會將clusterNode的slots數組更新,節點會將自己負責處理的槽也就是slots數組通過消息發送給集群中的其他節點,其他節點在接收當消息后會更新對應clusterNode的slots數組以及clusterState的solts數組。
這個其實也比較簡單,當我們向Redis集群中的某個節點發送CLUSTER ADDSLOTS命令時,當前節點首先會通過clusterState中的slots數組來確認指派給當前節點的槽是否沒有指派給其他節點,如果已經指派了,那么會直接拋出異常,返回錯誤給指派的客戶端。如果指派給當前節點的所有槽都未指派給其他節點,那么當前節點會將這些槽指派給自己。 指派主要有三個步驟:
更新clusterState的slots數組,將指定槽slots[i]指向當前clusterNode
更新clusterNode的slots數組,將指定槽slots[i]處的值更新為1
向集群中的其他節點發送消息,將clusterNode的slots數組發送給其他節點,其他節點接收到消息后也更新對應的clusterState的slots數組和clusterNode的slots數組
在了解這個問題之前先要知道一個點,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集群的服務端和客戶端共同負責完成。
這個問題其實涵括了很多問題,比如移除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結構,然后節點通過消息傳播槽的指派信息,至此集群槽遷移工作完成,且集群中的其他節點也更新了新的槽分配信息。
優秀的你總會想到這種并發情況,牛皮呀!大佬們!
這個問題官方也考慮了,還記得我們在聊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命令之后就需要將請求轉向正確的節點了,不過這里有一點需要注意的地方**(因此我放個表情包在這里,方便讀者注意)。**
前面說了,當節點發現當前槽不屬于自己處理時會返回MOVED指令,那么在遷移中的槽時怎么處理的呢?這個Redis集群是這個玩的。 節點發現槽正在遷移則向客戶端返回ASKING命令,客戶端會接收到ASKING命令,其中包含了槽遷入的clusterNode的節點ip和port。那么客戶端首先會向遷入的clusterNode發送一條ASKING命令,這個命令必須要發目的是告訴當前節點,你要破例處理這次請求,因為這個槽已經遷移到你這里了,你不能直接拒絕我(因此如果Redis未接收到ASKING命令,會直接查詢節點的clusterState,而正在遷移中的槽還沒有更新到clusterState中,那么只能直接返回MOVED,這樣不就會一直循環很多次……),接收到ASKING命令的節點會強制執行一次這個請求(只執行一次,下次再來需要重新提前發送ASKING命令)。
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中主從復制、Sentinel、集群有什么用”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。