前面我們介紹了國人自己開發的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錯誤:
GET x
-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為例,埠號和配置檔案根據需要修改):
port 6379
cluster-enabled yes
cluster-config-file nodes6379.conf
appendonly yes
複製程式碼
配置檔案的名稱也需要修改,修改完成後,分別啟動6個例項(圖片中有一個埠號改錯了……)。
建立Redis Cluster
例項啟動完成後,就可以建立Redis Cluster了,如果Redis的版本是3.x或4.x,需要使用一個叫做redis-trib的工具,而對於Redis5.0之後的版本,Redis Cluster的命令已經整合到了redis-cli中了。這裡我用的是Redis5,所以沒有再單獨安裝redis-trib工具。
接下來執行命令
redis-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
複製程式碼
當你看到輸出了
[OK] All 16384 slots covered
複製程式碼
就表示Redis Cluster已經建立成功了。
檢視節點資訊
此時我們使用cluster nodes 命令就可檢視Redis Cluster的節點資訊了。
可以看到,6379、6380和6479三個節點被配置為master節點。
reshard
接下來我們再來嘗試一下reshard操作
如圖,輸入命令
redis-cli --cluster reshard 127.0.0.1:6380
複製程式碼
Redis Cluster會問你要移動多少個槽位,這裡我們移動1000個,接著會詢問你要移動到哪個節點,這裡我們輸入6479的NODE ID
reshard完成後,可以輸入命令檢視節點的情況
redis-cli --cluster check 127.0.0.1:6480
複製程式碼
可以看到6479節點已經多了1000個槽位了,分別是0-498和5461-5961。
新增master節點
我們可以使用add-node命令為Redis Cluster新增master節點,可以看到我們增加的是6679節點,新增成功後,並不會為任何slot提供服務。新增slave節點
我們也可以用add-node命令新增slave節點,只不過需要加上--cluster-slave引數,並且使用--cluster-master-id指明新增的slave屬於哪個master。
總結
最後來總結一下,我們介紹了
Redis Cluster的特性:寫安全、可用性、效能
Key分配模型:使用CRC16演算法,如果需要分配到相同的slot,可以使用tag
兩種重定向:MOVED和ASK
容錯機制:PFAIL和FAIL兩種狀態
最後又動手搭建了一個實驗的Redis Cluster。