Redis中切片集群詳解
一.切片集群
Redis中,數(shù)據(jù)增多了,是該加內(nèi)存還是加實(shí)例?
采用云主機(jī)來(lái)運(yùn)行 Redis 實(shí)例,那么,該如何選擇云主機(jī)的內(nèi)存容量呢?
用 Redis 保存 5000 萬(wàn)個(gè)鍵值對(duì),每個(gè)鍵值對(duì)大約是 512B
方案一:大內(nèi)存云主機(jī):選擇一臺(tái) 32GB 內(nèi)存的云主機(jī)來(lái)部署 Redis。因?yàn)?32GB 的內(nèi)存能保存所有數(shù)據(jù),而且還留有 7GB,可以保證系統(tǒng)的正常運(yùn)行。同時(shí),我還采用 RDB 對(duì)數(shù)據(jù)做持久化,以確保 Redis 實(shí)例故障后,還能從 RDB 恢復(fù)數(shù)據(jù)。
結(jié)果:Redis 的響應(yīng)有時(shí)會(huì)非常慢,使用 INFO 命令查看 Redis 的 latest_fork_usec 指標(biāo)值(表示最近一次 fork 的耗時(shí)),結(jié)果顯示這個(gè)指標(biāo)值特別高,快到秒級(jí)別了。這跟 Redis 的持久化機(jī)制有關(guān)系。在使用 RDB 進(jìn)行持久化時(shí),Redis 會(huì) fork 子進(jìn)程來(lái)完成,fork 操作的用時(shí)和 Redis 的數(shù)據(jù)量是正相關(guān)的,而 fork 在執(zhí)行時(shí)會(huì)阻塞主線程。數(shù)據(jù)量越大,fork 操作造成的主線程阻塞的時(shí)間越長(zhǎng)。所以,在使用 RDB 對(duì) 25GB 的數(shù)據(jù)進(jìn)行持久化時(shí),數(shù)據(jù)量較大,后臺(tái)運(yùn)行的子進(jìn)程在 fork 創(chuàng)建時(shí)阻塞了主線程,于是就導(dǎo)致 Redis 響應(yīng)變慢了。
方案二:Redis 的切片集群。雖然組建切片集群比較麻煩,但是它可以保存大量數(shù)據(jù),而且對(duì) Redis 主線程的阻塞影響較小。
如果把 25GB 的數(shù)據(jù)平均分成 5 份(當(dāng)然,也可以不做均分),使用 5 個(gè)實(shí)例來(lái)保存,每個(gè)實(shí)例只需要保存 5GB 數(shù)據(jù)。如下圖所示:

那么,在切片集群中,實(shí)例在為 5GB 數(shù)據(jù)生成 RDB 時(shí),數(shù)據(jù)量就小了很多,fork 子進(jìn)程一般不會(huì)給主線程帶來(lái)較長(zhǎng)時(shí)間的阻塞。采用多個(gè)實(shí)例保存數(shù)據(jù)切片后,我們既能保存 25GB 數(shù)據(jù),又避免了 fork 子進(jìn)程阻塞主線程而導(dǎo)致的響應(yīng)突然變慢。
1.什么是切片集群?
切片集群,也叫分片集群,就是指啟動(dòng)多個(gè) Redis 實(shí)例組成一個(gè)集群,然后按照一定的規(guī)則,把收到的數(shù)據(jù)劃分成多份,每一份用一個(gè)實(shí)例來(lái)保存。
2.如何保存更多的數(shù)據(jù)?
2.1橫向擴(kuò)展與縱向擴(kuò)展
上邊案例中使用了大內(nèi)存云片機(jī)和切片集群的方法。這兩種方法分別對(duì)應(yīng)著Redis應(yīng)對(duì)的數(shù)據(jù)量增多的兩種方案:縱向擴(kuò)展(sclae up)和橫向擴(kuò)展(scale out).
- 縱向擴(kuò)展:升級(jí)單個(gè) Redis 實(shí)例的資源配置,包括增加內(nèi)存容量、增加磁盤(pán)容量、使用更高配置的 CPU。就像下圖中,原來(lái)的實(shí)例內(nèi)存是 8GB,硬盤(pán)是 50GB,縱向擴(kuò)展后,內(nèi)存增加到 24GB,磁盤(pán)增加到 150GB。
- 橫向擴(kuò)展:橫向增加當(dāng)前 Redis 實(shí)例的個(gè)數(shù),就像下圖中,原來(lái)使用 1 個(gè) 8GB 內(nèi)存、50GB 磁盤(pán)的實(shí)例,現(xiàn)在使用三個(gè)相同配置的實(shí)例。

2.2橫向擴(kuò)展和縱向擴(kuò)展的優(yōu)缺點(diǎn)
2.2.1縱向擴(kuò)展
好處:實(shí)施起來(lái)簡(jiǎn)單、直接。
潛在問(wèn)題:
第一個(gè)問(wèn)題是,當(dāng)使用 RDB 對(duì)數(shù)據(jù)進(jìn)行持久化時(shí),如果數(shù)據(jù)量增加,需要的內(nèi)存也會(huì)增加,主線程 fork 子進(jìn)程時(shí)就可能會(huì)阻塞(比如剛剛的例子中的情況)。不過(guò),如果你不要求持久化、保存 Redis 數(shù)據(jù),那么,縱向擴(kuò)展會(huì)是一個(gè)不錯(cuò)的選擇。
第二個(gè)問(wèn)題:縱向擴(kuò)展會(huì)受到硬件和成本的限制。這很容易理解,畢竟,把內(nèi)存從 32GB 擴(kuò)展到 64GB 還算容易,但是,要想擴(kuò)充到 1TB,就會(huì)面臨硬件容量和成本上的限制了。
2.2.2橫向擴(kuò)展
橫向擴(kuò)展是一個(gè)擴(kuò)展性更好的方案。要想保存更多的數(shù)據(jù),采用這種方案的話,只用增加 Redis 的實(shí)例個(gè)數(shù)就行了,不用擔(dān)心單個(gè)實(shí)例的硬件和成本限制。在面向百萬(wàn)、千萬(wàn)級(jí)別的用戶規(guī)模時(shí),橫向擴(kuò)展的 Redis 切片集群會(huì)是一個(gè)非常好的選擇。
3.切片集群面臨兩大問(wèn)題:
數(shù)據(jù)切片后,在多個(gè)實(shí)例之間如何分布?
客戶端怎么確定想要訪問(wèn)的數(shù)據(jù)在哪個(gè)實(shí)例上?
3.1橫向擴(kuò)展:數(shù)據(jù)切片和實(shí)例的對(duì)應(yīng)分布關(guān)系
在切片集群中,數(shù)據(jù)需要分布在不同實(shí)例上,數(shù)據(jù)和實(shí)例之間如何對(duì)應(yīng)呢?
Redis Cluster 方案
在 Redis 3.0 之前,官方并沒(méi)有針對(duì)切片集群提供具體的方案。從 3.0 開(kāi)始,官方提供了一個(gè)名為 Redis Cluster 的方案,用于實(shí)現(xiàn)切片集群。Redis Cluster 方案中就規(guī)定了數(shù)據(jù)和實(shí)例的對(duì)應(yīng)規(guī)則。
3.1.1什么是Redis Cluster?
Redis Cluster 方案采用哈希槽(Hash Slot),來(lái)處理數(shù)據(jù)和實(shí)例之間的映射關(guān)系。在 Redis Cluster 方案中,一個(gè)切片集群共有 16384 個(gè)哈希槽,這些哈希槽類(lèi)似于數(shù)據(jù)分區(qū),每個(gè)鍵值對(duì)都會(huì)根據(jù)它的 key,被映射到一個(gè)哈希槽中。
具體的映射過(guò)程分為兩大步:首先根據(jù)鍵值對(duì)的 key,按照CRC16 算法計(jì)算一個(gè) 16 bit 的值;然后,再用這個(gè) 16bit 值對(duì) 16384 取模,得到 0~16383 范圍內(nèi)的模數(shù),每個(gè)模數(shù)代表一個(gè)相應(yīng)編號(hào)的哈希槽。
3.1.2哈希槽又是如何被映射到具體的 Redis 實(shí)例?
在部署 Redis Cluster 方案時(shí),可以使用 cluster create 命令創(chuàng)建集群,此時(shí),Redis 會(huì)自動(dòng)把這些槽平均分布在集群實(shí)例上。例如,如果集群中有 N 個(gè)實(shí)例,那么,每個(gè)實(shí)例上的槽個(gè)數(shù)為 16384/N 個(gè)。
也可以使用 cluster meet 命令手動(dòng)建立實(shí)例間的連接,形成集群,再使用 cluster addslots 命令,指定每個(gè)實(shí)例上的哈希槽個(gè)數(shù)
舉個(gè)例子,假設(shè)集群中不同 Redis 實(shí)例的內(nèi)存大小配置不一,如果把哈希槽均分在各個(gè)實(shí)例上,在保存相同數(shù)量的鍵值對(duì)時(shí),和內(nèi)存大的實(shí)例相比,內(nèi)存小的實(shí)例就會(huì)有更大的容量壓力。遇到這種情況時(shí),你可以根據(jù)不同實(shí)例的資源配置情況,使用 cluster addslots 命令手動(dòng)分配哈希槽。

