與親生的Redis Cluster,來一次親密接觸

小姐姐味道發表於2019-07-29

與親生的Redis Cluster,來一次親密接觸

更多精彩文章。

《微服務不是全部,只是特定領域的子集》

《“分庫分表" ?選型和流程要慎重,否則會失控》

這麼多監控元件,總有一款適合你

《使用Netty,我們到底在開發些什麼?》

《這可能是最中肯的Redis規範了》

《程式設計師畫像,十年沉浮》

最有用系列:

《Linux生產環境上,最常用的一套“vim“技巧》

《Linux生產環境上,最常用的一套“Sed“技巧》

《Linux生產環境上,最常用的一套“AWK“技巧》

如果你認同這些知識,歡迎關注微信公眾號小姐姐味道

ID:xjjdog

筆者曾經維護過上千個redis例項,這些例項採用的簡單主從結構,叢集方案主要是客戶端jar包。剛開始,個人並不是太喜歡redis cluster,因為它的路由實在是太死板,運維複雜。為什麼這麼說,可以參考之前的一篇文章《現實中的路由規則,可能比你想象中複雜的多》

但官方在推這個東西,註定了它的應用越來越廣泛,這在平常的交流中就能夠發現。雖然有這樣那樣的缺點,但總抵擋不了權威推動的浪潮。隨著redis cluster越來越穩定,是時候和redis cluster來一次靈魂交流了。

簡介

redis cluster是親生的叢集方案,目前,在高可用和穩定性方面,都有了很大的進步。據統計和觀察,採用redis cluster架構的公司和社群越來越多,已經成為事實的標準。它的主要特點就是去中心化,無需proxy代理。其中一個主要設計目標就是達到線性可擴充套件性(linear scalability)。

僅僅靠redis cluster伺服器本身,並不能完成官方承諾的功能。廣義上的redis cluster應該既包含redis伺服器,又包含客戶端實現比如jedis等。它們是一個整體。

分散式儲存無非就是處理分片和副本。 對redis cluster來說,核心概念就是槽(slot),瞭解了它,基本就瞭解了叢集的管理方式。

優缺點

當了解這些特性以後,運維上其實是更簡單了。我們先看下比較明顯的優缺點。

優點

1、不再需要額外的Sentinel叢集,為使用者提供了一致的方案,減少了學習成本。
2、去中心架構,節點對等,叢集可支援上千個節點。
3、抽象出了slot概念,針對slot進行運維操作。
4、副本功能能夠實現自動故障轉移,大部分情況下無需人工介入。

缺點

1、客戶端要快取部分資料,實現Cluster協議,相對複雜。
2、資料是通過非同步複製的,不能保證資料的強一致性。
3、資源隔離困難,經常流量不均衡,尤其是多個業務共用叢集的時候。資料不知道在哪裡,針對熱點資料,也無法通過專項優化完成。 4、從庫是完全的冷備,無法分擔讀操作,真是太太浪費了。需要做額外工作。 5、MultiOp和Pipeline支援有限,老程式碼要是進行架構升級,要小心了。 6、資料遷移是基於key而不是基於slot的,過程較慢。

基本原理

從槽到key,定位過程明顯就是一個雙層的路由。

key的路由

redis cluster和常用的一致性hash沒什麼關係,它主要採用了雜湊槽的概念。當需要在其中存取一個key時,redis客戶端會首先對這個key採用crc16演算法算出一個值,然後對這個值進行mod操作。

crc16(key)mod 16384
複製程式碼

與親生的Redis Cluster,來一次親密接觸
所以,每個key都會落在其中的一個hash槽上。16384等同於2^14(16k),redis節點傳送心跳包時,需要把所有的槽資訊放在這個心跳包裡,所以要竭盡全力的優化,感興趣的可以看下為什麼預設的槽數量是16384。

服務端簡單原理

上面談到,redis cluster共定義了16384個槽,所有的叢集操作都是圍繞著這個槽資料進行編碼。服務端使用一個簡單的陣列儲存這些資訊。

