前言
Redis單例項的架構,從最開始的一主N從,到讀寫分離,再到Sentinel哨兵機制,單例項的Redis快取足以應對大多數的使用場景,也能實現主從故障遷移。
但是,在某些場景下,單例項存Redis快取會存在的幾個問題:
-
寫併發:Redis單例項讀寫分離可以解決讀操作的負載均衡,但對於寫操作,仍然是全部落在了master節點上面,在海量資料高併發場景,一個節點寫資料容易出現瓶頸,造成master節點的壓力上升。
-
海量資料的儲存壓力:單例項Redis本質上只有一臺Master作為儲存,如果面對海量資料的儲存,一臺Redis的伺服器就應付不過來了,而且資料量太大意味著持久化成本高,嚴重時可能會阻塞伺服器,造成服務請求成功率下降,降低服務的穩定性。
針對以上的問題,Redis叢集提供了較為完善的方案,解決了儲存能力受到單機限制,寫操作無法負載均衡的問題。
Redis提供了去中心化的叢集部署模式,叢集內所有Redis節點之間兩兩連線,而很多的客戶端工具會根據key將請求分發到對應的分片下的某一個節點上進行處理。Redis也內建了高可用機制,支援N個master節點,每個master節點都可以掛載多個slave節點,當master節點掛掉時,叢集會提升它的某個slave節點作為新的master節點。
預設情況下,redis叢集的讀和寫都是到master上去執行的,不支援slave節點讀和寫,跟Redis主從複製下讀寫分離不一樣,因為redis叢集的核心的理念,主要是使用slave做資料的熱備,以及master故障時的主備切換,實現高可用的。Redis的讀寫分離,是為了橫向任意擴充套件slave節點去支撐更大的讀吞吐量。而redis叢集架構下,本身master就是可以任意擴充套件的,如果想要支撐更大的讀或寫的吞吐量,都可以直接對master進行橫向擴充套件。
一個典型的Redis叢集部署場景如下圖所示:
在Redis叢集裡面,又會劃分出分割槽的概念,一個叢集中可有多個分割槽。分割槽有幾個特點:
- 同一個分割槽內的Redis節點之間的資料完全一樣,多個節點保證了資料有多份副本冗餘儲存,且可以提供高可用保障。
- 不同分片之間的資料不相同。
- 透過水平增加多個分片的方式,可以實現整體叢集的容量的擴充套件。
按照Cluster模式進行部署的時候,要求最少需要部署6個Redis節點(3個分片,每個分片中1主1從),其中叢集中每個分片的master節點負責對外提供讀寫操作,slave節點則作為故障轉移使用(master出現故障的時候充當新的master)、對外提供只讀請求處理。
1 叢集資料分佈策略
1.1 Redis Sharding(資料分片)
在Redis Cluster前,為了解決資料分發到各個分割槽的問題,普遍採用的是Redis Sharding(資料分片)方案。所謂的Sharding,其實就是一種資料分發的策略。根據key的hash值進行取模,確定最終歸屬的節點。
優點就是比較簡單,但是
-
擴容或者摘除節點時需要重新根據對映關係計算,會導致資料重新遷移。
-
叢集擴容的時候,會導致請求被分發到錯誤節點上,導致快取命中率降低。
如果需要解決這個問題,就需要對原先擴容前已經儲存的資料重新進行一次hash計算和取模操作,將全部的資料重新分發到新的正確節點上進行儲存。這個操作被稱為重新Sharding,重新sharding期間服務不可用,可能會對業務造成影響。
1.2 一致性Hash
為了降低節點的增加或者移除對於整體已有快取資料訪問的影響,最大限度的保證快取命中率,改良後的一致性Hash演算法浮出水面。
1.3 Hash槽
何為Hash槽?Hash槽的原理與HashMap有點相似,Redis叢集中有16384個雜湊槽(槽的範圍是 0 -16383,雜湊槽),將不同的雜湊槽分佈在不同的Redis節點上面進行管理,也就是說每個Redis節點只負責一部分的雜湊槽。在對資料進行操作的時候,叢集會對使用CRC16演算法對key進行計算並對16384取模(slot = CRC16(key)%16383),得到的結果就是 Key-Value 所放入的槽,透過這個值,去找到對應的槽所對應的Redis節點,然後直接到這個對應的節點上進行存取操作。
使用雜湊槽的好處就在於可以方便的新增或者移除節點,並且無論是新增刪除或者修改某一個節點,都不會造成叢集不可用的狀態。
- 當需要增加節點時,只需要把其他節點的某些雜湊槽挪到新節點就可以了;
- 當需要移除節點時,只需要把移除節點上的雜湊槽挪到其他節點就行了;
雜湊槽資料分割槽演算法具有以下幾種特點:
- 解耦資料和節點之間的關係,簡化了擴容和收縮難度;
- 節點自身維護槽的對映關係,不需要客戶端代理服務維護槽分割槽後設資料
- 支援節點、槽、鍵之間的對映查詢,用於資料路由,線上伸縮等場景
為什麼redis叢集採用“hash槽”來解決資料分配問題,而不採用“一致性hash”演算法呢?
- 一致性雜湊的節點分佈基於圓環,無法很好的手動控制資料分佈,比如有些節點的硬體差,希望少存一點資料,這種很難操作(還得透過虛擬節點對映,總之較繁瑣)。
- 而redis叢集的槽位空間是可以使用者手動自定義分配的,類似於 windows 盤分割槽的概念,可以手動控制大小。
- 其實,無論是 “一致性雜湊” 還是 “hash槽” 的方式,在增減節點的時候,都會對一部分資料產生影響,都需要我們遷移資料。當然,redis叢集也提供了相關手動遷移槽資料的命令。
2 Hash槽原理詳解
2.1 clusterNode
儲存節點的當前狀態,比如節點的建立時間,節點的名字,節點當前的配置紀元,節點的IP和地址,等等。
typedef struct clusterNode {
//建立節點的時間
mstime_t ctime;
//節點的名字,由40個16進位制字元組成
char name[CLUSTER_NAMELEN];
//節點標識 使用各種不同的標識值記錄節點的角色(比如主節點或者從節點)
char shard_id[CLUSTER_NAMELEN];
//以及節點目前所處的狀態(比如線上或者下線)
int flags;
//節點當前的配置紀元,用於實現故障轉移
uint64_t configEpoch;
//由這個節點負責處理的槽
//一共有REDISCLUSTER SLOTS/8個位元組長 每個位元組的每個位記錄了一個槽的儲存狀態
// 位的值為 1 表示槽正由本節點處理,值為0則表示槽並非本節點處理
unsigned char slots[CLUSTER_SLOTS/8];
uint16_t *slot_info_pairs; /* Slots info represented as (start/end) pair (consecutive index). */
int slot_info_pairs_count; /* Used number of slots in slot_info_pairs */
//該節點負責處理的槽數里
int numslots;
//如果本節點是主節點,那麼用這個屬性記錄從節點的數里
int numslaves;
//指標陣列,指向各個從節點
struct clusterNode **slaves;
//如果這是一個從節點,那麼指向主節點
struct clusterNode *slaveof;
unsigned long long last_in_ping_gossip; /* The number of the last carried in the ping gossip section */
//最後一次傳送 PING 命令的時間
mstime_t ping_sent;
//最後一次接收 PONG 回覆的時間戳
mstime_t pong_received;
mstime_t data_received;
//最後一次被設定為 FAIL 狀態的時間
mstime_t fail_time;
// 最後一次給某個從節點投票的時間
mstime_t voted_time;
//最後一次從這個節點接收到複製偏移裡的時間
mstime_t repl_offset_time;
mstime_t orphaned_time;
//這個節點的複製偏移裡
long long repl_offset;
//節點的 IP 地址
char ip[NET_IP_STR_LEN];
sds hostname; /* The known hostname for this node */
//節點的埠號
int port;
int pport; /* Latest known clients plaintext port. Only used
if the main clients port is for TLS. */
int cport; /* Latest known cluster port of this node. */
//儲存連線節點所需的有關資訊
clusterLink *link;
clusterLink *inbound_link; /* TCP/IP link accepted from this node */
//連結串列,記錄了所有其他節點對該節點的下線報告
list *fail_reports;
} clusterNode;
2.2 clusterState
記錄當前節點所認為的叢集目前所處的狀態。
typedef struct clusterState {
//指向當前節點的指標
clusterNode *myself;
//叢集當前的配置紀元,用於實現故障轉移
uint64_t currentEpoch;
// 叢集當前的狀態:是線上還是下線
int state;
//叢集中至少處理著一個槽的節點的數量。
int size;
//叢集節點名單(包括 myself 節點)
//字典的鍵為節點的名字,字典的值為 clusterWode 結構
dict *nodes;
dict *shards; /* Hash table of shard_id -> list (of nodes) structures */
//節點黑名單,用於CLUSTER FORGET 命令
//防止被 FORGET 的命令重新被新增到叢集裡面
dict *nodes_black_list;
//記錄要從當前節點遷移到目標節點的槽,以及遷移的目標節點
//migrating_slots_to[i]= NULL 表示槽 i 未被遷移
//migrating_slots_to[i]= clusterNode_A 表示槽i要從本節點遷移至節點 A
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
//記錄要從源節點遷移到本節點的槽,以及進行遷移的源節點
//importing_slots_from[i]= NULL 表示槽 i 未進行匯入
//importing_slots from[i]=clusterNode A 表示正從節點A中匯入槽 i
clusterNode *importing_slots_from[CLUSTER_SLOTS];
//負責處理各個槽的節點
//例如 slots[i]=clusterNode_A 表示槽i由節點A處理
clusterNode *slots[CLUSTER_SLOTS];
rax *slots_to_channels;
//以下這些值被用於進行故障轉移選舉
//上次執行選舉或者下次執行選舉的時間
mstime_t failover_auth_time;
//節點獲得的投票數量
int failover_auth_count;
//如果值為 1,表示本節點已經向其他節點傳送了投票請求
int failover_auth_sent;
int failover_auth_rank; /* This slave rank for current auth request. */
uint64_t failover_auth_epoch; /* Epoch of the current election. */
int cant_failover_reason; /* Why a slave is currently not able to
failover. See the CANT_FAILOVER_* macros. */
/*共用的手動故障轉移狀態*/
//手動故障轉移執行的時間限制
mstime_t mf_end;
/*主伺服器的手動故障轉移狀態 */
clusterNode *mf_slave;
/*叢伺服器的手動故障轉移狀態 */
long long mf_master_offset;
// 指示手動故障轉移是否可以開始的標誌值 值為非 0 時表示各個主伺服器可以開始投票
int mf_can_start;
/*以下這些值由主伺服器使用,用於記錄選舉時的狀態*/
//叢集最後一次進行投票的紀元
uint64_t lastVoteEpoch;
//在進入下個事件迴圈之前要做的事情,以各個 flag 來記錄
int todo_before_sleep;
/* Stats */
//透過 cluster 連線傳送的訊息數量
long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
//透過cluster 接收到的訊息數量
long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
long long stats_pfail_nodes; /* Number of nodes in PFAIL status,
excluding nodes without address. */
unsigned long long stat_cluster_links_buffer_limit_exceeded; /* Total number of cluster links freed due to exceeding buffer limit */
} clusterState;
2.3 節點的槽指派資訊
clusterNode資料結構的slots屬性和numslot屬性記錄了節點負責處理那些槽:
slots屬性是一個二進位制位陣列(bit array),這個陣列的長度為16384/8=2048個位元組,共包含16384個二進位制位。Master節點用bit來標識對於某個槽自己是否擁有,時間複雜度為O(1)
2.4 叢集所有槽的指派資訊
當收到叢集中其他節點傳送的資訊時,透過將節點槽的指派資訊儲存在本地的clusterState.slots陣列裡面,程式要檢查槽i是否已經被指派,又或者取得負責處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,時間複雜度僅為O(1)
如上圖所示,ClusterState 中儲存的 Slots 陣列中每個下標對應一個槽,每個槽資訊中對應一個 clusterNode 也就是快取的節點。這些節點會對應一個實際存在的 Redis 快取服務,包括 IP 和 Port 的資訊。Redis Cluster 的通訊機制實際上保證了每個節點都有其他節點和槽資料的對應關係。無論Redis 的客戶端訪問叢集中的哪個節點都可以路由到對應的節點上,因為每個節點都有一份 ClusterState,它記錄了所有槽和節點的對應關係。
3 叢集的請求重定向
Redis叢集在客戶端層面是沒有采用代理的,並且無論Redis 的客戶端訪問叢集中的哪個節點都可以路由到對應的節點上,下面來看看 Redis 客戶端是如何透過路由來呼叫快取節點的:
3.1 MOVED請求
如上圖所示,Redis 客戶端透過 CRC16(key)%16383 計算出 Slot 的值,發現需要找“快取節點1”進行資料操作,但是由於快取資料遷移或者其他原因導致這個對應的 Slot 的資料被遷移到了“快取節點2”上面。那麼這個時候 Redis 客戶端就無法從“快取節點1”中獲取資料了。但是由於“快取節點1”中儲存了所有叢集中快取節點的資訊,因此它知道這個 Slot 的資料在“快取節點2”中儲存,因此向 Redis 客戶端傳送了一個 MOVED 的重定向請求。這個請求告訴其應該訪問的“快取節點2”的地址。Redis 客戶端拿到這個地址,繼續訪問“快取節點2”並且拿到資料。
3.2 ASK請求
上面的例子說明了,資料 Slot 從“快取節點1”已經遷移到“快取節點2”了,那麼客戶端可以直接找“快取節點2”要資料。那麼如果兩個快取節點正在做節點的資料遷移,此時客戶端請求會如何處理呢?
Redis 客戶端向“快取節點1”發出請求,此時“快取節點1”正向“快取節點 2”遷移資料,如果沒有命中對應的 Slot,它會返回客戶端一個 ASK 重定向請求並且告訴“快取節點2”的地址。客戶端向“快取節點2”傳送 Asking 命令,詢問需要的資料是否在“快取節點2”上,“快取節點2”接到訊息以後返回資料是否存在的結果。
3.3 頻繁重定向造成的網路開銷的處理:smart客戶端
什麼是 smart客戶端:
在大部分情況下,可能都會出現一次請求重定向才能找到正確的節點,這個重定向過程顯然會增加叢集的網路負擔和單次請求耗時。所以大部分的客戶端都是smart的。所謂 smart客戶端,就是指客戶端本地維護一份hashslot => node的對映表快取,大部分情況下,直接走本地快取就可以找到hashslot => node,不需要透過節點進行moved重定向,
JedisCluster的工作原理:
- 在JedisCluster初始化的時候,就會隨機選擇一個node,初始化hashslot => node對映表,同時為每個節點建立一個JedisPool連線池。
- 每次基於JedisCluster執行操作時,首先會在本地計算key的hashslot,然後在本地對映表找到對應的節點node。
- 如果那個node正好還是持有那個hashslot,那麼就ok;如果進行了reshard操作,可能hashslot已經不在那個node上了,就會返回moved。
- 如果JedisCluter API發現對應的節點返回moved,那麼利用該節點返回的後設資料,更新本地的hashslot => node對映表快取
- 重複上面幾個步驟,直到找到對應的節點,如果重試超過5次,那麼就報錯,JedisClusterMaxRedirectionException
hashslot遷移和ask重定向:
如果hashslot正在遷移,那麼會返回ask重定向給客戶端。客戶端接收到ask重定向之後,會重新定位到目標節點去執行,但是因為ask發生在hashslot遷移過程中,所以JedisCluster API收到ask是不會更新hashslot本地快取。
雖然ASK與MOVED都是對客戶端的重定向控制,但是有本質區別。ASK重定向說明叢集正在進行slot資料遷移,客戶端無法知道遷移什麼時候完成,因此只能是臨時性的重定向,客戶端不會更新slots快取。但是MOVED重定向說明鍵對應的槽已經明確指定到新的節點,客戶端需要更新slots快取。
4 Redis叢集中節點的通訊機制:goosip協議
redis叢集的雜湊槽演算法解決的是資料的存取問題,不同的雜湊槽位於不同的節點上,而不同的節點維護著一份它所認為的當前叢集的狀態,同時,Redis叢集是去中心化的架構。那麼,當叢集的狀態發生變化時,比如新節點加入、slot遷移、節點當機、slave提升為新Master等等,我們希望這些變化儘快被其他節點發現,Redis是如何進行處理的呢?也就是說,Redis不同節點之間是如何進行通訊進行維護叢集的同步狀態呢?
在Redis叢集中,不同的節點之間採用gossip協議進行通訊,節點之間通訊的目的是為了維護節點之間的後設資料資訊。這些後設資料就是每個節點包含哪些資料,是否出現故障,透過gossip協議,達到最終資料的一致性。
gossip協議,是基於流行病傳播方式的節點或者程序之間資訊交換的協議。原理就是在不同的節點間不斷地通訊交換資訊,一段時間後,所有的節點就都有了整個叢集的完整資訊,並且所有節點的狀態都會達成一致。每個節點可能知道所有其他節點,也可能僅知道幾個鄰居節點,但只要這些節可以透過網路連通,最終他們的狀態就會是一致的。Gossip協議最大的好處在於,即使叢集節點的數量增加,每個節點的負載也不會增加很多,幾乎是恆定的。
Redis叢集中節點的通訊過程如下:
- 叢集中每個節點都會單獨開一個TCP通道,用於節點間彼此通訊。
- 每個節點在固定週期內透過待定的規則選擇幾個節點傳送ping訊息
- 接收到ping訊息的節點用pong訊息作為響應
使用gossip協議的優點在於將後設資料的更新分散在不同的節點上面,降低了壓力;但是缺點就是後設資料的更新有延時,可能導致叢集中的一些操作會有一些滯後。另外,由於 gossip 協議對伺服器時間的要求較高,時間戳不準確會影響節點判斷訊息的有效性。而且節點數量增多後的網路開銷也會對伺服器產生壓力,同時結點數太多,意味著達到最終一致性的時間也相對變長,因此官方推薦最大節點數為1000左右。
redis cluster架構下的每個redis都要開放兩個埠號,比如一個是6379,另一個就是加1w的埠號16379。
- 6379埠號就是redis伺服器入口。
- 16379埠號是用來進行節點間通訊的,也就是 cluster bus 的東西,cluster bus 的通訊,用來進行故障檢測、配置更新、故障轉移授權。cluster bus 用的就是gossip 協議
5 叢集的擴容與收縮
作為分散式部署的快取節點總會遇到快取擴容和快取故障的問題。這就會導致快取節點的上線和下線的問題。由於每個節點中儲存著槽資料,因此當快取節點數出現變動時,這些槽資料會根據對應的虛擬槽演算法被遷移到其他的快取節點上。所以對於redis叢集,叢集伸縮主要在於槽和資料在節點之間移動。
5.1 擴容
- 啟動新節點
- 使用cluster meet命令將新節點加入到叢集
- 遷移槽和資料:新增新節點後,需要將一些槽和資料從舊節點遷移到新節點
如上圖所示,叢集中本來存在“快取節點1”和“快取節點2”,此時“快取節點3”上線了並且加入到叢集中。此時根據虛擬槽的演算法,“快取節點1”和“快取節點2”中對應槽的資料會應該新節點的加入被遷移到“快取節點3”上面。
新節點加入到叢集的時候,作為孤兒節點是沒有和其他節點進行通訊的。因此需要在叢集中任意節點執行 cluster meet 命令讓新節點加入進來。假設新節點是 192.168.1.1 5002,老節點是 192.168.1.1 5003,那麼執行以下命令將新節點加入到叢集中。
192.168.1.1 5003> cluster meet 192.168.1.1 5002
這個是由老節點發起的,有點老成員歡迎新成員加入的意思。新節點剛剛建立沒有建立槽對應的資料,也就是說沒有快取任何資料。如果這個節點是主節點,需要對其進行槽資料的擴容;如果這個節點是從節點,就需要同步主節點上的資料。總之就是要同步資料。
如上圖所示,由客戶端發起節點之間的槽資料遷移,資料從源節點往目標節點遷移。
- 客戶端對目標節點發起準備匯入槽資料的命令,讓目標節點準備好匯入槽資料。這裡使用 cluster setslot {slot} importing {sourceNodeId} 命令。
- 之後對源節點發起送命令,讓源節點準備遷出對應的槽資料。使用命令 cluster setslot {slot} importing {sourceNodeId}。
- 此時源節點準備遷移資料了,在遷移之前把要遷移的資料獲取出來。透過命令 cluster getkeysinslot {slot} {count}。Count 表示遷移的 Slot 的個數。
- 然後在源節點上執行,migrate {targetIP} {targetPort} “” 0 {timeout} keys {keys} 命令,把獲取的鍵透過流水線批次遷移到目標節點。
- 重複 3 和 4 兩步不斷將資料遷移到目標節點。
- 完成資料遷移到目標節點以後,透過 cluster setslot {slot} node {targetNodeId} 命令通知對應的槽被分配到目標節點,並且廣播這個資訊給全網的其他主節點,更新自身的槽節點對應表。
5.2 收縮
- 遷移槽。
- 忘記節點。透過命令 cluster forget {downNodeId} 通知其他的節點
為了安全刪除節點,Redis叢集只能下線沒有負責槽的節點。因此如果要下線有負責槽的master節點,則需要先將它負責的槽遷移到其他節點。遷移的過程也與上線操作類似,不同的是下線的時候需要通知全網的其他節點忘記自己,此時透過命令 **cluster forget {downNodeId} **通知其他的節點。
6 叢集的故障檢測與故障轉恢復機制:
6.1 叢集的故障檢測
Redis叢集的故障檢測是基於gossip協議的,叢集中的每個節點都會定期地向叢集中的其他節點傳送PING訊息,以此交換各個節點狀態資訊,檢測各個節點狀態:線上狀態、疑似下線狀態PFAIL、已下線狀態FAIL。
6.2 主觀下線(pfail)
當節點A檢測到與節點B的通訊時間超過了cluster-node-timeout 的時候,就會更新本地節點狀態,把節點B更新為主觀下線。
主觀下線並不能代表某個節點真的下線了,有可能是節點A與節點B之間的網路斷開了,但是其他的節點依舊可以和節點B進行通訊。
6.3 客觀下線
由於叢集內的節點會不斷地與其他節點進行通訊,下線資訊也會透過 Gossip 訊息傳遍所有節點,因此叢集內的節點會不斷收到下線報告。
當半數以上的主節點標記了節點B是主觀下線時,便會觸發客觀下線的流程(該流程只針對主節點,如果是從節點就會忽略)。將主觀下線的報告儲存到本地的 ClusterNode 的結構fail_reports連結串列中,並且對主觀下線報告的時效性進行檢查,如果超過 cluster-node-timeout*2 的時間,就忽略這個報告,否則就記錄報告內容,將其標記為客觀下線。
接著向叢集廣播一條主節點B的Fail 訊息,所有收到訊息的節點都會標記節點B為客觀下線。
6.4 叢集地故障恢復
當故障節點下線後,如果是持有槽的主節點則需要在其從節點中找出一個替換它,從而保證高可用。此時下線主節點的所有從節點都擔負著恢復義務,這些從節點會定時監測主節點是否進入客觀下線狀態,如果是,則觸發故障恢復流程。故障恢復也就是選舉一個節點充當新的master,選舉的過程是基於Raft協議選舉方式來實現的。
-
從節點過濾:檢查每個slave節點與master節點斷開連線的時間,如果超過了cluster-node-timeout * cluster-slave-validity-factor,那麼就沒有資格切換成master
-
投票選舉:
-
節點排序:對透過過濾條件的所有從節點進行排序,按照priority、offset、run id排序,排序越靠前的節點,越優先進行選舉。 (在這步排序領先的從節點通常會獲得更多的票,因為它觸發選舉的時間更早一些,獲得票的機會更大)
-
priority的值越低,優先順序越高
-
offset越大,表示從master節點複製的資料越多,選舉時間越靠前,優先進行選舉
-
如果offset相同,run id越小,優先順序越高
-
-
更新配置紀元:每個主節點會去更新配置紀元(clusterNode.configEpoch),這個值是不斷增加的整數。這個值記錄了每個節點的版本和整個叢集的版本。每當發生重要事情的時候(例如:出現新節點,從節點精選)都會增加全域性的配置紀元並且賦給相關的主節點,用來記錄這個事件。更新這個值目的是,保證所有主節點對這件“大事”保持一致,大家都統一成一個配置紀元,表示大家都知道這個“大事”了。
-
發起選舉:更新完配置紀元以後,從節點會向叢集發起廣播選舉的訊息(CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST),要求所有收到這條訊息,並且具有投票權的主節點進行投票。每個從節點在一個紀元中只能發起一次選舉。
14 選舉投票:如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,表示這個主節點支援從節點成為新的主節點。每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,並根據自己收到了多少條這種訊息來統計自己獲得了多少主節點的支援。如果超過(N/2 + 1)數量的master節點都投票給了某個從節點,那麼選舉透過,這個從節點可以切換成master,如果在 cluster-node-timeout*2 的時間內從節點沒有獲得足夠數量的票數,本次選舉作廢,更新配置紀元,並進行第二輪選舉,直到選出新的主節點為止。
-
-
替換主節點:當滿足投票條件的從節點被選出來以後,會觸發替換主節點的操作。刪除原主節點負責的槽資料,把這些槽資料新增到自己節點上,並且廣播讓其他的節點都知道這件事情,新的主節點誕生了。
- 被選中的從節點執行SLAVEOF NO ONE命令,使其成為新的主節點
- 新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽全部指派給自己
- 新的主節點對叢集進行廣播PONG訊息,告知其他節點已經成為新的主節點
- 的主節點開始接收和處理槽相關的請求
- 備註:如果叢集中某個節點的master和slave節點都當機了,那麼叢集就會進入fail狀態,因為叢集的slot對映不完整。如果叢集超過半數以上的master掛掉,無論是否有slave,叢集都會進入fail狀態。
7 Redis叢集的運維
7.1 資料遷移問題
Redis叢集可以進行節點的動態擴容縮容,這一過程目前還處於半自動狀態,需要人工介入。在擴縮容的時候,需要進行資料遷移。而 Redis為了保證遷移的一致性,遷移所有操作都是同步操作,執行遷移時,兩端的 Redis均會進入時長不等的阻塞狀態,對於小Key,該時間可以忽略不計,但如果一旦Key的記憶體使用過大,嚴重的時候會接觸發叢集內的故障轉移,造成不必要的切換。
7.2 頻寬消耗問題
Redis叢集是無中心節點的叢集架構,依靠Gossip協議協同自動化修復叢集的狀態,但goosip有訊息延時和訊息冗餘的問題,在叢集節點數量過多的時候,goosip協議通訊會消耗大量的頻寬,主要體現在以下幾個方面:
- 訊息傳送頻率:跟cluster-node-timeout密切相關,當節點發現與其他節點的最後通訊時間超過 cluster-node-timeout/2時會直接傳送ping訊息
- 訊息資料量:每個訊息主要的資料佔用包含:slots槽陣列(2kb)和整個叢集1/10的狀態資料
- 節點部署的機器規模:機器的頻寬上限是固定的,因此相同規模的叢集分佈的機器越多,每臺機器劃分的節點越均勻,則整個叢集內整體的可用頻寬越高
也就是說,每個節點的slot不能有太多,否則叢集節點之間互相通訊時,redis會有大量的時間和頻寬在完成通訊
叢集頻寬消耗主要分為:讀寫命令消耗+Gossip訊息消耗,因此搭建Redis叢集需要根據業務資料規模和訊息通訊成本做出合理規劃:
- 在滿足業務需求的情況下儘量避免大叢集,同一個系統可以針對不同業務場景拆分使用若干個叢集。
- 適度提供cluster-node-timeout降低訊息傳送頻率,但是cluster-node-timeout還影響故障轉移的速度,因此需要根據自身業務場景兼顧二者平衡
- 如果條件允許儘量均勻部署在更多機器上,避免集中部署。如果有60個節點的叢集部署在3臺機器上每臺20個節點,這是機器的頻寬消耗將非常嚴重
7.3 Pub/Sub廣播問題
叢集模式下內部對所有publish命令都會向所有節點進行廣播,加重頻寬負擔,所以叢集應該避免頻繁使用Pub/sub功能
7.4 叢集傾斜
叢集傾斜是指不同節點之間資料量和請求量出現明顯差異,這種情況將加大負載均衡和開發運維的難度。因此需要理解叢集傾斜的原因
-
資料傾斜:
-
節點和槽分配不均
-
不同槽對應鍵數量差異過大
-
集合物件包含大量元素
-
記憶體相關配置不一致
-
-
請求傾斜:
- 合理設計鍵,熱點大集合物件做拆分或者使用hmget代替hgetall避免整體讀取
7.5 叢集讀寫分離
叢集模式下讀寫分離成本比較高,直接擴充套件主節點數量來提高叢集效能是更好的選擇。
出自:https://www.cnblogs.com/seven97-top/p/18587487