示意圖中的切片集群一共有 3 個(gè)實(shí)例,同時(shí)假設(shè)有 5 個(gè)哈希槽,我們首先可以通過(guò)下面的命令手動(dòng)分配哈希槽:實(shí)例 1 保存哈希槽 0 和 1,實(shí)例 2 保存哈希槽 2 和 3,實(shí)例 3 保存哈希槽 4。
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1 redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3 redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4
在集群運(yùn)行的過(guò)程中,key1 和 key2 計(jì)算完 CRC16 值后,對(duì)哈希槽總個(gè)數(shù) 5 取模,再根據(jù)各自的模數(shù)結(jié)果,就可以被映射到對(duì)應(yīng)的實(shí)例 1 和實(shí)例 3 上了。
注意:在手動(dòng)分配哈希槽時(shí),需要把 16384 個(gè)槽都分配完,否則 Redis 集群無(wú)法正常工作。
切片集群就實(shí)現(xiàn)了數(shù)據(jù)到哈希槽、哈希槽再到實(shí)例的分配
即使實(shí)例有了哈希槽的映射信息,客戶端又是怎么知道要訪問(wèn)的數(shù)據(jù)在哪個(gè)實(shí)例上呢?
3.2客戶端如何定位數(shù)據(jù)?
在定位鍵值對(duì)數(shù)據(jù)時(shí),它所處的哈希槽slots是可以通過(guò)計(jì)算得到的,這個(gè)計(jì)算可以在客戶端發(fā)送請(qǐng)求時(shí)來(lái)執(zhí)行。但是,要進(jìn)一步定位到實(shí)例,還需要知道哈希槽分布在哪個(gè)實(shí)例上。
一般來(lái)說(shuō),客戶端和集群實(shí)例建立連接后,實(shí)例就會(huì)把哈希槽的分配信息發(fā)給客戶端。但是,在集群剛剛創(chuàng)建的時(shí)候,每個(gè)實(shí)例只知道自己被分配了哪些哈希槽,是不知道其他實(shí)例擁有的哈希槽信息。
客戶端為什么可以在訪問(wèn)任何一個(gè)實(shí)例時(shí),都能獲得所有的哈希槽信息呢?
Redis 實(shí)例會(huì)把自己的哈希槽信息發(fā)給和它相連接的其它實(shí)例,來(lái)完成哈希槽分配信息的擴(kuò)散。當(dāng)實(shí)例之間相互連接后,每個(gè)實(shí)例就有所有哈希槽的映射關(guān)系了。客戶端收到哈希槽信息后,會(huì)把哈希槽信息緩存在本地。當(dāng)客戶端請(qǐng)求鍵值對(duì)時(shí),會(huì)先計(jì)算鍵所對(duì)應(yīng)的哈希槽,然后就可以給相應(yīng)的實(shí)例發(fā)送請(qǐng)求了。
在集群中,實(shí)例和哈希槽的對(duì)應(yīng)關(guān)系并不是一成不變的,最常見(jiàn)的變化有兩個(gè):
- 在集群中,實(shí)例有新增或刪除,Redis 需要重新分配哈希槽;
- 為了負(fù)載均衡,Redis 需要把哈希槽在所有實(shí)例上重新分布一遍。
實(shí)例之間還可以通過(guò)相互傳遞消息,獲得最新的哈希槽分配信息
客戶端是無(wú)法主動(dòng)感知這些變化的。這就會(huì)導(dǎo)致,它緩存的分配信息和最新的分配信息就不一致了,那該怎么辦呢?
Redis Cluster 方案提供了一種重定向機(jī)制,就是指,客戶端給一個(gè)實(shí)例發(fā)送數(shù)據(jù)讀寫(xiě)操作時(shí),這個(gè)實(shí)例上并沒(méi)有相應(yīng)的數(shù)據(jù),客戶端要再給一個(gè)新實(shí)例發(fā)送操作命令。
3.2.1MOVED 重定向命令的使用方法
客戶端怎么知道重定向時(shí)新實(shí)例訪問(wèn)地址?客戶端端請(qǐng)求到了一個(gè)不包含 key 對(duì)應(yīng)的哈希槽,集群將做何響應(yīng)?
當(dāng)客戶端把一個(gè)鍵值對(duì)的操作請(qǐng)求發(fā)送給一個(gè)實(shí)例,這個(gè)實(shí)例上沒(méi)有鍵值對(duì)映射的哈希槽,這個(gè)實(shí)例就會(huì)給客戶端返回MOVED命令響應(yīng)結(jié)果,結(jié)果中就包含了新實(shí)例的訪問(wèn)地址。
get hello:key (error)MOVED 13320 172.16.19.5:6379
MOVED 命令表示,客戶端請(qǐng)求的鍵值對(duì)所在的哈希槽 13320,實(shí)際是在 172.16.19.5 這個(gè)實(shí)例上。通過(guò)返回的 MOVED 命令,就相當(dāng)于把哈希槽所在的新實(shí)例的信息告訴給客戶端了。這樣一來(lái),客戶端就可以直接和 172.16.19.5 連接,并發(fā)送操作請(qǐng)求了。
由于負(fù)載均衡,Slot 2 中的數(shù)據(jù)已經(jīng)從實(shí)例 2 遷移到了實(shí)例 3,但是,客戶端緩存仍然記錄著“Slot 2 在實(shí)例 2”的信息,所以會(huì)給實(shí)例 2 發(fā)送命令。實(shí)例 2 給客戶端返回一條 MOVED 命令,把 Slot 2 的最新位置(也就是在實(shí)例 3 上),返回給客戶端,客戶端就會(huì)再次向?qū)嵗?3 發(fā)送請(qǐng)求,同時(shí)還會(huì)更新本地緩存,把 Slot 2 與實(shí)例的對(duì)應(yīng)關(guān)系更新過(guò)來(lái)

3.2.2ASK命令使用方法
可能會(huì)出現(xiàn)這樣一種情況:屬于被遷移槽的一部分鍵值對(duì)保存在源節(jié)點(diǎn)里面,而另一部分鍵值對(duì)則保存在目標(biāo)節(jié)點(diǎn)里面。
例如:客戶端向?qū)嵗?2 發(fā)送請(qǐng)求,但此時(shí),Slot 2 中的數(shù)據(jù)只有一部分遷移到了實(shí)例 3,還有部分?jǐn)?shù)據(jù)沒(méi)有遷移。在這種遷移部分完成的情況下,客戶端就會(huì)收到一條 ASK 報(bào)錯(cuò)信息,如下所示:
Get hello:key (error)ASK 13320 172.16.19.5:6379
這個(gè)結(jié)果中的 ASK 命令就表示,客戶端請(qǐng)求的鍵值對(duì)所在的哈希槽 13320,在 172.16.19.5 這個(gè)實(shí)例上,但是這個(gè)哈希槽正在遷移。此時(shí),客戶端需要先給 172.16.19.5 這個(gè)實(shí)例發(fā)送一個(gè) ASKING 命令。這個(gè)命令的意思是,讓這個(gè)實(shí)例允許執(zhí)行客戶端接下來(lái)發(fā)送的命令。然后,客戶端再向這個(gè)實(shí)例發(fā)送 GET 命令,以讀取數(shù)據(jù).
ASK 命令表示兩層含義:第一,表明 Slot 數(shù)據(jù)還在遷移中;第二,ASK 命令把客戶端所請(qǐng)求數(shù)據(jù)的最新實(shí)例地址返回給客戶端,此時(shí),客戶端需要給實(shí)例 3 發(fā)送 ASKING 命令,然后再發(fā)送操作命令