對於判斷有無的操作,使用bitmap來儲存是最節省空間的。redis cluster就是使用一個叫做slot的陣列來儲存當前節點是否持有了這個槽。

如圖,這個陣列的長度為 16384/8=2048 Byte,那麼就可以使用0或者1來標識本節點對某個槽是否擁有。

與親生的Redis Cluster,來一次親密接觸
其實,只需要第一份資料ClusterState也能完成操作,儲存另外一個維度的Slot陣列,能夠方便編碼和儲存。一個節點除了會將自己負責處理的槽記錄在兩個地方(clusterNode結構的slots和numslots),它還會將自己的slots陣列通過訊息傳送給叢集中的其他節點,以此來告訴其他節點自己目前擁有的槽。

當資料庫中的16384個槽都有節點在處理時,叢集處於上線狀態(ok);相反地,如果資料庫中有任何一個槽沒有得到處理,那麼叢集處於下線狀態(fail)。

當客戶端向節點傳送相關命令時,接收命令的節點會計算出命令要處理的key屬於哪個槽,並檢查這個槽是否指派給了自己。如果不是自己的,會指引客戶端到正確的節點。

所以,客戶端連線叢集中的任意一臺機器,都能夠完成操作。

安裝一個6節點叢集

準備工作

假如我們要組裝一個3分片的叢集,每個分片有一個副本。那麼總共需要的node例項就有3*2=6個。redis可以通過指定配置檔案的方式啟動,我們所做的工作就是修改配置檔案。

複製6份預設的配置檔案。

for i in {0..5}  
do  
cp redis.conf  redis-700$i.conf
done  
複製程式碼

修改其中的配置檔案內容,拿redis-7000.conf來說,我們要啟用它的cluster模式。

cluster-enabled yes
port 7000
cluster-config-file nodes-7000.conf
複製程式碼

nodes-7000.conf會儲存一些叢集資訊到當前節點,所以需要獨立。

啟動&關閉

同樣的,我們使用指令碼來啟動它。

for i in {0..5}
do
nohup ./redis-server redis-700$i.conf &
done
複製程式碼

為了演示,我們暴力把它關閉。

ps -ef| grep redis | awk '{print $2}' | xargs kill -9
複製程式碼

組合叢集

我們使用redis-cli進行叢集的組合。redis將自動完成這個過程。這一系列的過程,是通過傳送指令給每個節點進行組合的。

./redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
複製程式碼

與親生的Redis Cluster,來一次親密接觸

幾個高階原理

節點故障

叢集中的每個節點都會定期地向叢集中的其他節點傳送ping訊息,以此來檢測對方是否線上,如果接收ping訊息的節點沒有在規定的時間內返回pong訊息,那麼傳送ping訊息的節點就會將接收ping訊息的節點標記為疑似下線(PFAIL)。

如果在一個叢集裡面,半數以上節點都將某個主節點x報告為疑似下線,那麼這個主節點x將被標記為已下線(FAIL),將x標記為FAIL的節點會向叢集廣播一條關於x的FAIL訊息,所有收到這條FAIL訊息的節點都會立即將x標記為FAIL。

大家可以注意到這個過程,與es和zk的節點判斷類似,都是半數以上才進行判斷,所以主節點的數量一般都是奇數。由於沒有最小組群配置,理論上會有腦裂(暫時並未遇到過)。

主從切換

當一個節點發現自己的主節點進入fail狀態,將會從這個節點的從節點當中,選出一臺,執行slaveof no one命令,變身為主節點。

新的節點完成自己的槽指派以後,會向叢集廣播一條pong訊息,以便讓其他節點立即知道自己的這些變化。它告訴別人:我已經是主節點了,我已經接管了有問題的節點,成為了它的替身。

redis內部對叢集的這些管理,大量使用了已經定義好的這些指令。所以這些指令不僅僅供我們從命令列使用,redis自己內部也用。

資料同步

當一臺從機連線到master之後,會傳送一個sync指令。master在收到這個指令後,會在後臺啟動存檔程式。執行完畢後,master將整個資料庫檔案傳輸到slave,這樣就完成了第一次全量同步。

