請求路由
目前我們已經(jīng)搭建好Redis集群并且理解了通信和伸縮細(xì)節(jié),但還沒有使用客戶端去操作集群。Redis集群對客戶端通信協(xié)議做了比較大的修改,為了追求性能最大化,并沒有采用代理的方式而是采用客戶端直連節(jié)點(diǎn)的方式。因此對于希望從單機(jī)切換到集群環(huán)境的應(yīng)用需要修改客戶端代碼。本節(jié)我們關(guān)注集群請求路由的細(xì)節(jié),以及客戶端如何高效的操作集群。
-
請求重定向
在集群模式下,Redis接收任何鍵相關(guān)命令時(shí)首先計(jì)算對應(yīng)的槽,再根據(jù)槽找出所對應(yīng)的節(jié)點(diǎn),如果節(jié)點(diǎn)是自身,則處理鍵命令;否則回復(fù)MOVED重定向錯(cuò)誤,通知客戶端請求正確的節(jié)點(diǎn)。這個(gè)過程稱為MOVED重定向。
redis-cli自動(dòng)幫我們連接到正確的節(jié)點(diǎn)執(zhí)行命令,這個(gè)過程是在redis-cli內(nèi)部維護(hù),實(shí)質(zhì)上是client端接到MOVED信息之后再次發(fā)起請求,并不在Redis節(jié)點(diǎn)中完成轉(zhuǎn)發(fā)。節(jié)點(diǎn)對于不屬于它的鍵命令只回復(fù)重定向響應(yīng),并不負(fù)責(zé)轉(zhuǎn)發(fā)。熟悉Cassandra的喲農(nóng)戶希望在這里做好區(qū)分,不要混淆。正因?yàn)榧耗J较掳呀馕霭l(fā)起重定向的過程放到客戶端完成,所以集群客戶端協(xié)議相對于單機(jī)有了很大的變化。
鍵命令執(zhí)行步驟主要分為兩步:計(jì)算槽,查找槽對應(yīng)的節(jié)點(diǎn)。節(jié)點(diǎn)對于判定鍵命令是執(zhí)行還是MOVED重定向,都是借助slots [CLUSTER_SLOTS]數(shù)組實(shí)現(xiàn)。根據(jù)MOVED重定向機(jī)制,客戶端可以隨機(jī)連接集群內(nèi)任意Redis獲取鍵所在節(jié)點(diǎn),這種客戶端又叫Dummy(傀儡)客戶端,它優(yōu)點(diǎn)是代碼實(shí)現(xiàn)簡單,對客戶端協(xié)議影響較小,只需要根據(jù)重定向信息再次發(fā)送請求即可。但是它的弊端很明顯,每次執(zhí)行鍵命令前都要到Redis上進(jìn)行重定向才能找到要執(zhí)行命令的節(jié)點(diǎn),額外增加了IO開銷,這不是Redis集群高效的使用方式。正因?yàn)槿绱送ǔ<嚎蛻舳硕疾捎昧硪环N實(shí)現(xiàn):Smart(智能)客戶端。
-
Smart客戶端
-
客戶端原理
大多數(shù)開發(fā)語言的Redis客戶端都采用Smart客戶端支持集群協(xié)議,從中找出符合自己要求的客戶端類庫。Smart客戶端通過在內(nèi)部維護(hù)slot->node的映射關(guān)系,本地就可實(shí)現(xiàn)鍵到節(jié)點(diǎn)的查找,從而保證IO效率的最大化,而MOVED重定向負(fù)責(zé)協(xié)助Smart客戶端更新slot->node映射。Smart客戶端操作集群的流程如下:
1)首先在JedisCluster初始化是會(huì)選擇一個(gè)運(yùn)行節(jié)點(diǎn),初始化槽和節(jié)點(diǎn)映射關(guān)系,使用cluster slots命令完成。
2)Jedis Cluster解析cluster slots結(jié)果緩存到本地,并為每個(gè)節(jié)點(diǎn)創(chuàng)建唯一的JedisPool連接池。
3)JedisCluster執(zhí)行鍵命令的過程有些復(fù)雜,但是理解這個(gè)過程對于開發(fā)人員分析定位問題非常有幫助。鍵命令執(zhí)行流程:
計(jì)算slot并根據(jù)slots緩存獲取目標(biāo)節(jié)點(diǎn)連接,發(fā)送命令。
如果出現(xiàn)連接錯(cuò)誤,使用隨機(jī)連接重新執(zhí)行鍵命令,每次命令重試對redi-rections參數(shù)減1.
捕獲到MOVED重定向錯(cuò)誤,使用cluster slots命令更新slots緩存(renewSlotCache方法)。
重復(fù)執(zhí)行第一步和第三步,知道命令執(zhí)行成功,或者當(dāng)redirections<=0時(shí)拋出JedsiClusterMaxRedirectionsException異常。
從上面流程中發(fā)現(xiàn),客戶端需要結(jié)合異常和重試機(jī)制時(shí)刻保證跟Redis集群的slots同步,因此Smart客戶端相比單機(jī)客戶端有了很大的變化和實(shí)現(xiàn)難度。了解命令執(zhí)行流程后,下面我們對Smart客戶端成本和可能存在的問題進(jìn)行分析:
1)客戶端內(nèi)部維護(hù)slots緩存表,并且針對每個(gè)節(jié)點(diǎn)維護(hù)連接池,當(dāng)集群規(guī)模非常大時(shí),客戶端會(huì)維護(hù)非常多的連接并消耗更多的內(nèi)存。
2)使用Jedis湊走集群是最常見的錯(cuò)誤是:
throw new JedisClusterMaxRedirectionsExceptions("Too many Cluster redirections?");這經(jīng)常會(huì)引起開發(fā)人員的疑惑,它隱藏了內(nèi)部錯(cuò)誤細(xì)節(jié),原因是節(jié)點(diǎn)宕機(jī)或請求超時(shí)都會(huì)拋出JedisConnectionException,導(dǎo)致觸發(fā)了隨機(jī)重試,當(dāng)重試次數(shù)耗盡拋出這個(gè)錯(cuò)誤。
3)當(dāng)出現(xiàn)JedisConnectionException時(shí),Jedis認(rèn)為可能是集群節(jié)點(diǎn)故障需要隨機(jī)重試來更新slots緩存,因此了解哪些異常將拋出JedisConnectionException變得非常重要,有如下幾種情況會(huì)拋出JedisConnectionException:
Jedis連接節(jié)點(diǎn)發(fā)生socket錯(cuò)誤時(shí)拋出。
所有命令/Lua囧愛本讀寫超時(shí)拋出。
JedisPool連接池獲取可用Jedis對象超時(shí)拋出。
前兩點(diǎn)都可能是節(jié)點(diǎn)故障需要通過JedisConnectionException來更新slots緩存,但是第三點(diǎn)沒有必要,因此Jedis2.8.1版本之后對于連接池的超時(shí)拋出JedisException,從而避免觸發(fā)隨機(jī)重試機(jī)制。
4)Redis集群支持自動(dòng)故障轉(zhuǎn)移,但是從故障發(fā)現(xiàn)到完成轉(zhuǎn)移需要一定的時(shí)間,節(jié)點(diǎn)宕機(jī)期間所有指向這個(gè)節(jié)點(diǎn)的名都會(huì)觸發(fā)隨機(jī)重試,每次收到MOVED重定向后會(huì)調(diào)用JedisClusterInfoCache類的renewSlotCache方法。獲得寫鎖后再執(zhí)行cluster slots命令初始化緩存,由于集群所有的鍵命令都會(huì)執(zhí)行g(shù)etSlotPool方法方法計(jì)算槽對應(yīng)節(jié)點(diǎn),它內(nèi)部要求讀鎖。ReentrantReadWriteLock是讀鎖共享且讀寫鎖互斥,從而導(dǎo)致所有的請求都會(huì)造成阻塞。對于并發(fā)量高的場景將極大地影響集群吞吐。這個(gè)現(xiàn)象稱為cluster slots風(fēng)暴,有如下現(xiàn)象:
重試機(jī)制導(dǎo)致IO通信放大問題。比如默認(rèn)重試5次的情況,當(dāng)拋出JedisClusterMaxRedirectionsException異常時(shí),內(nèi)部最少需要9次IO通信:5次發(fā)送命令+2次ping命令保證隨機(jī)節(jié)點(diǎn)正常+2次cluster slots命令初始化slots緩存。導(dǎo)致異常判定時(shí)間變長。
個(gè)別節(jié)點(diǎn)操作異常導(dǎo)致頻繁的更新slots緩存,多次調(diào)用cluster slots命令,高并發(fā)是將過度消耗Redis節(jié)點(diǎn)資源,如果集群slot<->映射龐大則cluster slots返回信息越多,問題越嚴(yán)重。
頻繁觸發(fā)更新本地slots緩存操作,內(nèi)部使用了寫鎖,阻塞對集群所有的鍵命令調(diào)用。
針對以上問題在Jedis2.8.2版本做了改進(jìn):
當(dāng)接收到JedisConnectionException時(shí)不再輕易初始化slots緩存,大幅降低內(nèi)部IO次數(shù)。邏輯為只有當(dāng)重試次數(shù)到最后一次或者出現(xiàn)MovedDataException時(shí)才更新slots操作,降低了cluster slots命令代用次數(shù)。
當(dāng)更新slots緩存時(shí),不再使用ping命令檢測節(jié)點(diǎn)活躍度,并且使用redis covering變量保證同一時(shí)刻只有一個(gè)線程更新slots緩存,其他線程忽略,優(yōu)化了寫鎖阻塞和cluster slots調(diào)用次數(shù)。
綜上所述,當(dāng)出現(xiàn)JedisConnectionException時(shí),命令發(fā)送次數(shù)變?yōu)?次:4次重試命令+1次cluster slots命令,同時(shí)避免了cluster slots不必要的并發(fā)調(diào)用。
開發(fā)提示:
執(zhí)行cluster slots的過程不需要加入任何讀寫鎖,因?yàn)閏luster slots命令執(zhí)行不需要做并發(fā)控制,值由修改本地slots時(shí)才需要控制并發(fā),這樣降低了寫鎖持有時(shí)間。
當(dāng)獲取新的slots映射后使用讀鎖跟老slots比對,只有新老slots不一致時(shí)再加入寫鎖進(jìn)行更新。防止集群slots映射沒有變化時(shí)進(jìn)行不必要的加寫鎖行為。
-
Smart客戶端——JedisCluster
(1)JedisCluster的定義
Jedis為Redis Cluster提供了Smart客戶端,對應(yīng)的類是JedisCluster,它的初始化方法如下:
public JedisCluster (Set<HostAndPort> jedisClusterNode, int connectionTiemout, int soTimeout, int maxAttempts, final GenericObjectPoolConfig poolConfig) { ... }其中包含了5個(gè)參數(shù):
Set<HostAndPort> jedisClusterNode:所有Redis Cluster節(jié)點(diǎn)信息(也可以是一部分,因?yàn)榭蛻舳丝梢酝ㄟ^cluster slots自動(dòng)發(fā)現(xiàn))。
int connectionTimeout:連接超時(shí)。
int soTimeout:讀寫超時(shí)。
int maxAttempts:重試次數(shù)。
GenericObjectPoolConfig poolConfig:連接池參數(shù),JedisCluster會(huì)為Redis Cluster的每個(gè)節(jié)點(diǎn)創(chuàng)建連接池。對于JedisCluster的使用需要注意以下幾點(diǎn):
JedisCluster包含了所有節(jié)點(diǎn)的連接池(JedisPool),所以建議JedisCluster使用單例。
JedisCluster每次操作完成后,不需要管理連接池的借還,它在內(nèi)部已經(jīng)完成。
JedisCluster一般不要執(zhí)行close(),它會(huì)將所有JedisPool執(zhí)行destroy操作。
(2)多節(jié)點(diǎn)命令和操作。
Redis Cluster雖然提供了分布式的特性,但是有些命令或者操作,諸如keys、flushall、刪除 指定模式的鍵,需要遍歷所有節(jié)點(diǎn)才可以完成。具體分為如下幾個(gè)步驟:
通過jedisCluster.getClusterNodes()獲取所有節(jié)點(diǎn)的連接池。
使用info replication篩選上一步中的主節(jié)點(diǎn)。
比那里主節(jié)點(diǎn),使用scan命令找到指定模式的key,使用Pipeline機(jī)制刪除。
(3)批量操作的方法
Redis Cluster中,由于key分布到各個(gè)節(jié)點(diǎn)上,會(huì)造成無法實(shí)現(xiàn)mget、mset等功能。但是可以利用CRC16算法計(jì)算出key對應(yīng)的slot,以及Smart客戶端保存了slot和節(jié)點(diǎn)對應(yīng)關(guān)系的特性,將屬于同一個(gè)Redis節(jié)點(diǎn)的key進(jìn)行歸檔,然后分別對每個(gè)節(jié)點(diǎn)對應(yīng)的子key列表執(zhí)行mget或者pipeline操作。
(4)使用Lua、事務(wù)等特性的方法
Lua和事務(wù)需要所操作的key,必須在一個(gè)節(jié)點(diǎn)上,不過Redis Cluster提供了hashtag,如果開發(fā)人員確實(shí)需要使用Lua或者事務(wù),可以將所要操作的key使用一個(gè)hashtag。具體操作步驟如下:
將事務(wù)中所有的key添加hashtag。
使用CRC16計(jì)算hashtag對應(yīng)的slot。
獲取指定slot對應(yīng)的節(jié)點(diǎn)連接池JedisPool。
在JedisPool上執(zhí)行事務(wù)。
-
-
ASK重定向
-
客戶端ASK重定向流程
Redis集群支持在線遷移槽(slot)和數(shù)據(jù)來完成水平伸縮,當(dāng)slot對應(yīng)的數(shù)據(jù)從源節(jié)點(diǎn)到目標(biāo)節(jié)點(diǎn)遷移過程中,客戶端需要做到只能識別,保證鍵命令可正常執(zhí)行。例如當(dāng)一個(gè)slot數(shù)據(jù)從源節(jié)點(diǎn)遷移到目標(biāo)節(jié)點(diǎn)時(shí),期間可能出現(xiàn)一部分?jǐn)?shù)據(jù)在源節(jié)點(diǎn),而另一部分在目標(biāo)節(jié)點(diǎn)。
當(dāng)出現(xiàn)上述情況時(shí),客戶端鍵命令執(zhí)行流程將發(fā)生變化:
客戶端根據(jù)本地slots緩存發(fā)送命令道源節(jié)點(diǎn),如果存在鍵對象則直接執(zhí)行并返回結(jié)果給客戶端。
如果鍵對象不存在,則可能存在于目標(biāo)節(jié)點(diǎn),這時(shí)源節(jié)點(diǎn)會(huì)恢復(fù)ASK重定向異常。格式如下:(error) ASK {slot} {targetIP} : {targetPort}.
客戶端從ASK重定向異常提取出目標(biāo)節(jié)點(diǎn)信息,發(fā)送asking命令道目標(biāo)節(jié)點(diǎn)打開客戶端連接標(biāo)識,再執(zhí)行鍵命令。如果存在則執(zhí)行,不存在則返回不存在信息。
ASK與MOVED雖然都是對客戶端的重定向控制,但是有著本質(zhì)區(qū)別。ASK重定向說明集群正在進(jìn)行slot數(shù)據(jù)遷移,客戶端無法知道什么時(shí)候遷移完成,因此只能是臨時(shí)性的重定向,客戶端不會(huì)更新slots緩存。但是MOVED重定向說明鍵對應(yīng)的槽已經(jīng)明確指定到新的節(jié)點(diǎn),因此需要更新slots緩存。
-
節(jié)點(diǎn)內(nèi)部處理
為了支持ASK重定向,源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)在內(nèi)部的clusterState結(jié)構(gòu)中維護(hù)當(dāng)前正在遷移的槽信息,用于識別槽遷移情況。節(jié)點(diǎn)每次接收到鍵命令是,都會(huì)根據(jù)clusterState內(nèi)的遷移屬性進(jìn)行命令處理,如下所示:
如果鍵所在的槽由當(dāng)前節(jié)點(diǎn)負(fù)責(zé),但鍵不存在則查找migrating_slots_to數(shù)組查看槽是否正在遷出,如果是返回ASK重定向。
如果客戶端發(fā)送asking命令打開了CLIENT_ASKING標(biāo)識,則該客戶端下次發(fā)送鍵命令時(shí)查找importing_slots_from數(shù)組獲取clusterNode,如果指向自身則執(zhí)行命令。需要注意的是,asking命令時(shí)一次性命令,每次執(zhí)行完后客戶端標(biāo)識都會(huì)修改回原狀態(tài),因此每次客戶端接收ASK重定向后都需要發(fā)送asking命令。
批量操作。ASK重定向?qū)捂I命令支持的很完善,但是,在開發(fā)送我們經(jīng)常使用批量操作,如mget或pipeline。當(dāng)槽處于遷移狀態(tài)是,批量操作會(huì)受到影響。
使用smart客戶端批量操作集群時(shí),需要評估m(xù)get/mset、Pipeline等方式在slot遷移場景下的容錯(cuò)性,防止集群遷移造成大量錯(cuò)誤和數(shù)據(jù)丟失的情況。
開發(fā)提示:集群環(huán)境下對于使用批量操作的場景,建議優(yōu)先使用Pipeline方式,在客戶端實(shí)現(xiàn)對ASK重定向的正確處理,這樣既可以受益于批量操作的IO優(yōu)化,又可以兼容slot遷移場景。
-