3.2.3MOVED命令和ASK命令區(qū)別
- MOVED命令會(huì)更新客戶端緩存的哈希槽分配信息,ASK不會(huì)更新客戶端緩存。如果客戶端再次請(qǐng)求 Slot 2 中的數(shù)據(jù),它還是會(huì)給實(shí)例 2 發(fā)送請(qǐng)求。
- ASK命令作用只是讓客戶端能給新實(shí)例發(fā)送一次請(qǐng)求,而MOVED命令修改本地緩存,讓后續(xù)命令發(fā)往新實(shí)例。
3.切片集群總結(jié)
本篇主要講述了,切片集群在保存大量數(shù)據(jù)方面的優(yōu)勢(shì),以及基于哈希槽的數(shù)據(jù)分布機(jī)制和客戶端定位鍵值對(duì)的方法。
- 在應(yīng)對(duì)數(shù)據(jù)量大的數(shù)據(jù),數(shù)據(jù)擴(kuò)容時(shí),雖然增加內(nèi)存這種縱向擴(kuò)展的方式簡(jiǎn)單直接,但是會(huì)造成內(nèi)存過(guò)大,導(dǎo)致性能變慢。同事也受到硬件和成本的限制。
- Redis切片集群提供了橫向擴(kuò)展的模式,也就是使用多個(gè)實(shí)例,并給每個(gè)實(shí)例分配一定的哈希槽,數(shù)據(jù)可以通過(guò)鍵的哈希值映射到哈希槽,在通過(guò)哈希槽分散分布在不同的實(shí)例上。擴(kuò)展性好,通過(guò)增加實(shí)例可以存儲(chǔ)大量數(shù)據(jù)。
- 集群是實(shí)例的增減和為了實(shí)現(xiàn)負(fù)載均衡而進(jìn)行的數(shù)據(jù)重新分布,導(dǎo)致哈希槽和實(shí)例映射關(guān)系的變化,客戶端請(qǐng)求時(shí),會(huì)收到命令執(zhí)行報(bào)錯(cuò)信息。MOVED 和 ASK 命令,讓客戶端獲取最新信息。
- 在 Redis 3.0 之前,Redis 官方并沒(méi)有提供切片集群方案,但是,其實(shí)當(dāng)時(shí)業(yè)界已經(jīng)有了一些切片集群的方案,例如基于客戶端分區(qū)的 ShardedJedis,基于代理的 Codis、Twemproxy 等。這些方案的應(yīng)用早于 Redis Cluster 方案
Redis Cluster 方案通過(guò)哈希槽的方式把鍵值對(duì)分配到不同的實(shí)例上,這個(gè)過(guò)程需要對(duì)鍵值對(duì)的 key 做 CRC 計(jì)算,然后再和哈希槽做映射,這樣做有什么好處嗎?如果用一個(gè)表直接把鍵值對(duì)和實(shí)例的對(duì)應(yīng)關(guān)系記錄下來(lái)(例如鍵值對(duì) 1 在實(shí)例 2 上,鍵值對(duì) 2 在實(shí)例 1 上),這樣就不用計(jì)算 key 和哈希槽的對(duì)應(yīng)關(guān)系了,只用查表就行了,Redis 為什么不這么做呢?
1、整個(gè)集群存儲(chǔ)key的數(shù)量是無(wú)法預(yù)估的,key的數(shù)量非常多時(shí),直接記錄每個(gè)key對(duì)應(yīng)的實(shí)例映射關(guān)系,這個(gè)映射表會(huì)非常龐大,這個(gè)映射表無(wú)論是存儲(chǔ)在服務(wù)端還是客戶端都占用了非常大的內(nèi)存空間。
2、Redis Cluster采用無(wú)中心化的模式(無(wú)proxy,客戶端與服務(wù)端直連),客戶端在某個(gè)節(jié)點(diǎn)訪問(wèn)一個(gè)key,如果這個(gè)key不在這個(gè)節(jié)點(diǎn)上,這個(gè)節(jié)點(diǎn)需要有糾正客戶端路由到正確節(jié)點(diǎn)的能力(MOVED響應(yīng)),這就需要節(jié)點(diǎn)之間互相交換路由表,每個(gè)節(jié)點(diǎn)擁有整個(gè)集群完整的路由關(guān)系。如果存儲(chǔ)的都是key與實(shí)例的對(duì)應(yīng)關(guān)系,節(jié)點(diǎn)之間交換信息也會(huì)變得非常龐大,消耗過(guò)多的網(wǎng)絡(luò)資源,而且就算交換完成,相當(dāng)于每個(gè)節(jié)點(diǎn)都需要額外存儲(chǔ)其他節(jié)點(diǎn)的路由表,內(nèi)存占用過(guò)大造成資源浪費(fèi)。
3、當(dāng)集群在擴(kuò)容、縮容、數(shù)據(jù)均衡時(shí),節(jié)點(diǎn)之間會(huì)發(fā)生數(shù)據(jù)遷移,遷移時(shí)需要修改每個(gè)key的映射關(guān)系,維護(hù)成本高。
4、而在中間增加一層哈希槽,可以把數(shù)據(jù)和節(jié)點(diǎn)解耦,key通過(guò)Hash計(jì)算,只需要關(guān)心映射到了哪個(gè)哈希槽,然后再通過(guò)哈希槽和節(jié)點(diǎn)的映射表找到節(jié)點(diǎn),相當(dāng)于消耗了很少的CPU資源,不但讓數(shù)據(jù)分布更均勻,還可以讓這個(gè)映射表變得很小,利于客戶端和服務(wù)端保存,節(jié)點(diǎn)之間交換信息時(shí)也變得輕量。
5、當(dāng)集群在擴(kuò)容、縮容、數(shù)據(jù)均衡時(shí),節(jié)點(diǎn)之間的操作例如數(shù)據(jù)遷移,都以哈希槽為基本單位進(jìn)行操作,簡(jiǎn)化了節(jié)點(diǎn)擴(kuò)容、縮容的難度,便于集群的維護(hù)和管理。
請(qǐng)求路由、數(shù)據(jù)遷移
Redis使用集群方案就是為了解決單個(gè)節(jié)點(diǎn)數(shù)據(jù)量大、寫(xiě)入量大產(chǎn)生的性能瓶頸的問(wèn)題。多個(gè)節(jié)點(diǎn)組成一個(gè)集群,可以提高集群的性能和可靠性,但隨之而來(lái)的就是集群的管理問(wèn)題,最核心問(wèn)題有2個(gè):請(qǐng)求路由、數(shù)據(jù)遷移(擴(kuò)容/縮容/數(shù)據(jù)平衡)。
1、請(qǐng)求路由:一般都是采用哈希槽的映射關(guān)系表找到指定節(jié)點(diǎn),然后在這個(gè)節(jié)點(diǎn)上操作的方案。
Redis Cluster在每個(gè)節(jié)點(diǎn)記錄完整的映射關(guān)系(便于糾正客戶端的錯(cuò)誤路由請(qǐng)求),同時(shí)也發(fā)給客戶端讓客戶端緩存一份,便于客戶端直接找到指定節(jié)點(diǎn),客戶端與服務(wù)端配合完成數(shù)據(jù)的路由,這需要業(yè)務(wù)在使用Redis Cluster時(shí),必須升級(jí)為集群版的SDK才支持客戶端和服務(wù)端的協(xié)議交互。
其他Redis集群化方案例如Twemproxy、Codis都是中心化模式(增加Proxy層),客戶端通過(guò)Proxy對(duì)整個(gè)集群進(jìn)行操作,Proxy后面可以掛N多個(gè)Redis實(shí)例,Proxy層維護(hù)了路由的轉(zhuǎn)發(fā)邏輯。操作Proxy就像是操作一個(gè)普通Redis一樣,客戶端也不需要更換SDK,而Redis Cluster是把這些路由邏輯做在了SDK中。當(dāng)然,增加一層Proxy也會(huì)帶來(lái)一定的性能損耗。
2、數(shù)據(jù)遷移:當(dāng)集群節(jié)點(diǎn)不足以支撐業(yè)務(wù)需求時(shí),就需要擴(kuò)容節(jié)點(diǎn),擴(kuò)容就意味著節(jié)點(diǎn)之間的數(shù)據(jù)需要做遷移,而遷移過(guò)程中是否會(huì)影響到業(yè)務(wù),這也是判定一個(gè)集群方案是否成熟的標(biāo)準(zhǔn)。
Twemproxy不支持在線擴(kuò)容,它只解決了請(qǐng)求路由的問(wèn)題,擴(kuò)容時(shí)需要停機(jī)做數(shù)據(jù)重新分配。而Redis Cluster和Codis都做到了在線擴(kuò)容(不影響業(yè)務(wù)或?qū)I(yè)務(wù)的影響非常?。?,重點(diǎn)就是在數(shù)據(jù)遷移過(guò)程中,客戶端對(duì)于正在遷移的key進(jìn)行操作時(shí),集群如何處理?還要保證響應(yīng)正確的結(jié)果?
Redis Cluster和Codis都需要服務(wù)端和客戶端/Proxy層互相配合,遷移過(guò)程中,服務(wù)端針對(duì)正在遷移的key,需要讓客戶端或Proxy去新節(jié)點(diǎn)訪問(wèn)(重定向),這個(gè)過(guò)程就是為了保證業(yè)務(wù)在訪問(wèn)這些key時(shí)依舊不受影響,而且可以得到正確的結(jié)果。由于重定向的存在,所以這個(gè)期間的訪問(wèn)延遲會(huì)變大。等遷移完成之后,Redis Cluster每個(gè)節(jié)點(diǎn)會(huì)更新路由映射表,同時(shí)也會(huì)讓客戶端感知到,更新客戶端緩存。Codis會(huì)在Proxy層更新路由表,客戶端在整個(gè)過(guò)程中無(wú)感知。
除了訪問(wèn)正確的節(jié)點(diǎn)之外,數(shù)據(jù)遷移過(guò)程中還需要解決異常情況(遷移超時(shí)、遷移失?。⑿阅軉?wèn)題(如何讓數(shù)據(jù)遷移更快、bigkey如何處理),這個(gè)過(guò)程中的細(xì)節(jié)也很多。
Redis Cluster的數(shù)據(jù)遷移是同步的,遷移一個(gè)key會(huì)同時(shí)阻塞源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn),遷移過(guò)程中會(huì)有性能問(wèn)題。而Codis提供了異步遷移數(shù)據(jù)的方案,遷移速度更快,對(duì)性能影響最小,當(dāng)然,實(shí)現(xiàn)方案也比較復(fù)雜。
二:切片集群方案:Codis \ Redis Cluster
Redis 官方提供的切片集群方案 Redis Cluster。但是Redis Cluster 方案正式發(fā)布前,業(yè)界已經(jīng)廣泛使用的 Codis值得關(guān)注
1.Codis 的整體架構(gòu)和基本流程
Codis 集群中包含了 4 類(lèi)關(guān)鍵組件。
- codis server:這是進(jìn)行了二次開(kāi)發(fā)的 Redis 實(shí)例,其中增加了額外的數(shù)據(jù)結(jié)構(gòu),支持?jǐn)?shù)據(jù)遷移操作,主要負(fù)責(zé)處理具體的數(shù)據(jù)讀寫(xiě)請(qǐng)求。
- codis proxy:接收客戶端請(qǐng)求,并把請(qǐng)求轉(zhuǎn)發(fā)給 codis server。
- Zookeeper 集群:保存集群元數(shù)據(jù),例如數(shù)據(jù)位置信息和 codis proxy 信息。
- codis dashboard 和 codis fe:共同組成了集群管理工具。其中,codis dashboard 負(fù)責(zé)執(zhí)行集群管理工作,包括增刪 codis server、codis proxy 和進(jìn)行數(shù)據(jù)遷移。而 codis fe 負(fù)責(zé)提供 dashboard 的 Web 操作界面,便于我們直接在 Web 界面上進(jìn)行集群管理。