接下來,master會把自己收到的變更指令,依次傳送給slave,從而達到資料的最終同步。從redis 2.8開始,就支援主從複製的斷點續傳,如果主從複製過程中,網路連線斷掉了,那麼可以接著上次複製的地方,繼續複製下去,而不是從頭開始複製一份。

資料丟失

redis cluster中節點之間使用非同步複製,並沒有類似kafka這種ack的概念。節點之間通過gossip協議交換狀態資訊,用投票機制完成Slave到Master的角色提升,完成這個過程註定了需要時間。在發生故障的過程中就容易存在視窗,導致丟失寫入的資料。比如以下兩種情況。

一、命令已經到到master,此時資料並沒有同步到slave,master會對客戶端回覆ok。如果這個時候主節點當機,那麼這條資料將會丟失。redis這樣做會避免很多問題,但對一個對資料可靠性要求較高的系統,是不可忍受的。

二、由於路由表是在客戶端存放的,存在一個時效問題。如果分割槽導致一個節點不可達,提升了某個從節點,但原來的主節點在這個時候又可以用了(並未完成failover)。這個時候一旦客戶端的路由表並沒有更新,那麼它將會把資料寫到錯誤的節點,造成資料丟失。

所以redis cluster在通常情況下執行的很好,在極端情況下某些值丟失問題,目前無解。

複雜的運維

redis cluster的運維非常的繁雜,雖然已經進行了抽象,但這個過程依然不簡單。有些指令,必須在詳細瞭解它的實現原理之後,才能真正放心的去用。

與親生的Redis Cluster,來一次親密接觸
上圖就是擴容會用到的一些命令。在實際使用的過程中,可能需要多次頻繁地輸入這些命令,且輸入的過程中還要監視它的狀態,所以基本上是不可能人工跑這些命令的。

運維的入口有兩個。 一個是使用redis-cli連線任意一臺,然後傳送cluster打頭的命令,這部分命令大多數是對槽進行操作。 在開始組合叢集時,就是反覆呼叫這些命令進行的具體邏輯執行。

另外一個入口是使用redis-cli命令,加上--cluster引數和指令。這種形式主要是用來管控叢集節點資訊 ,比如增刪節點等。所以推薦使用這種方式。

redis cluster提供了非常複雜的命令,難於操作和記憶。推薦使用類似CacheCloud的工具進行管理。

下面是幾個例子。


通過向節點 A 傳送 CLUSTER MEET 命令,客戶端可以讓接收命令的節點 A 將另一個節點 B 新增到節點 A 當前所在的叢集裡面:

CLUSTER MEET  127.0.0.1 7006
複製程式碼

通過cluster addslots命令,可以將一個或者多個槽指派給某個節點負責。

127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 . . . 5000
複製程式碼

設定從節點。

CLUSTER REPLICATE <node_id>
複製程式碼

redis-cli --cluster

redis-trib.rb是官方提供的Redis Cluster的管理工具,但最新版本已經推薦使用redis-cli進行操作。

向叢集中新增新節點

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7007 --cluster-replicas 1
複製程式碼

從叢集中刪除節點

redis-cli --cluster del-node 127.0.0.1:7006 54abb85ea9874af495057b6f95e0af5776b35a52
複製程式碼

遷移槽到新的節點

redis-cli --cluster reshard 127.0.0.1:7006 --cluster-from 54abb85ea9874af495057b6f95e0af5776b35a52 --cluster-to 895e1d1f589dfdac34f8bdf149360fe9ca8a24eb  --cluster-slots 108
複製程式碼

類似的命令還有很多。

create:建立叢集
check:檢查叢集
info:檢視叢集資訊
fix:修復叢集
reshard:線上遷移slot
rebalance:平衡叢集節點slot數量
add-node:新增新節點
del-node:刪除節點
set-timeout:設定節點的超時時間
call:在叢集所有節點上執行命令
import:將外部redis資料匯入叢集

其他方案概覽

