基本目標與設計基本思想
Redis cluster 目標
- 高性能,并且能線性擴展到1000個節(jié)點。不需要代理,使用異步復制,值沒有合并的操作
- 可接受的寫安全
- 可用性
實現(xiàn)的子集
Redis cluster 實現(xiàn)了所有的single key 操作,對于multi key操作的話,這些key必須在一個節(jié)點上面,redis cluster 通過 hash tags決定key存貯在哪個slot上面。
client 與server 在集群中的角色
節(jié)點首要功能是存貯數(shù)據(jù),集群狀態(tài),映射key到相應的節(jié)點。自動發(fā)現(xiàn)其他節(jié)點,發(fā)現(xiàn)失敗節(jié)點,讓從變?yōu)橹鳌?/p>
為了完成以上功能,cluster使用tcp和二進制協(xié)議(Redis Cluster Bus),節(jié)點間互聯(lián).node 同時使用gossip協(xié)議傳播信息,包括節(jié)點的發(fā)現(xiàn),發(fā)送ping包,Pub/Sub信息。
因為節(jié)點并不代理請求轉(zhuǎn)發(fā),會返回MOVED和ASk錯誤,clients就可以直連到其他節(jié)點。client理論上面可以給任意節(jié)點發(fā)送請求,如果需要就重定向。但實際應用中client存貯一個從key到node的map來提高性能。
寫安全
Redis cluster 使用異步復制的模式,故障轉(zhuǎn)移的時候,被選為主的節(jié)點,會用自己的數(shù)據(jù)去覆蓋其他副本節(jié)點的數(shù)據(jù)。所以總有一個時間口會丟失數(shù)據(jù)。
下面一個例子會丟失數(shù)據(jù):
一個寫請求到master節(jié)點,master返回成功給client,但還沒異步寫個副本的時候,這時候master死掉了,如果一定時間不恢復,從升為主節(jié)點,數(shù)據(jù)就永遠丟了。
理論上面丟失數(shù)據(jù)還有下面一種情況
master partition 變得不可用
它的一個從變?yōu)橹?/p>
一定時間之后,這個主又可用了
客戶端這時候還使用舊的的路由,在這個主變?yōu)閺闹埃瑢懻埱蟮竭_這個主。
3、可用性
假設n個主節(jié)點,每個主下面掛載一個從,掛掉一個,集群仍然可用。掛點兩個,可用性是1 -(1/(n2 -1))(第一個節(jié)點掛掉后,還剩下n2-1個節(jié)點),只有一個節(jié)點的主掛掉的可能性是 1/n*2 -1)
replicas migration 使可用性更高
4、性能
reids cluster 不代理請求到正確的節(jié)點,而是告訴客戶端正確的節(jié)點
client 會保存一份最新的key與node映射,一般情況,會直接訪問到正確的節(jié)點。
異步寫副本
一般的操作和單臺redis有相同的性能,一個有n個主節(jié)點的集群性能接近n*單個redis
綜上 高性能 線性擴展 合理的寫安全 高可用 是rediscluser 的主要目標
為什么避免數(shù)據(jù)合并
因為首先redis 存貯的數(shù)據(jù)量會特別大,如果合并需要更大的空間
Redis Cluseter 主要組件
key 分布模式
key空間分布被劃分為16384個slot,所以一個集群,主節(jié)點的個數(shù)最大為16384(一般建議master最大節(jié)點數(shù)為1000)
HASH_SLOT = CRC16(key) mod 16384
Keys hash tags
hash tag 是為了保證不同的key,可以分布到同一個slot上面,來執(zhí)行multi-key的操作
hash tag的規(guī)則是以第一個{開始,到第一個}結尾,中間的內(nèi)容,來做hash。
例子
{user1000}.following 與 {user1000}.followers user1000作為key
foo{}{bar} 整個key
{{bar}} {bar 為key
{bar}{zap} bar 為key
Ruby Example
def HASH_SLOT(key)
s = key.index "{"
if s
e = key.index "}",s+1
if e && e != s+1
key = key[s+1..e-1]
end
end
crc16(key) % 16384
end`
Cluster nodes 屬性
$ 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
從左到右依次為:node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots
其中node id是第一次啟動獲得的一個160字節(jié)的隨機字符串,并把id保存在配置文件中,一直不會再變
Cluster bus
每個節(jié)點有一個額外的TCP端口,這個端口用來和其他節(jié)點交換信息。這個端口一般是在與客戶端鏈接端口上面加10000,比如客戶端端口為6379,那么cluster bus的端口為16379.
node-to-node 交流是通過cluster bus與 cluster bus protocol進行。其中cluster bus protocol 是一個二進制協(xié)議,因為官方不建議其他應用與redis 節(jié)點進行通信,所以沒有公開的文檔,要查看的話只能去看源碼。
cluster 拓撲
Redis cluster 是一個網(wǎng)狀的,每一個節(jié)點通過tcp與其他每個節(jié)點連接。假如n個節(jié)點的集群,每個節(jié)點有n-1個出的鏈接,n-1個進的鏈接。這些鏈接會一直存活。假如一個節(jié)點發(fā)送了一個ping,很就沒收到pong,但還沒到時間把這個節(jié)點設為 unreachable,就會通過重連刷新鏈接。
Nodes handshake
node 會在cluster bus端口一直接受連接,回復ping,即使這個ping 的node是不可信的。但是其他的包會被丟掉,如果發(fā)送者不是cluster 一員。
一個node有兩種方式接受其他其他node作為集群一員
-
如果一個節(jié)點發(fā)送MEET信息(METT 類似ping,但是強迫接受者,把它作為集群一員)。一個節(jié)點發(fā)送MEET信息,只有管理員通過命令行,運行如下命令
CLUSTER MEET ip port
如果這個節(jié)點已經(jīng)被一個節(jié)點信任,那么也會被其他節(jié)點信任。比如A 知道B,B知道C,B會發(fā)送gossip信息給A關于C的信息。A就會認為C是集群一員,并與其建立連接。
這樣只要我們把節(jié)點加入到一個節(jié)點,就會自動被其他節(jié)點自動發(fā)現(xiàn)。
Redirection and resharding
MOVED Redirection
客戶端可以自由的連接任何一個node,如果這個node 不能處理會返回一個MOVED的錯誤,類似下面這樣
GET x
-MOVED 3999 127.0.0.1:6381
描述了key 的hash slot,屬于哪個node
client 會維護一個hash slots到IP:port的映射
當收到moved錯誤的時候,可以通過CLUSTER NODES或者CLUSTER SLOTS去刷新一遍整個client
Cluster live reconfiguration
cluster 支持運行狀態(tài)下添加和刪除節(jié)點。添加刪除節(jié)點抽象:把一部分hash slot從一個節(jié)點移動到另一個節(jié)點。
- 添加一個新節(jié)點,hash slot 從一些節(jié)點移動到這個新節(jié)點
- 移除一個節(jié)點,把這個節(jié)點的hash slot 移動到其他的節(jié)點
- rebalance ,部分hash slot在節(jié)點間移動
所以,動態(tài)擴容的核心就是在節(jié)點之間移動hash slot,hash slot 又是key的集合。所以reshare 就是把key從一個節(jié)點移動到其他節(jié)點。
redis 提供如下命令:
- 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
前兩個指令:ADDSLOTS和DELSLOTS,用于向當前node分配或者移除slots,指令可以接受多個slot值。分配slots的意思是告知指定的master(即此指令需要在某個master節(jié)點執(zhí)行)此后由它接管相應slots的服務;slots分配后,這些信息將會通過gossip發(fā)給集群的其他nodes。
ADDSLOTS指令通常在創(chuàng)建一個新的Cluster時使用,一個新的Cluster有多個空的Masters構成,此后管理員需要手動為每個master分配slots,并將16384個slots分配完畢,集群才能正常服務。簡而言之,ADDSLOTS只能操作那些尚未分配的(即不被任何nodes持有)slots,我們通常在創(chuàng)建新的集群或者修復一個broken的集群(集群中某些slots因為nodes的永久失效而丟失)時使用。為了避免出錯,Redis Cluster提供了一個redis-trib輔助工具,方便我們做這些事情。
DELSLOTS就是將指定的slots刪除,前提是這些slots必須在當前node上,被刪除的slots處于“未分配”狀態(tài)(當然其對應的keys數(shù)據(jù)也被clear),即尚未被任何nodes覆蓋,這種情況可能導致集群處于不可用狀態(tài),此指令通常用于debug,在實際環(huán)境中很少使用。那些被刪除的slots,可以通過ADDSLOTS重新分配。
SETSLOT是個很重要的指令,對集群slots進行reshard的最重要手段;它用來將單個slot在兩個nodes間遷移。根據(jù)slot的操作方式,它有兩種狀態(tài)“MIGRATING”、“IMPORTING”
1)MIGRATING:將slot的狀態(tài)設置為“MIGRATING”,并遷移到destination-node上,需要注意當前node必須是slot的持有者。在遷移期間,Client的查詢操作仍在當前node上執(zhí)行,如果key不存在,則會向Client反饋“-ASK”重定向信息,此后Client將會把請求重新提交給遷移的目標node。
2)IMPORTING:將slot的狀態(tài)設置為“IMPORTING”,并將其從source-node遷移到當前node上,前提是source-node必須是slot的持有者。Client交互機制同上。
假如我們有兩個節(jié)點A、B,其中slot 8在A上,我們希望將8從A遷移到B,可以使用如下方式:
1)在B上:CLUSTER SETSLOT 8 IMPORTING A
2)在A上:CLUSTER SETSLOT 8 MIGRATING B
在遷移期間,集群中其他的nodes的集群信息不會改變,即slot 8仍對應A,即此期間,Client查詢?nèi)栽贏上:
1)如果key在A上存在,則有A執(zhí)行。
2)否則,將向客戶端返回ASK,客戶端將請求重定向到B。
這種方式下,新key的創(chuàng)建就不會在A上執(zhí)行,而是在B上執(zhí)行,這也就是ASK重定向的原因(遷移之前的keys在A,遷移期間created的keys在B上);當上述SET SLOT執(zhí)行完畢后,slot的狀態(tài)也會被自動清除,同時將slot遷移信息傳播給其他nodes,至此集群中slot的映射關系將會變更,此后slot 8的數(shù)據(jù)請求將會直接提交到B上。
動態(tài)分片的步驟:
- 在目標節(jié)點設置為 SETSLOT <slot> IMPORTING <source-node-id>.
- 在原節(jié)點 SETSLOT <slot> MIGRATING <destination-node-id>.
- CLUSTER GETKEYSINSLOT 獲得所有的key ,使用MIGRATE 從原節(jié)點遷移到目標節(jié)點
- 在原節(jié)點或者目標節(jié)點 CLUSTER SETSLOT <slot> NODE <destination-node-id>
ASK重定向
在上文中,我們已經(jīng)介紹了MOVED重定向,ASK與其非常相似。在resharding期間,為什么不能用MOVED?MOVED意思為hash slots已經(jīng)永久被另一個node接管、接下來的相應的查詢應該與它交互,ASK的意思是當前query暫時與指定的node交互;在遷移期間,slot 8的keys有可能仍在A上,所以Client的請求仍然需要首先經(jīng)由A,對于A上不存在的,我們才需要到B上進行嘗試。遷移期間,Redis Cluster并沒有粗暴的將slot 8的請求全部阻塞、直到遷移結束,這種方式盡管不再需要ASK,但是會影響集群的可用性。
1)當Client接收到ASK重定向,它僅僅將當前query重定向到指定的node;此后的請求仍然交付給舊的節(jié)點。
2)客戶端并不會更新本地的slots映射,仍然保持slot 8與A的映射;直到集群遷移完畢,且遇到MOVED重定向。
一旦slot 8遷移完畢之后(集群的映射信息也已更新),如果Client再次在A上訪問slot 8時,將會得到MOVED重定向信息,此后客戶端也更新本地的集群映射信息。
客戶端首次鏈接以及重定向處理
可能有些Cluster客戶端的實現(xiàn),不會在內(nèi)存中保存slots映射關系(即nodes與slots的關系),每次請求都從聲明的、已知的nodes中,隨機訪問一個node,并根據(jù)重定向(MOVED)信息來尋找合適的node,這種訪問模式,通常是非常低效的。
當然,Client應該盡可能的將slots配置信息緩存在本地,不過配置信息也不需要絕對的實時更新,因為在請求時偶爾出現(xiàn)“重定向”,Client也能兼容此次請求的正確轉(zhuǎn)發(fā),此時再更新slots配置。(所以Client通常不需要間歇性的檢測Cluster中配置信息是否已經(jīng)更新)客戶端通常是全量更新slots配置:
- 首次鏈接到集群的某個節(jié)點
- 當遇到MOVED重定向消息時
遇到MOVED時,客戶端僅僅更新特定的slot是不夠的,因為集群中的reshard通常會影響到多個slots。客戶端通過向任意一個nodes發(fā)送“CLUSTER NODES”或者“CLUSTER SLOTS”指令均可以獲得當前集群最新的slots映射信息;“CLUSTER SLOTS”指令返回的信息更易于Client解析。
slaves擴展reads請求
通常情況下,read、write請求都將有持有slots的master節(jié)點處理;因為redis的slaves可以支持read操作(前提是application能夠容忍stale數(shù)據(jù)),所以客戶端可以使用“READONLY”指令來擴展read請求。
“READONLY”表明其可以訪問集群的slaves節(jié)點,能夠容忍stale數(shù)據(jù),而且此次鏈接不會執(zhí)行writes操作。當鏈接設定為readonly模式后,Cluster只有當keys不被slave的master節(jié)點持有時才會發(fā)送重定向消息(即Client的read請求總是發(fā)給slave,只有當此slave的master不持有slots時才會重定向,很好理解):
1)此slave的master節(jié)點不持有相應的slots
2)集群重新配置,比如reshard或者slave遷移到了其他master上,此slave本身也不再支持此slot。
容錯
心跳與gossip消息
集群中的nodes持續(xù)的交換ping、pong數(shù)據(jù),這兩種數(shù)據(jù)包的結構一樣,同樣都攜帶集群的配置信息,唯一不同的就是message中的type字段。
通常,一個node發(fā)送ping消息,那么接收者將會反饋pong消息;不過有時候并非如此,比如當集群中添加新的node時,接收者會將pong信息發(fā)給其他的nodes,而不是直接反饋給發(fā)送者。這樣的好處是會將配置盡快的在cluster傳播。
通常一個node每秒都會隨機向幾個nodes發(fā)送ping,所以無論集群規(guī)模多大,每個nodes發(fā)送的ping數(shù)據(jù)包的總量是恒定的。每個node都確保盡可能半個NODE_TIMEOUT時間內(nèi),向那些尚未發(fā)送過ping或者未接收到它們的pong消息的nodes發(fā)送ping。在NODE_TIMEOUT逾期之前,nodes也會嘗試與那些通訊異常的nodes重新建立TCP鏈接,確保不能僅僅因為當前鏈接異常而認為它們就是不可達的。
當NODE_TIMEOUT值較小、集群中nodes規(guī)模較大時,那么全局交換的信息量也會非常龐大,因為每個node都盡力在半個NODE_TIMEOUT時間內(nèi),向其他nodes發(fā)送ping。比如有100個nodes,NODE_TIMEOUT為60秒,那么每個node在30秒內(nèi)向其他99各nodes發(fā)送ping,平均每秒3.3個消息,那么整個集群全局就是每秒330個消息。這些消息量,并不會對集群的帶寬帶來不良問題。
心跳包的內(nèi)容
心跳數(shù)據(jù)包的內(nèi)容
- node ID
- currentEpoch和configEpoch
- node flags:比如表示此node是maste、slave等
- hash slots:發(fā)送者持有的slots
- TCP port
- state (down or ok)
- master node ID (如果是從節(jié)點)
ping和pong數(shù)據(jù)包中也包含gossip部分,這部分信息告訴接受者,當前節(jié)點持有其他節(jié)點的狀態(tài),不過它只包含sender已知的隨機幾個nodes,nodes的數(shù)量根據(jù)集群規(guī)模的大小按比例計算。
gossip部分包含了
- Node ID
- IP and port of the node
- Node flags
失敗檢測
集群失效檢測就是,當某個master或者slave不能被大多數(shù)nodes可達時,用于故障遷移并將合適的slave提升為master。當slave提升未能有效實施時,集群將處于error狀態(tài)且停止接收Client端查詢。
每個node持有其已知nodes的列表包括flags,有2個flag狀態(tài):PFAIL和FAIL;PFAIL表示“可能失效”,是一種尚未完全確認的失效狀態(tài)(即某個節(jié)點或者少數(shù)masters認為其不可達)。FAIL表示此node已經(jīng)被集群大多數(shù)masters判定為失效(大多數(shù)master已認定為不可達,且不可達時間已達到設定值,需要failover)。
nodes的ID、ip+port、flags,那么接收者將根據(jù)sender的視圖,來判定節(jié)點的狀態(tài),這對故障檢測、節(jié)點自動發(fā)現(xiàn)非常有用。
PFAIL flag:
當node不可達的時間超過NODE_TIMEOUT,這個節(jié)點就被標記為PFAIL(Possible failure),master和slave都可以標記其他節(jié)點為PFAIL。所謂不可達,就是當“active ping”(發(fā)送ping且能受到pong)尚未成功的時間超過NODE_TIMEOUT,因此我們設定的NODE_TIMEOUT的值應該比網(wǎng)絡交互往返的時間延遲要大一些(通常要大的多,以至于交互往返時間可以忽略)。為了避免誤判,當一個node在半個NODE_TIMEOUT時間內(nèi)仍未能pong,那么當前node將會盡力嘗試重新建立連接進行重試,以排除pong未能接收