1.1Codis 是如何處理請(qǐng)求的
首先--》為了讓集群能接收并處理請(qǐng)求:先使用 codis dashboard 設(shè)置 codis server 和 codis proxy 的訪問(wèn)地址,完成設(shè)置后,codis server 和 codis proxy 才會(huì)開(kāi)始接收連接。
其次--》當(dāng)客戶端要讀寫(xiě)數(shù)據(jù)時(shí),客戶端直接和 codis proxy 建立連接.codis proxy 本身支持 Redis 的 RESP 交互協(xié)議.客戶端訪問(wèn) codis proxy 時(shí),和訪問(wèn)原生的 Redis 實(shí)例沒(méi)有什么區(qū)別。原本連接單實(shí)例的客戶端就可以輕松地和 Codis 集群建立起連接。
最后--》codis proxy 接收到請(qǐng)求,就會(huì)查詢(xún)請(qǐng)求數(shù)據(jù)和 codis server 的映射關(guān)系,并把請(qǐng)求轉(zhuǎn)發(fā)給相應(yīng)的 codis server 進(jìn)行處理。當(dāng) codis server 處理完請(qǐng)求后,會(huì)把結(jié)果返回給 codis proxy,proxy 再把數(shù)據(jù)返回給客戶端。

2.Codis的關(guān)鍵技術(shù)原理
2.1數(shù)據(jù)如何在集群里分布
在 Codis 集群中,一個(gè)數(shù)據(jù)應(yīng)該保存在哪個(gè) codis server 上,這是通過(guò)邏輯槽(Slot)映射來(lái)完成的,具體來(lái)說(shuō),總共分成兩步。
第一步,Codis 集群一共有 1024 個(gè) Slot,編號(hào)依次是 0 到 1023。把這些 Slot 手動(dòng)分配給 codis server,每個(gè) server 上包含一部分 Slot。也可以讓 codis dashboard 進(jìn)行自動(dòng)分配,例如,dashboard 把 1024 個(gè) Slot 在所有 server 上均分。
第二步,當(dāng)客戶端要讀寫(xiě)數(shù)據(jù)時(shí),會(huì)使用 CRC32 算法計(jì)算數(shù)據(jù) key 的哈希值,并把這個(gè)哈希值對(duì) 1024 取模。而取模后的值,則對(duì)應(yīng) Slot 的編號(hào)。此時(shí),根據(jù)第一步分配的 Slot 和 server 對(duì)應(yīng)關(guān)系,可以知道數(shù)據(jù)保存在哪個(gè) server 上了。
2.2例子
下圖顯示的就是數(shù)據(jù)、Slot 和 codis server 的映射保存關(guān)系。其中,Slot 0 和 1 被分配到了 server1,Slot 2 分配到 server2,Slot 1022 和 1023 被分配到 server8。當(dāng)客戶端訪問(wèn) key 1 和 key 2 時(shí),這兩個(gè)數(shù)據(jù)的 CRC32 值對(duì) 1024 取模后,分別是 1 和 1022。因此,它們會(huì)被保存在 Slot 1 和 Slot 1022 上,而 Slot 1 和 Slot 1022 已經(jīng)被分配到 codis server 1 和 8 上了。key 1 和 key 2 的保存位置就很清楚。

數(shù)據(jù) key 和 Slot 的映射關(guān)系是客戶端在讀寫(xiě)數(shù)據(jù)前直接通過(guò) CRC32 計(jì)算得到的,而 Slot 和 codis server 的映射關(guān)系是通過(guò)分配完成的,所以就需要用一個(gè)存儲(chǔ)系統(tǒng)保存下來(lái),否則,如果集群有故障了,映射關(guān)系就會(huì)丟失。
把 Slot 和 codis server 的映射關(guān)系稱(chēng)為數(shù)據(jù)路由表(簡(jiǎn)稱(chēng)路由表)。我們?cè)?codis dashboard 上分配好路由表后,dashboard 會(huì)把路由表發(fā)送給 codis proxy,同時(shí),dashboard 也會(huì)把路由表保存在 Zookeeper 中。codis-proxy 會(huì)把路由表緩存在本地,當(dāng)它接收到客戶端請(qǐng)求后,直接查詢(xún)本地的路由表,就可以完成正確的請(qǐng)求轉(zhuǎn)發(fā)了

在數(shù)據(jù)分布的實(shí)現(xiàn)方法上,Codis 和 Redis Cluster 很相似,都采用了 key 映射到 Slot、Slot 再分配到實(shí)例上的機(jī)制
2.3Codis 和 Redis Cluster數(shù)據(jù)分布上的區(qū)別
- Codis 中的路由表:通過(guò) codis dashboard 分配和修改的,并被保存在 Zookeeper 集群中。一旦數(shù)據(jù)位置發(fā)生變化(例如有實(shí)例增減),路由表被修改了,codis dashbaord 就會(huì)把修改后的路由表發(fā)送給 codis proxy,proxy 就可以根據(jù)最新的路由信息轉(zhuǎn)發(fā)請(qǐng)求了。
- 在 Redis Cluster 中,數(shù)據(jù)路由表是通過(guò)每個(gè)實(shí)例相互間的通信傳遞的,最后會(huì)在每個(gè)實(shí)例上保存一份。當(dāng)數(shù)據(jù)路由信息發(fā)生變化時(shí),就需要在所有實(shí)例間通過(guò)網(wǎng)絡(luò)消息進(jìn)行傳遞。所以,如果實(shí)例數(shù)量較多的話,就會(huì)消耗較多的集群網(wǎng)絡(luò)資源。
3.集群擴(kuò)容和數(shù)據(jù)遷移
Codis 集群擴(kuò)容包括了兩方面:增加 codis server 和增加 codis proxy。
3.1增加codis server
兩步操作:
- 啟動(dòng)新的 codis server,將它加入集群;
- 把部分?jǐn)?shù)據(jù)遷移到新的 server。
3.1.1數(shù)據(jù)遷移的基本流程
Codis 集群按照 Slot 的粒度進(jìn)行數(shù)據(jù)遷移,數(shù)據(jù)遷移是一個(gè)重要的機(jī)制
- 在源 server 上,Codis 從要遷移的 Slot 中隨機(jī)選擇一個(gè)數(shù)據(jù),發(fā)送給目的 server。
- 目的 server 確認(rèn)收到數(shù)據(jù)后,會(huì)給源 server 返回確認(rèn)消息。這時(shí),源 server 會(huì)在本地將剛才遷移的數(shù)據(jù)刪除。
- 第一步和第二步就是單個(gè)數(shù)據(jù)的遷移過(guò)程。Codis 會(huì)不斷重復(fù)這個(gè)遷移過(guò)程,直到要遷移的 Slot 中的數(shù)據(jù)全部遷移完成。