主從模式

redis最早支援的,就是M-S模式,也就是一主多從。redis單機qps可達到10w+,但是在某些高訪問量場景下,依然不太夠用。一般通過讀寫分離來增加slave,減少主機的壓力。

既然是主從架構,就面臨著同步問題,redis主從模式的同步分為全同步和部分同步。當剛建立一個從機的時候,不可避免的要進行一次全量同步。等全量同步結束之後,進入增量同步階段。這個和redis cluster是沒什麼區別的。

這種模式還是比較穩定的,但要額外做一些工作。使用者需要自行開發主從切換的功能,也就是使用哨兵去探測每個例項的健康狀況,然後通過指令進行叢集狀態的改變。

當叢集規模增大,主從模式會很快遇到瓶頸。所以一般會採用客戶端hash的方法進行擴充套件,包括類似於memcached的一致性雜湊。

與親生的Redis Cluster,來一次親密接觸
客戶端hash的路由可能會很複雜,通常會通過釋出jar包或者配置的方式維護這些meta資訊,這也給線上環境增加了很多不確定性。

不過,通過加入類似ZK主動通知的功能,將配置維護在雲端,可以顯著降低風險。筆者曾經維護過的幾千個redis節點,就是用這種方式進行管理的。

代理模式

程式碼模式在redis cluster出現之前,非常流行,比如codis。代理層通過把自己模擬成一個redis,接收來自客戶端的請求,然後按照自定義的路由邏輯進行資料分片以及遷移,而業務方不需要改動任何程式碼。除了能夠平滑的進行擴容,一些主從切換、FailOver的功能也在代理層完成,客戶端甚至可以沒有任何感知。這類程式又稱為分散式中介軟體。

一個典型的實現如下圖,背後的redis叢集甚至可以是混合的。

與親生的Redis Cluster,來一次親密接觸
但這種方式的缺點也是顯而易見的。首先,它引入了一個新的代理層,在結構上、運維上都顯複雜。需要進行大量的編碼,比如failover、讀寫分離、資料遷移等。另外,proxy層的加入,對效能也有相應的損耗。

多個proxy一般使用lvs等前置進行負載均衡的設計,如果proxy層的機器很少而後端redis的流量很高,那麼網路卡會成為主要的瓶頸。

Nginx也可以作為redis的代理層,比較專業的說法叫做Smart Proxy。這種方式較為偏門,如果你對nginx比較熟悉,不失為一種優雅的做法。

使用限制和坑

redis的速度特別的快。一般越快的東西,出問題的時候造成的後果越大。 不久之前,寫過一篇針對於redis的規範:《這可能是最中肯的Redis規範了》。規範和架構一樣,適合自己公司環境的,才是最好的,但會提供一些起碼的思路。

嚴格禁止的東西,一般都是前人踩坑的地方。除了這篇規範的內容,對於redis-cluster,補充以下幾點。

1、redis cluster號稱能夠支援1k個節點,但你最好不要這麼做。當節點數量增加到10,就能夠感受到叢集的一些抖動。這麼大的叢集證明你的業務已經很牛x了,考慮一下客戶端分片吧。

2、一定要避免產生熱點,如果流量全部打到了某個節點,後果一般很嚴重。

3、大key不要放redis,它會產生大量的慢查詢,影響正常的查詢。

4、如果你不是作為儲存,快取一定要設定過期時間。佔著茅坑不拉屎的感覺是非常討厭的。

5、大流量,不要開aof,開rdb即可。

6、redis cluster的操作,少用pipeline,少用multi-key,它們會產生大量不可預料的結果。

以上是一些補充,更全還是參考規範吧 《這可能是最中肯的Redis規範了》。。

End

redis的程式碼才那麼一丁點,肯定不會實現非常複雜的分散式供能。redis的定位就是效能、水平伸縮和可用性,對於簡單的、一般流量的應用,已經足夠了。生產環境無小事,對於複雜的高併發應用,註定了是一個組合的優化方案。

與親生的Redis Cluster,來一次親密接觸

相關文章