前面我們介紹了國人自己開發的redis集群方案——Codis,Codis友好的管理界面以及強大的自動平衡槽位的功能深受廣大開發者的喜愛。今天我們一起來聊一聊Redis作者自己提供的集群方案——Cluster。希望讀完這篇文章,你能夠充分了解Codis和Cluster各自的優缺點,面對不同的應用場景可以從容的做出選擇。
Redis Cluster是去中心化的,這點與Codis有著本質的不同,Redis Cluster劃分了16384個slots,每個節點負責其中的一部分數據。slot的信息存儲在每個節點中,節點會將slot信息持久化到配置文件中,因此需要保證配置文件是可寫的。當客戶端連接時,會獲得一份slot的信息。這樣當客戶端需要訪問某個key時,就可以直接根據緩存在本地的slot信息來定位節點。這樣就會存在客戶端緩存的slot信息和服務器的slot信息不一致的問題,這個問題具體怎么解決呢?這里先賣個關子,后面會做解釋。
特性
首先我們來看下官方對Redis Cluster的介紹。
- High performance and linear scalability up to 1000 nodes. There are no proxies, asynchronous replication is used, and no merge operations are performed on values.
- Acceptable degree of write safety: the system tries (in a best-effort way) to retain all the writes originating from clients connected with the majority of the master nodes. Usually there are small windows where acknowledged writes can be lost. Windows to lose acknowledged writes are larger when clients are in a minority partition.
- Availability: Redis Cluster is able to survive partitions where the majority of the master nodes are reachable and there is at least one reachable slave for every master node that is no longer reachable. Moreover using replicas migration, masters no longer replicated by any slave will receive one from a master which is covered by multiple slaves.
是不是不(kan)想(bu)看(dong)?沒關系,我來給你掰開了揉碎了解釋一下。
寫安全
Redis Cluster使用異步的主從同步方式,只能保證最終一致性。所以會引起一些寫入數據丟失的問題,在繼續閱讀之前,可以先自己思考一下在什么情況下寫入的數據會丟失。
先來看一種比較常見的寫丟失的情況:
client向一個master發送一個寫請求,master寫成功并通知client。在同步到slave之前,這個master掛了,它的slave代替它成為了新的master。這時前面寫入的數據就丟失了。
此外,還有一種情況。
master節點與大多數節點無法通信,一段時間后,這個master被認為已經下線,并且被它的slave頂替,又過了一段時間,原來的master節點重寫恢復了連接。這時如果一個client存有過期的路由表,它就會把寫請求發送的這個舊的master節點(已經變成slave了)上,從而導致寫數據丟失。
不過,這種情況一般不會發生,因為當一個master失去連接足夠長時間而被認為已經下線時,就會開始拒絕寫請求。當它恢復之后,仍然會有一小段時間是拒絕寫請求的,這段時間是為了讓其他節點更新自己的路由表中的配置信息。
為了盡可能保證寫安全性,Redis Cluster在發生分區時,會盡量使客戶端連接到多數節點的那一部分,因為如果連接到少數部分,當master被替換時,會因為多數master不可達而拒絕所有的寫請求,這樣損失的數據要增大很多。
Redis Cluster維護了一個NODE_TIMEOUT變量,如果上述情況中,master在NODE_TIMEOUT時間內恢復連接,就不會有數據丟失。
可用性
如果集群的大部分master可達,并且每個不可達的master至少有一個slave,在NODE_TIMEOUT時間后,就會開始進行故障轉移(一般1到2秒),故障轉移完成后的集群仍然可用。
如果集群中得N個master節點都有1個slave,當有一個節點掛掉時,集群一定是可用的,如果有2個節點掛掉,那么就會有1/(N*2-1)的概率導致集群不可用。
Redis Cluster為了提高可用性,新增了一個新的feature,叫做replicas migration(副本遷移,ps:我自己翻譯的),這個feature其實就是在每次故障之后,重新布局集群的slave,給沒有slave的master配備上slave,以此來更好的應對下次故障。
性能
Redis Cluster不提供代理,而是讓client直接重定向到正確的節點。
client中會保存一份集群狀態的副本,一般情況下就會直接連接到正確的節點。
由于Redis Cluster是異步備份的,所以節點不需要等待其他節點確認寫成功就可以直接返回,除非顯式的使用了WAIT命令。
對于操作多個key的命令,所操作的key必須是在同一節點上的,因為數據是不會移動的。(除非是resharding)
Redis Cluster設計的主要目標是提高性能和擴展性,只提供弱的數據安全性和可用性(但是要合理)。
Key分配模型
Redis Cluster共劃分為16384個槽位。這也意味著一個集群最多可以有16384個master,不過官方建議master的最大數量是1000個。
如果Cluster不處于重新配置過程,那么就會達到一種穩定狀態。在穩定狀態下,一個槽位只由一個master提供服務,不過一個master節點會有一個或多個slave,這些slave可以提供緩解master的讀請求的壓力。
Redis Cluster會對key使用CRC16算法進行hash,然后對16384取模來確定key所屬的槽位(hash tag會打破這種規則)。
Keys hash tags
標簽是破壞上述計算規則的實現,Hash tag是一種保證多個鍵被分配到同一個槽位的方法。
hash tag的計算規則是:取一對大括號{}之間的字符進行計算,如果key存在多對大括號,那么就取第一個左括號和第一個右括號之間的字符。如果大括號之前沒有字符,則會對整個字符串進行計算。
說了這個多,可能你還是一頭霧水。別急,我們來吃幾個栗子。
- {Jackeyzhe}.following和{Jackeyzhe}.follower這兩個key都是計算Jackey的hash值
- foo{{bar}}這個key就會對{bar進行hash計算
- follow{}{Jackey}會對整個字符串進行計算
重定向
前面聊性能的時候我們提到過,Redis Cluster為了提高性能,不會提供代理,而是使用重定向的方式讓client連接到正確的節點。下面我們來詳細說明一下Redis Cluster是如何進行重定向的。
MOVED重定向
Redis客戶端可以向集群的任意一個節點發送查詢請求,節點接收到請求后會對其進行解析,如果是操作單個key的命令或者是包含多個在相同槽位key的命令,那么該節點就會去查找這個key是屬于哪個槽位的。
如果key所屬的槽位由該節點提供服務,那么就直接返回結果。否則就會返回一個MOVED錯誤:
1GET x
2-MOVED 3999 127.0.0.1:6381
這個錯誤包括了對應的key屬于哪個槽位(3999)以及該槽位所在的節點的IP地址和端口號。client收到這個錯誤信息后,就將這些信息存儲起來以便可以更準確的找到正確的節點。
當客戶端收到MOVED錯誤后,可以使用CLUSTER NODES或CLUSTER SLOTS命令來更新整個集群的信息,因為當重定向發生時,很少會是單個槽位的變更,一般都會是多個槽位一起更新。因此,在收到MOVED錯誤時,客戶端應該盡早更新集群的分布信息。當集群達到穩定狀態時,客戶端保存的槽位和節點的對應信息都是正確的,cluster的性能也會達到非常高效的狀態。
除了MOVED重定向之外,一個完整的集群還應該支持ASK重定向。
ASK重定向
對于Redis Cluster來講,MOVED重定向意味著請求的slot永遠由另一個node提供服務,而ASK重定向僅代表下一個請求需要發送到指定的節點。在Redis Cluster遷移的時候會用到ASK重定向,那Redis Cluster遷移的過程究竟是怎樣的呢?
Redis Cluster的遷移是以槽位單位的,遷移過程總共分3步(類似于把大象裝進冰箱),我們來舉個栗子,看一下一個槽位從節點A遷移到節點B需要經過哪些步驟:
- 首先打開冰箱門,也就是從A節點獲得槽位所有的key列表,再挨個key進行遷移,在這之前,A節點的該槽位被設置為migrating狀態,B節點被設置為importing的槽位(都是用CLUSTER SETSLOT命令)。
- 第二步,就是要把大象裝進去了,對于每個key來說,就是在A節點用dump命令對其進行序列化,再通過客戶端在B節點執行restore命令,反序列化到B節點。
- 第三步呢,就需要把冰箱門關上,也就是把對應的key從A節點刪除。
有同學會問了,說好的用到ASK重定向呢?上面我們所描述的只是遷移的過程,在遷移過程中,Redis還是要對外提供服務的。試想一下,如果在遷移過程中,我向A節點請求查詢x的值,A說:我這沒有啊,我也不知道是傳到B那去了還是我一直就沒有存,你還是先問問B吧。然后返回給我們一個-ASK targetNodeAddr的錯誤,讓我們去問B。而這時如果我們直接去問B,B肯定會直接說:這個不歸我管,你得去問A。(-MOVED重定向)。因為這時候遷移還沒有完成,所以B也沒說錯,這時候x真的不歸它管。但是我們不能讓它倆來回踢皮球啊,所以在問B之前,我們先給B發一個asking指令,告訴B:下面我問你一個key的值,你得當成是自己的key來處理,不能說不知道。這樣如果x已經遷移到B,就會直接返回結果,如果B也查不到x的下落,說明x不存在。
容錯
了解了Redis Cluster的重定向操作之后,我們再來聊一聊Redis Cluster的容錯機制,Redis Cluster和大多數集群一樣,是通過心跳來判斷一個節點是否存活的。
心跳和gossip消息
集群中的節點會不停的互相交換ping pong包,ping pong包具有相同的結構,只是類型不同,ping pong包合在一起叫做心跳包。
通常節點會發送ping包并接收接收者返回的pong包,不過這也不是絕對,節點也有可能只發送pong包,而不需要讓接收者發送返回包,這種操作通常用于廣播一個新的配置信息。
節點會每個幾秒鐘就發送一定數量的ping包。如果一個節點超過二分之一NODE_TIME時間沒有收到來自某個節點ping或pong包,那么就會在NODE_TIMEOUT之前像該節點發送ping包,在NODE_TIMEOUT之前,節點會嘗試TCP重連,避免由于TCP連接問題而誤以為節點不可達。
心跳包內容
前面我們說了,ping和pong包的結構是相同的,下面就來具體看一下包的內容。
ping和pong包的內容可以分為header和gossip消息兩部分,其中header包含以下信息:
- NODE ID是一個160bit的偽隨機字符串,它是節點在集群中的唯一標識
- currentEpoch和configEpoch字段
- node flag,標識節點是master還是slave,另外還有一些其他的標識位
- 節點提供服務的hash slot的bitmap
- 發送者的TCP端口
- 發送者認為的集群狀態(down or ok)
- 如果是slave,則包含master的NODE ID
gossip包含了該節點認為的其他節點的狀態,不過不是集群的全部節點。具體有以下信息:
- NODE ID
- 節點的IP和端口
- NODE flags
gossip消息在錯誤檢測和節點發現中起著重要的作用。
錯誤檢測
錯誤檢測用于識別集群中的不可達節點是否已下線,如果一個master下線,會將它的slave提升為master。如果無法提升,則集群會處于錯誤狀態。在gossip消息中,NODE flags的值包括兩種PFAIL和FAIL。
PFAIL flag
如果一個節點發現另外一個節點不可達的時間超過NODE_TIMEOUT ,則會將這個節點標記為PFAIL,也就是Possible failure(可能下線)。節點不可達是說一個節點發送了ping包,但是等待了超過NODE_TIMEOUT時間仍然沒有收到回應。這也就意味著,NODE_TIMEOUT必須大于一個網絡包來回的時間。
FAIL flag
PFAIL標志只是一個節點本地的信息,為了使slave提升為master,需要將PFAIL升級為FAIL。PFAIL升級為FAIL需要滿足一些條件:
- A節點將B節點標記為PFAIL
- A節點通過gossip消息收集其他大部分master節點標識的B節點的狀態
- 大部分master節點在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT時間段內,標識B節點為PFAIL或FAIL
如果滿足以上條件,A節點會將B節點標識為FAIL并且向所有節點發送B節點FAIL的消息。收到消息的節點也都會將B標為FAIL。
FAIL狀態是單向的,只能從PFAIL升級為FAIL,而不能從FAIL降為PFAIL。不過存在一些清除FAIL狀態的情況:
- 節點重新可達,并且是slave節點
- 節點重新可達,并且是master節點,但是不提供任何slot服務
- 節點重新可達,并且是master節點,但是長時間沒有slave被提升為master來頂替它
PFAIL提升到FAIL使用的是一種弱協議:
- 節點收集的狀態不在同一時間點,我們會丟棄時間較早的報告信息,但是也只能保證節點的狀態在一段時間內大部分master達成了一致
- 檢測到一個FAIL后,需要通知所有節點,但是沒有辦法保證每個節點都能成功收到消息
由于是弱協議,Redis Cluster只要求所有節點對某個節點的狀態最終保持一致。如果大部分master認為某個節點FAIL,那么最終所有節點都會將其標為FAIL。而如果只有一小部分master節點認為某個節點FAIL,slave并不會被提升為master,因此,FAIL狀態將會被清除。
搭建
原理說了這么多,我們一定要來親自動手搭建一個Redis Cluster,下面演示一個在一臺機器上模擬搭建3主3從的Redis Cluster。當然,如果你想了解更多Redis Cluster的其他原理,可以點擊閱讀原文查看官網的介紹。
Redis環境
首先要搭建起我們需要的Redis環境,這里啟動6個Redis實例,端口號分別是6379、6380、6479、6480、6579、6580
拷貝6份Redis配置文件并進行如下修改(以6379為例,端口號和配置文件根據需要修改):
1port 6379
2cluster-enabled yes
3cluster-config-file nodes6379.conf
4Appendonly yes
配置文件的名稱也需要修改,修改完成后,分別啟動6個實例(圖片中有一個端口號改錯了……)。
Redis instances
創建Redis Cluster
實例啟動完成后,就可以創建Redis Cluster了,如果Redis的版本是3.x或4.x,需要使用一個叫做redis-trib的工具,而對于Redis5.0之后的版本,Redis Cluster的命令已經集成到了redis-cli中了。這里我用的是Redis5,所以沒有再單獨安裝redis-trib工具。
接下來執行命令
1redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6479 127.0.0.1:6480 127.0.0.1:6579 127.0.0.1:6580 --cluster-replicas 1
當你看到輸出了
1[OK] All 16384 slots covered
就表示Redis Cluster已經創建成功了。
查看節點信息
此時我們使用cluster nodes 命令就可查看Redis Cluster的節點信息了。
cluster nodes
可以看到,6379、6380和6479三個節點被配置為master節點。
reshard
接下來我們再來嘗試一下reshard操作
reshard_start
如圖,輸入命令
1redis-cli --cluster reshard 127.0.0.1:6380
Redis Cluster會問你要移動多少個槽位,這里我們移動1000個,接著會詢問你要移動到哪個節點,這里我們輸入6479的NODE ID
reshard_end
reshard完成后,可以輸入命令查看節點的情況
1redis-cli --cluster check 127.0.0.1:6480
可以看到6479節點已經多了1000個槽位了,分別是0-498和5461-5961。
新增master節點
add_node我們可以使用add-node命令為Redis Cluster新增master節點,可以看到我們增加的是6679節點,新增成功后,并不會為任何slot提供服務。
新增slave節點
add_slave
我們也可以用add-node命令新增slave節點,只不過需要加上--cluster-slave參數,并且使用--cluster-master-id指明新增的slave屬于哪個master。
總結
最后來總結一下,我們介紹了
Redis Cluster的特性:寫安全、可用性、性能
Key分配模型:使用CRC16算法,如果需要分配到相同的slot,可以使用tag
兩種重定向:MOVED和ASK
容錯機制:PFAIL和FAIL兩種狀態
最后又動手搭建了一個實驗的Redis Cluster。