Codis 實(shí)現(xiàn)了兩種遷移模式,分別是同步遷移和異步遷移
3.1.2同步遷移
同步遷移是指,在數(shù)據(jù)從源 server 發(fā)送給目的 server 的過(guò)程中,源 server 是阻塞的,無(wú)法處理新的請(qǐng)求操作。這種模式很容易實(shí)現(xiàn),但是遷移過(guò)程中會(huì)涉及多個(gè)操作(包括數(shù)據(jù)在源 server 序列化、網(wǎng)絡(luò)傳輸、在目的 server 反序列化,以及在源 server 刪除),如果遷移的數(shù)據(jù)是一個(gè) bigkey,源 server 就會(huì)阻塞較長(zhǎng)時(shí)間,無(wú)法及時(shí)處理用戶請(qǐng)求。
3.1.3異步遷移
為了避免數(shù)據(jù)遷移阻塞源 server,Codis 實(shí)現(xiàn)的第二種遷移模式就是異步遷移
異步遷移的兩個(gè)關(guān)鍵特點(diǎn)
第一個(gè)特點(diǎn)是:
- 當(dāng)源 server 把數(shù)據(jù)發(fā)送給目的 server 后,就可以處理其他請(qǐng)求操作了,不用等到目的 server 的命令執(zhí)行完。而目的 server 會(huì)在收到數(shù)據(jù)并反序列化保存到本地后,給源 server 發(fā)送一個(gè) ACK 消息,表明遷移完成。此時(shí),源 server 在本地把剛才遷移的數(shù)據(jù)刪除。
- 在這個(gè)過(guò)程中,遷移的數(shù)據(jù)會(huì)被設(shè)置為只讀,所以,源 server 上的數(shù)據(jù)不會(huì)被修改,自然也就不會(huì)出現(xiàn)“和目的 server 上的數(shù)據(jù)不一致”的問(wèn)題了
第二個(gè)特點(diǎn)是:
- 于 bigkey,異步遷移采用了拆分指令的方式進(jìn)行遷移。具體來(lái)說(shuō)就是,對(duì) bigkey 中每個(gè)元素,用一條指令進(jìn)行遷移,而不是把整個(gè) bigkey 進(jìn)行序列化后再整體傳輸。這種化整為零的方式,就避免了 bigkey 遷移時(shí),因?yàn)橐蛄谢罅繑?shù)據(jù)而阻塞源 server 的問(wèn)題。
- 當(dāng) bigkey 遷移了一部分?jǐn)?shù)據(jù)后,如果 Codis 發(fā)生故障,就會(huì)導(dǎo)致 bigkey 的一部分元素在源 server,而另一部分元素在目的 server,這就破壞了遷移的原子性。所以,Codis 會(huì)在目標(biāo) server 上,給 bigkey 的元素設(shè)置一個(gè)臨時(shí)過(guò)期時(shí)間。如果遷移過(guò)程中發(fā)生故障,那么,目標(biāo) server 上的 key 會(huì)在過(guò)期后被刪除,不會(huì)影響遷移的原子性。當(dāng)正常完成遷移后,bigkey 元素的臨時(shí)過(guò)期時(shí)間會(huì)被刪除。
第二個(gè)特點(diǎn)例子:
- 假如要遷移一個(gè)有 1 萬(wàn)個(gè)元素的 List 類(lèi)型數(shù)據(jù),當(dāng)使用異步遷移時(shí),源 server 就會(huì)給目的 server 傳輸 1 萬(wàn)條 RPUSH 命令,每條命令對(duì)應(yīng)了 List 中一個(gè)元素的插入。在目的 server 上,這 1 萬(wàn)條命令再被依次執(zhí)行,就可以完成數(shù)據(jù)遷移。
- 為了提升遷移的效率,Codis 在異步遷移 Slot 時(shí),允許每次遷移多個(gè) key。可以通過(guò)異步遷移命令 SLOTSMGRTTAGSLOT-ASYNC 的參數(shù) numkeys 設(shè)置每次遷移的 key 數(shù)量
3.2增加codis proxy
Codis 集群中,客戶端是和 codis proxy 直接連接的,所以,當(dāng)客戶端增加時(shí),一個(gè) proxy 無(wú)法支撐大量的請(qǐng)求操作,就需要增加 proxy。增加 proxy 比較容易,直接啟動(dòng) proxy,再通過(guò) codis dashboard 把 proxy 加入集群就行。
此時(shí),codis proxy 的訪問(wèn)連接信息都會(huì)保存在 Zookeeper 上。所以,當(dāng)新增了 proxy 后,Zookeeper 上會(huì)有最新的訪問(wèn)列表,客戶端也就可以從 Zookeeper 上讀取 proxy 訪問(wèn)列表,把請(qǐng)求發(fā)送給新增的 proxy。這樣一來(lái),客戶端的訪問(wèn)壓力就可以在多個(gè) proxy 上分擔(dān)處理了,如下圖所示

4:客戶端能否與集群直接交互
使用 Redis 單實(shí)例時(shí),客戶端只要符合 RESP 協(xié)議,就可以和實(shí)例進(jìn)行交互和讀寫(xiě)數(shù)據(jù)。但是,在使用切片集群時(shí),有些功能是和單實(shí)例不一樣的,比如集群中的數(shù)據(jù)遷移操作,在單實(shí)例上是沒(méi)有的,而且遷移過(guò)程中,數(shù)據(jù)訪問(wèn)請(qǐng)求可能要被重定向(例如 Redis Cluster 中的 MOVE 命令)。
客戶端需要增加和集群功能相關(guān)的命令操作的支持。如果原來(lái)使用單實(shí)例客戶端,想要擴(kuò)容使用集群,就需要使用新客戶端,這對(duì)于業(yè)務(wù)應(yīng)用的兼容性來(lái)說(shuō),并不是特別友好。
Codis 集群在設(shè)計(jì)時(shí),就充分考慮了對(duì)現(xiàn)有單實(shí)例客戶端的兼容性。
Codis 使用 codis proxy 直接和客戶端連接,codis proxy 是和單實(shí)例客戶端兼容的。而和集群相關(guān)的管理工作(例如請(qǐng)求轉(zhuǎn)發(fā)、數(shù)據(jù)遷移等),都由 codis proxy、codis dashboard 這些組件來(lái)完成,不需要客戶端參與。
業(yè)務(wù)應(yīng)用使用 Codis 集群時(shí),就不用修改客戶端了,可以復(fù)用和單實(shí)例連接的客戶端,既能利用集群讀寫(xiě)大容量數(shù)據(jù),又避免了修改客戶端增加復(fù)雜的操作邏輯,保證了業(yè)務(wù)代碼的穩(wěn)定性和兼容性。
5.怎么保證集群可靠性?
可靠性是實(shí)際業(yè)務(wù)應(yīng)用的一個(gè)核心要求。對(duì)于一個(gè)分布式系統(tǒng)來(lái)說(shuō),它的可靠性和系統(tǒng)中的組件個(gè)數(shù)有關(guān):組件越多,潛在的風(fēng)險(xiǎn)點(diǎn)也就越多。
和 Redis Cluster 只包含 Redis 實(shí)例不一樣,Codis 集群包含的組件有 4 類(lèi)
Codis 不同組件的可靠性保證方法。
5.1codis server保證可靠性方法
- codis server 其實(shí)就是 Redis 實(shí)例,只不過(guò)增加了和集群操作相關(guān)的命令。Redis 的主從復(fù)制機(jī)制和哨兵機(jī)制在 codis server 上都是可以使用的,所以,Codis 就使用主從集群來(lái)保證 codis server 的可靠性。簡(jiǎn)單來(lái)說(shuō)就是,Codis 給每個(gè) server 配置從庫(kù),并使用哨兵機(jī)制進(jìn)行監(jiān)控,當(dāng)發(fā)生故障時(shí),主從庫(kù)可以進(jìn)行切換,從而保證了 server 的可靠性。
- 在這種配置情況下,每個(gè) server 就成為了一個(gè) server group,每個(gè) group 中是一主多從的 server。數(shù)據(jù)分布使用的 Slot,也是按照 group 的粒度進(jìn)行分配的。同時(shí),codis proxy 在轉(zhuǎn)發(fā)請(qǐng)求時(shí),也是按照數(shù)據(jù)所在的 Slot 和 group 的對(duì)應(yīng)關(guān)系,把寫(xiě)請(qǐng)求發(fā)到相應(yīng) group 的主庫(kù),讀請(qǐng)求發(fā)到 group 中的主庫(kù)或從庫(kù)上。
下圖展示的是配置了 server group 的 Codis 集群架構(gòu)。在 Codis 集群中,我們通過(guò)部署 server group 和哨兵集群,實(shí)現(xiàn) codis server 的主從切換,提升集群可靠性。

5.2codis proxy 和 Zookeeper可靠性
- 在 Codis 集群設(shè)計(jì)時(shí),proxy 上的信息源頭都是來(lái)自 Zookeeper(例如路由表)。而 Zookeeper 集群使用多個(gè)實(shí)例來(lái)保存數(shù)據(jù),只要有超過(guò)半數(shù)的 Zookeeper 實(shí)例可以正常工作, Zookeeper 集群就可以提供服務(wù),也可以保證這些數(shù)據(jù)的可靠性。
- 所以,codis proxy 使用 Zookeeper 集群保存路由表,可以充分利用 Zookeeper 的高可靠性保證來(lái)確保 codis proxy 的可靠性,不用再做額外的工作了。當(dāng) codis proxy 發(fā)生故障后,直接重啟 proxy 就行。重啟后的 proxy,可以通過(guò) codis dashboard 從 Zookeeper 集群上獲取路由表,然后,就可以接收客戶端請(qǐng)求進(jìn)行轉(zhuǎn)發(fā)了。這樣的設(shè)計(jì),也降低了 Codis 集群本身的開(kāi)發(fā)復(fù)雜度。
5.3codis dashboard 和 codis fe可靠性
它們主要提供配置管理和管理員手工操作,負(fù)載壓力不大,所以,它們的可靠性可以不用額外進(jìn)行保證了
6.切片集群方案選擇建議
6.1Codis 和 Redis Cluster 區(qū)別

