拆解Redis Cluster,怎么實現“寫安全”這個重要特性?
Redis是非常流行的緩存。在Redis升級到3.0版本后,升級到集群版本,被稱之為Redis Cluster。在集群版本中,會將數據分成多份,被保存到多個server中,從而保證集群的水平擴展能力,加之每份數據保存多個副本,從而保證可用性,并且集群版本保證一定程度的Write Safety。本文詳細介紹Redis Cluster的實現細節(jié),從而分析Redis Cluster的Write Safety的保證程度。
一、接口和架構
1、接口
Redis Cluster的接口基本向前兼容,仍然是key-value類型。
2、架構
Redis Cluster包含server和client兩個組件。一個Redis Cluster可以包含多個server,可以包含多個客戶端。每個客戶端可以連接任意的server,讀取寫入數據。保存在Redis Cluster中的數據會被分成多份,分散地保存在多個server中,并且每一份數據也會保存多個副本。
二、實現
1、節(jié)點
在Redis Cluster中,數據會被保存到多個Redis server中,每個Redis server都是一個獨立的進程,具有獨立的IP和Port,也被稱之為一個實例,或者叫做節(jié)點(Node)。Client通過這個IP和Port連接到這個Node。
每個節(jié)點都有個node id,node id是一個全局唯一的標識,它是在集群創(chuàng)建時隨機生成的。一個節(jié)點的id始終都不會變化的,但是node的IP和port是可以變化的。
2、Node table
一個Redis Cluster包含哪些節(jié)點,也就是這個集群包含哪些節(jié)點的id,也就是集群成員關系,這個信息被保存在一個表結構中,被稱之為node table。node table類似于:
node table會在每個節(jié)點上都保存一份,Redis Cluster通過gossip協議把node table復制到所有的節(jié)點。后面會繼續(xù)講述node table的復制。
3、集群成員關系變更
當添加一個節(jié)點或者刪除一個節(jié)點時,只需要將命令發(fā)給集群中的任意一個節(jié)點,這個節(jié)點會修改本地的node table,并且這個修改會最終復制到所有的節(jié)點上去。添加節(jié)點的命令是CLUSTER MEET,刪除節(jié)點的命令是CLUSTER FORGET。
舉例來說明:
- 打算搭建一個3個master節(jié)點的集群,當集群創(chuàng)建以前,所有3個節(jié)點的node table都只包含自己。給其中的一個節(jié)點A發(fā)送命令,CLUSTER MEET NodeB,節(jié)點A修改自己的node table,將NodeB添加到自己的node table中,并且連接節(jié)點B,把自己的node table發(fā)送給節(jié)點B,節(jié)點B收到節(jié)點A發(fā)送過來的node table,會更新自己的node table,這時節(jié)點B就知道集群中還有節(jié)點A存在。
這時,給節(jié)點A再發(fā)送CLUSTER MEET NodeC,節(jié)點A會把節(jié)點C添加到自己的node table,并且把自己的node table復制給節(jié)點B,節(jié)點B把接收到的node table更新自己的本地的node table,從而知道節(jié)點C的加入。同樣節(jié)點A會把自己的node table發(fā)給節(jié)點C,節(jié)點C會更新自己本地的node table,從而知道要加入的集群中已經存在節(jié)點A和節(jié)點B。
4、槽
前面說過Redis Cluster會把數據分成多份,也就是把數據進行分片。Redis Cluster中的每一份數據被稱為槽(Slot)。Redis Cluster將數據拆分成16384份,也就是說有16384個槽。
Redis Cluster采用哈希(Hash)機制來拆分數據。首先,數據的key通過CRC16算法計算出一個哈希值。這個哈希值再對16384取余,這個余數就是槽位,被稱為hash slot。具體的CRC16算法可以參看Redis官方文檔。所有余數相同的key都在一個slot中,也就是說,一個slot其實就是一批hash余數相同的key。
每個hash slot都會保存在Redis Cluster一個節(jié)點中。具體哪個hash slot被保存在哪個實例中,就形成了類似于一個map的數據結構,被稱之為hash slot map。hash slot map類似于:
- 0 -> NodeA
- 1 -> NodeA
- 2 -> NodeB
- ...
- 16383 -> NodeN
與node table相同,hash slot map也會在每個節(jié)點上都會保存一份,Redis Cluster通過gossip協議把hash slot map復制到所有節(jié)點。同樣,后面還會講述hash slot map的復制。
《Redis官方文檔》Redis Cluster Specification, https://redis.io/topics/cluster-spec.
5、數據分片變更
要修改數據分片關系,可以連接任意一個節(jié)點,給這個節(jié)點發(fā)送CLUSTER ADDSLOTS, CLUSTER SETSLOT, CLUSTER DELSLOT命令,修改這個節(jié)點上的hash slot map,該節(jié)點會把這個修改復制到所有其他節(jié)點,其他節(jié)點會用接收到的hash slot map更新自己的hash slot map。
CLUSTER ADDSLOTS、CLUSTER DELSLOTS、CLUSTER SETSLOT命令的使用如下:
- CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
- CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
- CLUSTER SETSLOT slot NODE node
CLUSTER ADDSLOTS用來把多個slot分配給當前連接的節(jié)點。例如,連接到節(jié)點A執(zhí)行:
- CLUSTER ADDSLOTS 1 2 3
這個命令會把slot1、slot2、slot3分配給節(jié)點A。
CLUSTER DELSLOTS用來把多個slot從當前連接的節(jié)點刪除。例如,連接到節(jié)點A執(zhí)行:
- CLUSTER DELSLOTS 1 2 3
這個命令會把slot1、slot2、slot3從節(jié)點A上刪除。
CLUSTER SETSLOT用來把一個slot分配給指定的節(jié)點,可以不是當前連接的節(jié)點,另外這個命令還可以設定MIGRATING和IMPORTING兩個狀態(tài),我們后面再講。例如,連接到節(jié)點A執(zhí)行:
- CLUSTER SETSLOT 1 nodeB
這個命令會把slot1分配給節(jié)點B。
6、Slave
一個slot會被保存多個副本,既一個slot會保存在多個節(jié)點上,也就是slot會復制到多個節(jié)點上。Redis Cluster的復制是以節(jié)點為單位的,一個節(jié)點上的所有slot會采用相同的復制。
具體來說就是,其中一個節(jié)點會負責處理這個節(jié)點上所有slot的寫操作,這個節(jié)點被稱為master,而其余的節(jié)點被稱為slave節(jié)點。一個master可以有多個slave。在同一個節(jié)點上的所有slot的所有的寫操作都會被從master節(jié)點異步復制到所有的slave節(jié)點。所以slave會具有與master相同的slot。
通過SLAVEOF命令來設置slave節(jié)點。SLAVEOF命令用來改變一個slave節(jié)點的復制設置。SLAVEOF命令有兩種格式:
- SLAVEOF NO ONE
- SLAVEOF host port
具體來講,SLAVEOF NO ONE命令會停止一個slave節(jié)點的復制,并且把這個slave節(jié)點變成master節(jié)點。SLAVEOF host port命令會停止一個slave節(jié)點的復制,丟棄數據集,開始從host和port指定的新master節(jié)點復制。
master和slave的關系會被記錄在hash slot table中,相當于一個slot會映射到多個節(jié)點上,其中一個節(jié)點是master,其他記錄的節(jié)點是slave。
加入了master/slave信息后的hash slot map類似于:
- 0 -> NodeA,NadeA1(slave)
- 1 -> NodeA,NadeA1(slave)
- 2 -> NodeB,NadeB1(slave)
- ...
- 16383 -> NodeN,NadeN1(slave)
作為hash slot map的一部分,master/slave信息也會通過gossip協議復制到集群中的每個節(jié)點。
7、Configuration
node table和hash slot map這兩個信息,被稱之為Configuration,在其他的分布式系統中,也被稱之為元數據。
前面我們講過,hash slot map和node table在每個節(jié)點都會保存一份,并且對這兩個信息的任何改動,都會通過gossip協議傳播(propogate)到所有的節(jié)點。本文我們就不繼續(xù)展開gossip協議了,對Redis Cluster的gossip協議的實現感興趣的同學可以參看redis的文檔。
在某些分布式系統中,元數據被存儲在一個單獨的架構組件中的。Redis Cluster并沒有這樣一個元數據存儲的組件,而是把元數據分散的存儲在所有的節(jié)點上。
hash slot map和node table在集群創(chuàng)建時會被創(chuàng)建出來,并且會隨著后續(xù)的集群變更(比如failover和擴容、縮容等運維操作,后面會講述)而跟隨著變更。變動會在一個Node上發(fā)起,通過gossip協議傳播到所有其他的節(jié)點。這兩個信息在所有節(jié)點都會保存,并且會最終達到一致。
8、集群創(chuàng)建
創(chuàng)建Redis Cluster時,首先要以cluster模式運行多個redis server,redis server運行起來后,這些server的node id就已經生成了。但是這些redis server并沒有形成集群,也就是server彼此之間并不知道相互的存在。接下來運行CLUSTER MEET命令,讓這些節(jié)點形成一個集群。但是這時集群仍然沒有處于運行狀態(tài),需要分配slot,通過CLUSTER SLOTADD命令把slot分配給具體的節(jié)點之后,集群就可以處理client的命令了。
9、客戶端的讀寫操作
客戶端要讀取、寫入數據時,雖然client可以連接任意server,但是實際中,client需要根據實際需求連接到server讀取、寫入數據。client需要先根據key計算出hash slot,連接到負責這個hash slot的節(jié)點進行讀寫操作。這樣的話,client就要需要知道hash slot -> node的映射關系,也就是需要知道hash slot map。
前面講過hash slot map被保存在server端的每個節(jié)點上。client可以從任意節(jié)點獲取hash slot map,并且把它緩存到client本地,下次操作時根據本地緩存直接進行操作,但是需要處理緩存信息過期的問題,如果client發(fā)現hash slot map發(fā)生變化(即client讀取寫入數據時server回錯誤,接下來會詳細講述),會重新從server端獲取新的hash slot map。通過hash slot map可以判斷某個key應該存在在哪個節(jié)點上,client再連接這個節(jié)點進行讀寫操作。
10、MOVED Redirection
hash slot map會發(fā)生變更,這些變更會復制到所有的節(jié)點,但是gossip保證的是最終復制到所有的節(jié)點,再加上client會緩存hash slot map,client可能會把某個key的請求發(fā)給錯誤的節(jié)點來處理。
錯誤的節(jié)點收到請求后,發(fā)現這個key不應該自己來處理,會給客戶端返回MOVED的錯誤,在錯誤消息中,會告訴客戶端,哪個節(jié)點應該負責這個slot。Client收到MOVED消息后,會向消息中指定的節(jié)點再次發(fā)送請求。
client可以將slot的節(jié)點信息更新到本地緩存的hash slot map中,但是更好的方法是,重新獲取完整的hash slot map,替換本地的緩存。因為在大多數情況下,hash slot map中的變更不僅僅只修改一個slot。
雖然client按照MOVED消息中的節(jié)點信息重新發(fā)送請求,但是client仍然可能再次從新節(jié)點收到MOVED錯誤消息,因為上一個節(jié)點的hash slot map可能也不是最新的。但是因為hash slot map最終會在所有的節(jié)點上一致,所以client在幾次收到MOVED錯誤后,最終會獲取到最新的hash slot map。
11、Failover、currentEpoch、lastVoteEpoch
當master發(fā)生故障宕機后,Redis Cluster會選出一個slave來接替這個master。
如果有多個slave存在,那么每個slave都可能都會發(fā)現master發(fā)生了宕機,并且試圖把自己變成為master,如果有多個slave成為master,那么這些新master都會更新本地hash slot map,把舊master負責的slot更新成自己,并且把自己對hash slot map的更新傳播給其他的節(jié)點。這會導致hash slot map在節(jié)點間出現差異。
從而導致,因為所連接的節(jié)點不同,client拿到不同的hash slot map,對于同一個slot,不同的client會連接不同的節(jié)點,最終導致節(jié)點上的數據出現差異。所以failover要保證,只有一個slave被選成新master。
Redis Cluster采用了類似Raft算法的技術來防止多個slave被選成master。每個節(jié)點都會有叫做currentEpoch、lastVoteEpoch的兩個值。在集群剛創(chuàng)建時,每個節(jié)點的currentEpoch都是0。
當slave發(fā)現master宕機時,這個slave會增加currentEpoch(即currentEpoch++)。并且向所有的master發(fā)送FAILOVER_AUTH_REQUEST請求,請求中會攜帶自己currentEpoch,master收到FAILOVER_AUTH_REQUEST,如果請求中的currentEpoch比自己的currentEpoch和lastVoteEpoch都大,則記錄請求中的currentEpoch值到自己的currentEpoch、lastVoteEpoch中,并且回復FAILOVER_AUTH_ACK給slave,回復中攜帶master的currentEpoch。
所以可以看出,FAILOVER_AUTH_ACK中的Epoch一定與slave的currentEpoch相同。Slave從大多數的master收到FAILOVER_AUTH_ACK后,則成為master。
上面的過程保證只有一個slave被選出,我們來舉例說明。五個master的集群,master節(jié)點分別是A、B、C、D、E,A節(jié)點有兩個slave,分別是A1和A2。A節(jié)點發(fā)生宕機,A1增加自己的currentEpoch=5(4+1),A1給所有的master發(fā)送FAILOVER_AUTH_REQUEST,節(jié)點B、C、D收到FAILOVER_AUTH_REQUEST,把自己的currentEpoch和lastVoteEpoch更新成5,并且給A1回復FAILOVER_AUTH_ACK,A1贏得選舉,成為新的master。但是與此同時,A2也發(fā)現A宕機,也試圖選舉成master。A2增加自己的currentEpoch=5(4+1),A2給所有的master發(fā)送FAILOVER_AUTH_REQUEST,但這時的B、C、D的lastVoteEpoch已經是5,所以B、C、D不會給A2回復,E還沒有收到A1的請求,所以只有E會給A2回復,但是不能形成大多數,所以A2不能稱為master。
上面講述的過程已經可以保證只有一個master被選出,但是除此之外,Redis Cluster還做了一個優(yōu)化,那就是master回復了一個請求后不會在給這個master的其他的slave發(fā)送回復。
12、Configuration epoch
failover完成后,新master會修改hash slot map,把相應的slot記錄的節(jié)點改成自己,并且把這次對hash slot map的改動傳播給其他節(jié)點。
雖然currentEpoch和lastVoteEpoch能保證每次failover只能有一個節(jié)點被選成新的master,但是先后兩次failover,可能選出的兩個不同的master,但是他們對hash slot map的修改的傳播卻是異步,也就是后面一次failover的改動可能先于第一次failover的改動到達某個節(jié)點,從而導致節(jié)點間對hash slot map這個信息產生不一致。
Redis Cluster通過configEpoch來解決這個問題。每個節(jié)點會保存一個configEpoch的值。相當于在node table中還會有一列數據叫configEpoch,類似于下面的表:
每次failover完成后,新選出的master會用currentEpoch覆蓋configEpoch。Failover的機制保證兩次failover的新master一定是具有不同的currentEpoch,并且后一次的failover的currentEpoch一定比前一次大。這樣就可以保證,即便采用了gossip這樣傳播協議,仍然能夠保證最后一次failover的hash slot map的變更會生效,也就是configEpoch更大的變更會生效,并且最終所有的節(jié)點上的hash slot map是一致的。
Slave節(jié)點的configEpoch就是其master的configEpoch。
由于gossip保證的hash slot map最終保持一致的,所以可能存在slave的hash slot map舊于master,failover不能基于舊的hash slot map基礎上做變更,所以前面failover的過程中還需要補充一個規(guī)則要遵守:
- 在FAILOVER_AUTH_REQUEST中會攜帶slave節(jié)點的configEpoch,如果這個slave的configEpoch比這個slave負責的所有slot的master的configEpoch中任意一個要小,則master不會給slave回復FAILOVER_AUTH_ACK。
13、Resharding
在集群創(chuàng)建之后,我們還會有對Redis cluster做擴容、縮容、balancing這樣的運維需求,這些需求本質上都可以用 Resharding 操作解決,resharding操作就是把slot在節(jié)點間重新的分布,把slot從一個節(jié)點轉移到另外一個節(jié)點上。在Redis Cluster中,擴容需求實質上就是加入一個新的節(jié)點,再把一些slot分配到這個新節(jié)點上。
縮容需求實質上就是先把這個節(jié)點上的所有slot分配到其他節(jié)點上,再把這個節(jié)點從集群中移出。當節(jié)點間的流量不均衡時,我們有balancing這樣的需求,balancing就是把流量比較大的節(jié)點上的一些slot分配都流量比較少的節(jié)點上。
Resharding操作可以是對整個hash slot map的調整,也就是可以包括對多個slot的遷移(migration),遷移就是把一個slot從一個節(jié)點遷移到另外一個節(jié)點。一個slot migration操作包括前面講的hash slot map變更,另外還包括key的遷移操作。要把一個slot遷移到另外的節(jié)點上,首先把這個slot上的所有的key遷移到這個節(jié)點,當把所有key都遷移完后,再進行hash slot map變更,當hash slot map變更完成,這次slot migration結束。
Redis Cluster使用CLUSTER SETSLOT來設置遷移。舉例說明,將slot1從節(jié)點A遷移到節(jié)點B。分別對節(jié)點A和節(jié)點B執(zhí)行下面的命令:
- 節(jié)點A上:CLUSTER SETSLOT 1 MIGRATING NODEB
- 節(jié)點B上:CLUSTER SETSLOT 1 IMPORTING NODEA
其中,MIGRATING表示數據要從這個節(jié)點遷出,而IMPORTING表示數據要往這個節(jié)點遷入。
執(zhí)行完這兩個命令后,節(jié)點A中的slot1不在創(chuàng)建新的key。一個叫做redis-trib的特殊的程序負責把所有的key從節(jié)點A遷移到節(jié)點B。redis-trib會執(zhí)行下面的命令:
- CLUSTER GETKEYSINSLOT slot count
這個命令會返回count個key,對于每個返回的key,redis-trib執(zhí)行下面的命令:
- MIGRATE target_host target_port key target_database id timeout
這個命令會原子地把一個key從節(jié)點A遷移到節(jié)點B。具體來說,MIGRATE命令會連接目標節(jié)點,并發(fā)向目標節(jié)點發(fā)送這個key,一旦目標節(jié)點收到這個key,則從自己的數據庫中刪除這個key,在這個過程中,節(jié)點A和節(jié)點B都會加鎖。
在把所有的key遷移完后,再分別在兩個節(jié)點上執(zhí)行下面的命令:
- CLUSTER SETSLOT slot NODE nodeA
把所有的key遷移完一般需要一些時間,也就是說在開始遷移后和完成遷移前,在這個窗口期內,key的實際的分布,與hash slot map里記錄的是不一致的,client按照hash slot map訪問key,會出現錯誤。
Redis Cluster通過ASK redirection來解決這個問題。按照client端的hash slot map,slot1的key一定會發(fā)給節(jié)點A,節(jié)點A收到這個請求后,如果發(fā)現這個key已經遷移到節(jié)點B了,那么就會給client回復ASK redirection,client收到ASK redirection后,會向節(jié)點b先發(fā)送一個ASKING命令,之后在發(fā)送對這個key的請求。
14、Configuration的實際存儲
Hash slot map和node table都是邏輯上的結構,他們在Redis Cluster中的實際存儲結構稍有不同(詳情看結尾參考資料1、2、3、4)。
在節(jié)點的內存中,用兩個變量來存儲這兩個信息:
- myself變量:myself代表本節(jié)點,是一個ClusterNode類型的變量,這個變量中,包含本節(jié)點的configEpoch,還包括slaveof,如果是slave節(jié)點則在slaveof中記錄著它的master節(jié)點,還包括一個bitmap,代表這個節(jié)點負責的所有的slot的槽位值。這個bitmap有2048個byte組成,一總是16384(2048*8)個bit,每個bit代表一個slot,bit置1,代表這個節(jié)點負責這個slot;
- cluster變量:代表了所在集群的狀態(tài),它包含currentEpoch、lastVoteEpoch和slots數組,slots數組的index代表了slot,數組的每個成員都指向一個節(jié)點,是一個ClusterNode類型的變量,與myself變量的類型一樣。
所有Configuration的更改都會被保存到磁盤中,具體來講是保存到一個名字叫node.conf的文件中,這個文件是Redis Cluster負責寫入的,不需要人工配置。
node.conf按照節(jié)點維度進行保存。每一行對應一個節(jié)點,每行分別包含這些信息:id,ip:port,flag,slaveof,ping timestamp, pong timespamp,configEpoch,link status,slots。
所有的節(jié)點結束后,會在文件的最后保存curruntEpoch和lastVoteEpoch兩個變量。其中flag字段是枚舉類型,會指明這個節(jié)點是不是自己,節(jié)點類型是master還是slave。
如果是slave節(jié)點,則會在slaveof字段記錄其master節(jié)點的id。如果是master節(jié)點,則在最后多一個slots字段,記錄著這個節(jié)點負責著哪些slot。Flags字段還記錄著其他非常重
要的狀態(tài),本文就不繼續(xù)展開了。
同樣,ping timestamp、pong timestmap、link staus三個字段本文也不繼續(xù)展開了。
具體的node.conf文件類似下面的例子:
- [root@10.112.178.141 data]# cat nodes-6384.conf
- fb763117270d14205c41174605b15741co03a945 10.112.178.174:6383 slave 5e35bda1a44c8d781eb54e08be88a3bab42070f3 0 1596683852819 2 connected
- 3dc5890fb1591e3b20196f81eb5f2f99754253e8 10.112.178.141:6383 master - 0 1596683851915 1 connected 0-5461
- f1967b687c9b2c27108cce08517e98e7a80d5e7e 10.112.178.171:6383 slave 3dc5890fb1591e3b20196f81eb5f2f99754253e8 0 1596683850813 1 connected
- 2bbab7353e973e991566df3bb52afb4857a7bf25 10.112.178.171:6384 slave 1f0a8cf1bfd0c915ef404482f3dc6bf5c7cf41f5 0 1596683848812 3 connected
- 5e35bda1a44c8d781eb54e08be88a3bab42070f3 10.112.178.142:6383 master - 0 1596683849813 2 connected 5462-10923
- 1f0a8cf1bfd0c915ef404482f3dc6bf5c7cf41f5 10.112.178.141:6384 myself,master - 0 0 3 connected 10924-16383
節(jié)點啟動時會讀取node.conf文件,把里面的信息加載到myself和cluster兩個變量中。Slot信息會被轉換成bitmap保存在myself變量中。并且slot信息還會逆向的轉換成slot到節(jié)點的映射保存在cluster變量中。
hash slot map變更或者node table變更,就是修改內存中的myself變量和cluater變量,并且每次變更都會把這兩個變量序列化轉化后保存到node.conf中。
15、查看configuration
Redis Cluster提供了兩個命令來查看configuration:
第一個是CLUSTER SLOT命令,用來展示hash slot維度的信息,CLUSTER SLOT命令的展示如下:
- 127.0.0.1:7000> cluster slots
- 1) 1) (integer) 5461
- 2) (integer) 10922
- 3) 1) "127.0.0.1"
- 2) (integer) 7001
- 4) 1) "127.0.0.1"
- 2) (integer) 7004
- 2) 1) (integer) 0
- 2) (integer) 5460
- 3) 1) "127.0.0.1"
- 2) (integer) 7000
- 4) 1) "127.0.0.1"
- 2) (integer) 7003
- 3) 1) (integer) 10923
- 2) (integer) 16383
- 3) 1) "127.0.0.1"
- 2) (integer) 7002
- 4) 1) "127.0.0.1"
- 2) (integer) 7005
第二個是CLUSTER NODE命令,用來展示node table維度的信息,CLUSTER NODE命令的展示如下:
- $ redis-cli cluster nodes
- d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
- 3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
- d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095
CLUSTER NODE、CLUSTER SLOT兩個命令可以連接到任意節(jié)點上執(zhí)行,這兩個命令都是讀取的這個節(jié)點的本地信息,根據gossip的特性,存在這兩個命令展示的不是最新的configuration的可能性。
16、Conflict
雖然前面講的failover過程通過大多數master投票的方式保證只有一個slave選中,并且產生唯一的configEpoch。但是Resharding的過程卻沒有經過大多數的master的投票。
執(zhí)行slot遷移時,僅僅是在集群中所有configEpoch中最大的那個configEpoch的基礎上,再加一而得到的。并且由于Resharding一般包括多個slot的遷移,Redis cluster目前的做法是,在一次resharding過程中,所有的slot遷移使用的configEpoch都是第一個slot遷移時產生的那個configEpoch。
而failover和resharding都會修改hash slot map,如果在resharding的過程中發(fā)生了failover,這就有可能導致對hash slot map的修改產生沖突。另外,手動failover也是不經過master投票的,也就是執(zhí)行CLUSTER FAILOVER命令(帶TAKEOVER參數)。
產生沖突就是指針對同一個slot,slot被修改成映射到不同的節(jié)點上,并且這些修改具有相同的configEpoch。
為了解決這個問題,Redis cluster需要存在一個沖突解決的機制。如果一個master發(fā)現相同的configEpoch,則比較一下兩個節(jié)點的id,id小的節(jié)點,把自己currentEpoch加一,作為自己的configEpoch。
三、Write Safety
由于有沖突的存在,可能導致不同的節(jié)點上的hash slot map不一致,取決于連接的節(jié)點不同,一部分client可能會把某個slot的key寫入到一個節(jié)點中,而另外一部分client會把同樣slot的key寫入到另外一個節(jié)點中。當沖突被解決后,其中一個節(jié)點上接受的寫入會丟失。
另外,由于master和slave之間的數據復制是異步的,在failover時如果slave還沒有收到最新的數據,就發(fā)生了failover,那么這部分寫入就會丟失。Redis cluster在這方面做了一個優(yōu)化,當一個slave發(fā)現master發(fā)生了宕機,它不會立即開始選舉的過程,它會等待一個時間,這個時間計算公式如下:
- DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds
這個計算公式中,
- 第一部分是一個固定500毫秒的時間,這是為了給master充分的時間,也發(fā)現節(jié)點宕機這個事實;
- 第二個是隨機等待一段時間,這是為了盡量避免多個slave同時發(fā)現master宕機,然后同時開始選舉,導致master被瓜分,從而導致所有的選舉都不成功;
- 第三部分是slave的rank,rank主要取決于slave的復制進度,復制的數據越多,則rank越小,也就是越短的等待時間,越先開始選舉,有更大的可能性被選成新的master。但這僅僅是個優(yōu)化,不能完全防止丟數據的可能。
作者介紹
陳東明,現任國美在線基礎架構總監(jiān)。曾任餓了么北京技術中心任架構組負責人,負責產品線架構設計及基礎架構研發(fā)工作;曾任百度架構師,負責即時通訊產品的架構設計。具有豐富的大規(guī)模系統構建和基礎架構研發(fā)經驗,善于復雜業(yè)務需求下的大并發(fā)、分布式系統設計和持續(xù)優(yōu)化。個人公眾號:dongming_cdm。