6.2實(shí)際應(yīng)用中的兩種方案
- 從穩(wěn)定性和成熟度來(lái)看,Codis 應(yīng)用得比較早,在業(yè)界已經(jīng)有了成熟的生產(chǎn)部署。雖然 Codis 引入了 proxy 和 Zookeeper,增加了集群復(fù)雜度,但是,proxy 的無(wú)狀態(tài)設(shè)計(jì)和 Zookeeper 自身的穩(wěn)定性,也給 Codis 的穩(wěn)定使用提供了保證。而 Redis Cluster 的推出時(shí)間晚于 Codis,相對(duì)來(lái)說(shuō),成熟度要弱于 Codis,如果你想選擇一個(gè)成熟穩(wěn)定的方案,Codis 更加合適些。
- 從業(yè)務(wù)應(yīng)用客戶端兼容性來(lái)看,連接單實(shí)例的客戶端可以直接連接 codis proxy,而原本連接單實(shí)例的客戶端要想連接 Redis Cluster 的話,就需要開(kāi)發(fā)新功能。所以,如果你的業(yè)務(wù)應(yīng)用中大量使用了單實(shí)例的客戶端,而現(xiàn)在想應(yīng)用切片集群的話,建議你選擇 Codis,這樣可以避免修改業(yè)務(wù)應(yīng)用中的客戶端。
- 從使用 Redis 新命令和新特性來(lái)看,Codis server 是基于開(kāi)源的 Redis 3.2.8 開(kāi)發(fā)的,所以,Codis 并不支持 Redis 后續(xù)的開(kāi)源版本中的新增命令和數(shù)據(jù)類(lèi)型。另外,Codis 并沒(méi)有實(shí)現(xiàn)開(kāi)源 Redis 版本的所有命令,比如 BITOP、BLPOP、BRPOP,以及和與事務(wù)相關(guān)的 MUTLI、EXEC 等命令。Codis 官網(wǎng)上列出了不被支持的命令列表,你在使用時(shí)記得去核查一下。所以,如果你想使用開(kāi)源 Redis 版本的新特性,Redis Cluster 是一個(gè)合適的選擇。
- 從數(shù)據(jù)遷移性能維度來(lái)看,Codis 能支持異步遷移,異步遷移對(duì)集群處理正常請(qǐng)求的性能影響要比使用同步遷移的小。所以,如果你在應(yīng)用集群時(shí),數(shù)據(jù)遷移比較頻繁的話,Codis 是個(gè)更合適的選擇。
7.Codis 和 Redis Cluster總結(jié)
Codis 集群包含 codis server、codis proxy、Zookeeper、codis dashboard 和 codis fe 這四大類(lèi)組件。
- codis proxy 和 codis server 負(fù)責(zé)處理數(shù)據(jù)讀寫(xiě)請(qǐng)求,其中,codis proxy 和客戶端連接,接收請(qǐng)求,并轉(zhuǎn)發(fā)請(qǐng)求給 codis server,而 codis server 負(fù)責(zé)具體處理請(qǐng)求。
- codis dashboard 和 codis fe 負(fù)責(zé)集群管理,其中,codis dashboard 執(zhí)行管理操作,而 codis fe 提供 Web 管理界面。
- Zookeeper 集群負(fù)責(zé)保存集群的所有元數(shù)據(jù)信息,包括路由表、proxy 實(shí)例信息等。這里,有個(gè)地方需要你注意,除了使用 Zookeeper,Codis 還可以使用 etcd 或本地文件系統(tǒng)保存元數(shù)據(jù)信息。
Codis 使用上的小建議:當(dāng)你有多條業(yè)務(wù)線要使用 Codis 時(shí),可以啟動(dòng)多個(gè) codis dashboard,每個(gè) dashboard 管理一部分 codis server,同時(shí),再用一個(gè) dashboard 對(duì)應(yīng)負(fù)責(zé)一個(gè)業(yè)務(wù)線的集群管理,這樣,就可以做到用一個(gè) Codis 集群實(shí)現(xiàn)多條業(yè)務(wù)線的隔離管理了。
假設(shè) Codis 集群中保存的 80% 的鍵值對(duì)都是 Hash 類(lèi)型,每個(gè) Hash 集合的元素?cái)?shù)量在 10 萬(wàn)~20 萬(wàn)個(gè),每個(gè)集合元素的大小是 2KB。你覺(jué)得,遷移一個(gè)這樣的 Hash 集合數(shù)據(jù),會(huì)對(duì) Codis 的性能造成影響嗎?
Codis 在遷移數(shù)據(jù)時(shí),設(shè)計(jì)的方案可以保證遷移性能不受影響。
- 1、異步遷移:源節(jié)點(diǎn)把遷移的數(shù)據(jù)發(fā)送給目標(biāo)節(jié)點(diǎn)后就返回,之后接著處理客戶端請(qǐng)求,這個(gè)階段不會(huì)長(zhǎng)時(shí)間阻塞源節(jié)點(diǎn)。目標(biāo)節(jié)點(diǎn)加載遷移的數(shù)據(jù)成功后,向源節(jié)點(diǎn)發(fā)送 ACK 命令,告知其遷移成功。
- 2、源節(jié)點(diǎn)異步釋放 key:源節(jié)點(diǎn)收到目標(biāo)節(jié)點(diǎn) ACK 后,在源實(shí)例刪除這個(gè) key,釋放 key 內(nèi)存的操作,會(huì)放到后臺(tái)線程中執(zhí)行,不會(huì)阻塞源實(shí)例。(沒(méi)錯(cuò),Codis 比 Redis 更早地支持了 lazy-free,只不過(guò)只用在了數(shù)據(jù)遷移中)。
- 3、小對(duì)象序列化傳輸:小對(duì)象依舊采用序列化方式遷移,節(jié)省網(wǎng)絡(luò)流量。
- 4、bigkey 分批遷移:bigkey 拆分成一條條命令,打包分批遷移(利用了 Pipeline 的優(yōu)勢(shì)),提升遷移速度。
- 5、一次遷移多個(gè) key:一次發(fā)送多個(gè) key 進(jìn)行遷移,提升遷移效率。
- 6、遷移流量控制:遷移時(shí)會(huì)控制緩沖區(qū)大小,避免占滿網(wǎng)絡(luò)帶寬。
- 7、bigkey 遷移原子性保證(兼容遷移失敗情況):遷移前先發(fā)一個(gè) DEL 命令到目標(biāo)節(jié)點(diǎn)(重試可保證冪等性),然后把 bigkey 拆分成一條條命令,并設(shè)置一個(gè)臨時(shí)過(guò)期時(shí)間(防止遷移失敗在目標(biāo)節(jié)點(diǎn)遺留垃圾數(shù)據(jù)),遷移成功后在目標(biāo)節(jié)點(diǎn)設(shè)置真實(shí)的過(guò)期時(shí)間。 Codis 在數(shù)據(jù)遷移方面要比 Redis Cluster 做得更優(yōu)秀,而且 Codis 還帶了一個(gè)非常友好的運(yùn)維界面,方便 DBA 執(zhí)行增刪節(jié)點(diǎn)、主從切換、數(shù)據(jù)遷移等操作。
三.通信開(kāi)銷(xiāo):限制Redis Cluster規(guī)模的關(guān)鍵因素
1:為什么要限定集群規(guī)模呢?
Redis Cluster 能保存的數(shù)據(jù)量以及支撐的吞吐量,跟集群的實(shí)例規(guī)模密切相關(guān)。Redis 官方給出了 Redis Cluster 的規(guī)模上限,就是一個(gè)集群運(yùn)行 1000 個(gè)實(shí)例
這里的一個(gè)關(guān)鍵因素就是,實(shí)例間的通信開(kāi)銷(xiāo)會(huì)隨著實(shí)例規(guī)模增加而增大,在集群超過(guò)一定規(guī)模時(shí)(比如 800 節(jié)點(diǎn)),集群吞吐量反而會(huì)下降。所以,集群的實(shí)際規(guī)模會(huì)受到限制。
2:實(shí)例通信方法和對(duì)集群規(guī)模的影響
Redis Cluster 在運(yùn)行時(shí),每個(gè)實(shí)例上都會(huì)保存 Slot 和實(shí)例的對(duì)應(yīng)關(guān)系(也就是 Slot 映射表),以及自身的狀態(tài)信息。
為了讓集群中的每個(gè)實(shí)例都知道其它所有實(shí)例的狀態(tài)信息,實(shí)例之間會(huì)按照一定的規(guī)則進(jìn)行通信。這個(gè)規(guī)則就是 Gossip 協(xié)議
2.1Gossip 協(xié)議
Gossip 協(xié)議的工作原理可以概括成兩點(diǎn)。:檢測(cè)實(shí)例時(shí)候在線\給發(fā)送PING命令實(shí)例返回PONG消息
- 一是,每個(gè)實(shí)例之間會(huì)按照一定的頻率,從集群中隨機(jī)挑選一些實(shí)例,把 PING 消息發(fā)送給挑選出來(lái)的實(shí)例,用來(lái)檢測(cè)這些實(shí)例是否在線,并交換彼此的狀態(tài)信息。PING 消息中封裝了發(fā)送消息的實(shí)例自身的狀態(tài)信息、部分其它實(shí)例的狀態(tài)信息,以及 Slot 映射表。
- 二是,一個(gè)實(shí)例在接收到 PING 消息后,會(huì)給發(fā)送 PING 消息的實(shí)例,發(fā)送一個(gè) PONG 消息。PONG 消息包含的內(nèi)容和 PING 消息一樣。

Gossip 協(xié)議可以保證在一段時(shí)間后,集群中的每一個(gè)實(shí)例都能獲得其它所有實(shí)例的狀態(tài)信息。
這樣一來(lái),即使有新節(jié)點(diǎn)加入、節(jié)點(diǎn)故障、Slot 變更等事件發(fā)生,實(shí)例間也可以通過(guò) PING、PONG 消息的傳遞,完成集群狀態(tài)在每個(gè)實(shí)例上的同步
3.通信的影響
實(shí)例間使用 Gossip 協(xié)議進(jìn)行通信時(shí),通信開(kāi)銷(xiāo)受到通信消息大小和通信頻率這兩方面的影響。消息越大、頻率越高,相應(yīng)的通信開(kāi)銷(xiāo)也就越大。如果想要實(shí)現(xiàn)高效的通信,可以從這兩方面入手去調(diào)優(yōu)
3.1Gossip 消息大小
Redis 實(shí)例發(fā)送的 PING 消息的消息體是由 clusterMsgDataGossip 結(jié)構(gòu)體組成的,這個(gè)結(jié)構(gòu)體的定義如下所示:
typedef struct {
char nodename[CLUSTER_NAMELEN]; //40字節(jié)
uint32_t ping_sent; //4字節(jié)
uint32_t pong_received; //4字節(jié)
char ip[NET_IP_STR_LEN]; //46字節(jié)
uint16_t port; //2字節(jié)
uint16_t cport; //2字節(jié)
uint16_t flags; //2字節(jié)
uint32_t notused1; //4字節(jié)
} clusterMsgDataGossip;其中,CLUSTER_NAMELEN 和 NET_IP_STR_LEN 的值分別是 40 和 46,分別表示,nodename 和 ip 這兩個(gè)字節(jié)數(shù)組的長(zhǎng)度是 40 字節(jié)和 46 字節(jié),我們?cè)侔呀Y(jié)構(gòu)體中其它信息的大小加起來(lái),就可以得到一個(gè) Gossip 消息的大小了,即 104 字節(jié)。
每個(gè)實(shí)例在發(fā)送一個(gè) Gossip 消息時(shí),除了會(huì)傳遞自身的狀態(tài)信息,默認(rèn)還會(huì)傳遞集群十分之一實(shí)例的狀態(tài)信息。
例子:
所以,對(duì)于一個(gè)包含了 1000 個(gè)實(shí)例的集群來(lái)說(shuō),每個(gè)實(shí)例發(fā)送一個(gè) PING 消息時(shí),會(huì)包含 100 個(gè)實(shí)例的狀態(tài)信息,總的數(shù)據(jù)量是 10400 字節(jié),再加上發(fā)送實(shí)例自身的信息,一個(gè) Gossip 消息大約是 10KB。為了讓 Slot 映射表能夠在不同實(shí)例間傳播,PING 消息中還帶有一個(gè)長(zhǎng)度為 16,384 bit 的 Bitmap,這個(gè) Bitmap 的每一位對(duì)應(yīng)了一個(gè) Slot,如果某一位為 1,就表示這個(gè) Slot 屬于當(dāng)前實(shí)例。這個(gè) Bitmap 大小換算成字節(jié)后,是 2KB實(shí)例狀態(tài)信息和 Slot 分配信息相加,就可以得到一個(gè) PING 消息的大小了,大約是 12KB。
PONG 消息和 PING 消息的內(nèi)容一樣,它的大小大約是 12KB。每個(gè)實(shí)例發(fā)送了 PING 消息后,還會(huì)收到返回的 PONG 消息,兩個(gè)消息加起來(lái)有 24KB。
從絕對(duì)值上來(lái)看,24KB 并不算很大,但是,如果實(shí)例正常處理的單個(gè)請(qǐng)求只有幾 KB 的話,那么,實(shí)例為了維護(hù)集群狀態(tài)一致傳輸?shù)?PING/PONG 消息,就要比單個(gè)業(yè)務(wù)請(qǐng)求大了。而且,每個(gè)實(shí)例都會(huì)給其它實(shí)例發(fā)送 PING/PONG 消息。隨著集群規(guī)模增加,這些心跳消息的數(shù)量也會(huì)越多,會(huì)占據(jù)一部分集群的網(wǎng)絡(luò)通信帶寬,進(jìn)而會(huì)降低集群服務(wù)正??蛻舳苏?qǐng)求的吞吐量。
3.2實(shí)例間通信頻率
Redis Cluster 的實(shí)例啟動(dòng)后,默認(rèn)會(huì)每秒從本地的實(shí)例列表中隨機(jī)選出 5 個(gè)實(shí)例,再?gòu)倪@ 5 個(gè)實(shí)例中找出一個(gè)最久沒(méi)有通信的實(shí)例,把 PING 消息發(fā)送給該實(shí)例。這是實(shí)例周期性發(fā)送 PING 消息的基本做法
這里有一個(gè)問(wèn)題:實(shí)例選出來(lái)的這個(gè)最久沒(méi)有通信的實(shí)例,畢竟是從隨機(jī)選出的 5 個(gè)實(shí)例中挑選的,這并不能保證這個(gè)實(shí)例就一定是整個(gè)集群中最久沒(méi)有通信的實(shí)例。可能會(huì)出現(xiàn),有些實(shí)例一直沒(méi)有被發(fā)送 PING 消息,導(dǎo)致它們維護(hù)的集群狀態(tài)已經(jīng)過(guò)期。
為了避免這種情況,Redis Cluster 的實(shí)例會(huì)按照每 100ms 一次的頻率,掃描本地的實(shí)例列表,如果發(fā)現(xiàn)有實(shí)例最近一次接收 PONG 消息的時(shí)間,已經(jīng)大于配置項(xiàng) cluster-node-timeout 的一半了(cluster-node-timeout/2),就會(huì)立刻給該實(shí)例發(fā)送 PING 消息,更新這個(gè)實(shí)例上的集群狀態(tài)信息
當(dāng)集群規(guī)模擴(kuò)大之后,因?yàn)榫W(wǎng)絡(luò)擁塞或是不同服務(wù)器間的流量競(jìng)爭(zhēng),會(huì)導(dǎo)致實(shí)例間的網(wǎng)絡(luò)通信延遲增加。如果有部分實(shí)例無(wú)法收到其它實(shí)例發(fā)送的 PONG 消息,就會(huì)引起實(shí)例之間頻繁地發(fā)送 PING 消息,這又會(huì)對(duì)集群網(wǎng)絡(luò)通信帶來(lái)額外的開(kāi)銷(xiāo)
總結(jié)下單實(shí)例每秒會(huì)發(fā)送的 PING 消息數(shù)量,如下所示:
PING 消息發(fā)送數(shù)量 = 1 + 10 * 實(shí)例數(shù)(最近一次接收 PONG 消息的時(shí)間超出 cluster-node-timeout/2)
其中,1 是指單實(shí)例常規(guī)按照每 1 秒發(fā)送一個(gè) PING 消息,10 是指每 1 秒內(nèi)實(shí)例會(huì)執(zhí)行 10 次檢查,每次檢查后會(huì)給 PONG 消息超時(shí)的實(shí)例發(fā)送消息。
例子:
假設(shè)單個(gè)實(shí)例檢測(cè)發(fā)現(xiàn),每 100 毫秒有 10 個(gè)實(shí)例的 PONG 消息接收超時(shí),那么,這個(gè)實(shí)例每秒就會(huì)發(fā)送 101 個(gè) PING 消息,約占 1.2MB/s 帶寬。如果集群中有 30 個(gè)實(shí)例按照這種頻率發(fā)送消息,就會(huì)占用 36MB/s 帶寬,這就會(huì)擠占集群中用于服務(wù)正常請(qǐng)求的帶寬
4.如何降低實(shí)例間的通信開(kāi)銷(xiāo)?
4.1減小實(shí)例傳輸?shù)南⒋笮?/h4>
為了降低實(shí)例間的通信開(kāi)銷(xiāo),從原理上說(shuō),可以減小實(shí)例傳輸?shù)南⒋笮。≒ING/PONG 消息、Slot 分配信息),但是,因?yàn)榧簩?shí)例依賴(lài) PING、PONG 消息和 Slot 分配信息,來(lái)維持集群狀態(tài)的統(tǒng)一,一旦減小了傳遞的消息大小,就會(huì)導(dǎo)致實(shí)例間的通信信息減少,不利于集群維護(hù),所以,減小實(shí)例傳輸?shù)南⒋笮〔荒懿捎眠@種方式。
4.2降低實(shí)例間發(fā)送消息的頻率:
實(shí)例間發(fā)送消息的頻率有兩個(gè)。
- 每個(gè)實(shí)例每 1 秒發(fā)送一條 PING 消息。這個(gè)頻率不算高,如果再降低該頻率的話,集群中各實(shí)例的狀態(tài)可能就沒(méi)辦法及時(shí)傳播了。
- 每個(gè)實(shí)例每 100 毫秒會(huì)做一次檢測(cè),給 PONG 消息接收超過(guò) cluster-node-timeout/2 的節(jié)點(diǎn)發(fā)送 PING 消息。實(shí)例按照每 100 毫秒進(jìn)行檢測(cè)的頻率,是 Redis 實(shí)例默認(rèn)的周期性檢查任務(wù)的統(tǒng)一頻率,我們一般不需要修改它。
就只有 cluster-node-timeout 這個(gè)配置項(xiàng)可以修改
配置項(xiàng) cluster-node-timeout 定義了集群實(shí)例被判斷為故障的心跳超時(shí)時(shí)間,默認(rèn)是 15 秒。如果 cluster-node-timeout 值比較小,那么,在大規(guī)模集群中,就會(huì)比較頻繁地出現(xiàn) PONG 消息接收超時(shí)的情況,從而導(dǎo)致實(shí)例每秒要執(zhí)行 10 次“給 PONG 消息超時(shí)的實(shí)例發(fā)送 PING 消息”這個(gè)操作
所以,為了避免過(guò)多的心跳消息擠占集群帶寬,可以調(diào)大 cluster-node-timeout 值,比如說(shuō)調(diào)大到 20 秒或 25 秒。這樣一來(lái), PONG 消息接收超時(shí)的情況就會(huì)有所緩解,單實(shí)例也不用頻繁地每秒執(zhí)行 10 次心跳發(fā)送操作了。
也不要把 cluster-node-timeout 調(diào)得太大,否則,如果實(shí)例真的發(fā)生了故障,需要等待 cluster-node-timeout 時(shí)長(zhǎng)后,才能檢測(cè)出這個(gè)故障,這又會(huì)導(dǎo)致實(shí)際的故障恢復(fù)時(shí)間被延長(zhǎng),會(huì)影響到集群服務(wù)的正常使用
為了驗(yàn)證調(diào)整 cluster-node-timeout 值后,是否能減少心跳消息占用的集群網(wǎng)絡(luò)帶寬,建議:可以在調(diào)整 cluster-node-timeout 值的前后,使用 tcpdump 命令抓取實(shí)例發(fā)送心跳信息網(wǎng)絡(luò)包的情況。
執(zhí)行下面的命令后,可以抓取到 192.168.10.3 機(jī)器上的實(shí)例從 16379 端口發(fā)送的心跳網(wǎng)絡(luò)包,并把網(wǎng)絡(luò)包的內(nèi)容保存到 r1.cap 文件中:
tcpdump host 192.168.10.3 port 16379 -i 網(wǎng)卡名 -w /tmp/r1.cap
通過(guò)分析網(wǎng)絡(luò)包的數(shù)量和大小,就可以判斷調(diào)整 cluster-node-timeout 值前后,心跳消息占用的帶寬情況了。
5.通信開(kāi)銷(xiāo)總結(jié)
Redis Cluster 實(shí)例間以 Gossip 協(xié)議進(jìn)行通信的機(jī)制。Redis Cluster 運(yùn)行時(shí),各實(shí)例間需要通過(guò) PING、PONG 消息進(jìn)行信息交換,這些心跳消息包含了當(dāng)前實(shí)例和部分其它實(shí)例的狀態(tài)信息,以及 Slot 分配信息。這種通信機(jī)制有助于 Redis Cluster 中的所有實(shí)例都擁有完整的集群狀態(tài)信息。
但是,隨著集群規(guī)模的增加,實(shí)例間的通信量也會(huì)增加。如果盲目地對(duì) Redis Cluster 進(jìn)行擴(kuò)容,就可能會(huì)遇到集群性能變慢的情況。這是因?yàn)?,集群中大?guī)模的實(shí)例間心跳消息會(huì)擠占集群處理正常請(qǐng)求的帶寬。而且,有些實(shí)例可能因?yàn)榫W(wǎng)絡(luò)擁塞導(dǎo)致無(wú)法及時(shí)收到 PONG 消息,每個(gè)實(shí)例在運(yùn)行時(shí)會(huì)周期性地(每秒 10 次)檢測(cè)是否有這種情況發(fā)生,一旦發(fā)生,就會(huì)立即給這些 PONG 消息超時(shí)的實(shí)例發(fā)送心跳消息。
集群規(guī)模越大,網(wǎng)絡(luò)擁塞的概率就越高,相應(yīng)的,PONG 消息超時(shí)的發(fā)生概率就越高,這就會(huì)導(dǎo)致集群中有大量的心跳消息,影響集群服務(wù)正常請(qǐng)求??梢酝ㄟ^(guò)調(diào)整 cluster-node-timeout 配置項(xiàng)減少心跳消息的占用帶寬情況,但是,在實(shí)際應(yīng)用中,如果不是特別需要大容量集群,建議把 Redis Cluster 的規(guī)??刂圃?400~500 個(gè)實(shí)例。
設(shè)單個(gè)實(shí)例每秒能支撐 8 萬(wàn)請(qǐng)求操作(8 萬(wàn) QPS),每個(gè)主實(shí)例配置 1 個(gè)從實(shí)例,那么,400~ 500 個(gè)實(shí)例可支持 1600 萬(wàn)~2000 萬(wàn) QPS(200/250 個(gè)主實(shí)例 *8 萬(wàn) QPS=1600/2000 萬(wàn) QPS),這個(gè)吞吐量性能可以滿足不少業(yè)務(wù)應(yīng)用的需求
如果我們采用跟 Codis 保存 Slot 分配信息相類(lèi)似的方法,把集群實(shí)例狀態(tài)信息和 Slot 分配信息保存在第三方的存儲(chǔ)系統(tǒng)上(例如 Zookeeper),這種方法會(huì)對(duì)集群規(guī)模產(chǎn)生什么影響嗎?
答案:假設(shè)我們將 Zookeeper 作為第三方存儲(chǔ)系統(tǒng),保存集群實(shí)例狀態(tài)信息和 Slot 分配信息,那么,實(shí)例只需要和 Zookeeper 通信交互信息,實(shí)例之間就不需要發(fā)送大量的心跳消息來(lái)同步集群狀態(tài)了。這種做法可以減少實(shí)例之間用于心跳的網(wǎng)絡(luò)通信量,有助于實(shí)現(xiàn)大規(guī)模集群。
而且,網(wǎng)絡(luò)帶寬可以集中用在服務(wù)客戶端請(qǐng)求上。不過(guò),在這種情況下,實(shí)例獲取或更新集群狀態(tài)信息時(shí),都需要和 Zookeeper 交互,Zookeeper 的網(wǎng)絡(luò)通信帶寬需求會(huì)增加。所以,采用這種方法的時(shí)候,需要給 Zookeeper 保證一定的網(wǎng)絡(luò)帶寬,避免 Zookeeper 受限于帶寬而無(wú)法和實(shí)例快速通信。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis的Python客戶端redis-py安裝使用說(shuō)明文檔
這篇文章主要介紹了Redis的Python客戶端redis-py安裝使用說(shuō)明文檔,本文講解了安裝方法、入門(mén)使用實(shí)例、API參考和詳細(xì)說(shuō)明,需要的朋友可以參考下2015-06-06
Redis模擬延時(shí)隊(duì)列實(shí)現(xiàn)日程提醒的方法
文章介紹了如何使用Redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的延時(shí)任務(wù)隊(duì)列,通過(guò)Redis的有序集合特性來(lái)存儲(chǔ)和管理延時(shí)任務(wù),通過(guò)定期檢查集合中小于等于當(dāng)前時(shí)間的任務(wù)并執(zhí)行,可以實(shí)現(xiàn)延時(shí)任務(wù)的管理,感興趣的朋友跟隨小編一起看看吧2024-11-11
Redis實(shí)現(xiàn)分布式鎖和等待序列的方法示例
這篇文章主要介紹了Redis實(shí)現(xiàn)分布式鎖和等待序列的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-06-06

