Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

前言

之前咱們一直在更新Redis相關內容,從基本架構到原理實現。今天,咱們繼續更新——Redis的集羣搭建,由淺及深的喫透Redis。

Redis 集羣提供了一種運行 Redis 的方式,其中數據在多個 Redis 節點間自動分區。Redis 集羣還在分區期間提供一定程度的可用性,即在實際情況下能夠在某些節點發生故障或無法通信時繼續運行。但是,如果發生較大故障(例如,大多數主站不可用時),集羣會停止運行。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

集羣 1:李代桃僵 —— Sentinel

上文我們提到,發生重大故障後,集羣會停止運行,所以我們必須有一個高可用方案來抵抗節點故障,當故障發生時可以自動進行從主切換,程序可以不用重啓,運維可以繼續睡大覺,彷彿什麼事也沒發生一樣。Redis 官方提供了這樣一種方案 —— Redis Sentinel(哨兵)。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

我們可以將 Redis Sentinel 集羣看成是一個 ZooKeeper 集羣,它是集羣高可用的心臟, 它一般是由 3~5 個節點組成,這樣掛了個別節點集羣還可以正常運轉。 它負責持續監控主從節點的健康,當主節點掛掉時,自動選擇一個最優的從節點切換爲 主節點。客戶端來連接集羣時,會首先連接 sentinel,通過 sentinel 來查詢主節點的地址, 然後再去連接主節點進行數據交互。當主節點發生故障時,客戶端會重新向 sentinel 要地 址,sentinel 會將最新的主節點地址告訴客戶端。如此應用程序將無需重啓即可自動完成節點切換。比如上圖的主節點掛掉後,集羣將可能自動調整爲下圖所示結構。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

從這張圖中我們能看到主節點掛掉了,原先的主從複製也斷開了,客戶端和損壞的主節 點也斷開了。從節點被提升爲新的主節點,其它從節點開始和新的主節點建立複製關係。客戶端通過新的主節點繼續進行交互。Sentinel 會持續監控已經掛掉了主節點,待它恢復後, 集羣會調整爲下面這張圖。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

此時原先掛掉的主節點現在變成了從節點,從新的主節點那裏建立複製關係。

消息丟失

Redis 主從採用異步複製,意味着當主節點掛掉時,從節點可能沒有收到全部的同步消 息,這部分未同步的消息就丟失了。如果主從延遲特別大,那麼丟失的數據就可能會特別多。Sentinel 無法保證消息完全不丟失,但是也儘可能保證消息少丟失。它有兩個選項可以限制主從延遲過大。

min-slaves-to-write 1
min-slaves-max-lag 10

 

第一個參數表示主節點必須至少有一個從節點在進行正常複製,否則就停止對外寫服 務,喪失可用性。

何爲正常複製,何爲異常複製?這個就是由第二個參數控制的,它的單位是秒,表示如 果 10s 沒有收到從節點的反饋,就意味着從節點同步不正常,要麼網絡斷開了,要麼一直沒有給反饋。

Sentinel 基本使用

接下來我們看看客戶端如何使用 sentinel,標準的流程應該是客戶端可以通過 sentinel 發現主從節點的地址,然後在通過這些地址建立相應的連接來進行數據存取操作。我們來看看 Python 客戶端是如何做的。

>>> from redis.sentinel import Sentinel
>>> sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
>>> sentinel.discover_master('mymaster')
('127.0.0.1', 6379)
>>> sentinel.discover_slaves('mymaster')
[('127.0.0.1', 6380)]

 

sentinel 的默認端口是 26379,不同於 Redis 的默認端口 6379,通過 sentinel 對象的 discover_xxx 方法可以發現主從地址,主地址只有一個,從地址可以有多個。

>>> master = sentinel.master_for('mymaster', socket_timeout=0.1)
>>> slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
>>> master.set('foo', 'bar')
>>> slave.get('foo')
'bar'

 

通過 xxx_for 方法可以從連接池中拿出一個連接來使用,因爲從地址有多個,redis 客戶端對從地址採用輪詢方案,也就是 RoundRobin 輪着來。

有個問題是,但 sentinel 進行主從切換時,客戶端如何知道地址變更了 ? 通過分析源碼,我發現 redis-py 在建立連接的時候進行了主庫地址變更判斷。

連接池建立新連接時,會去查詢主庫地址,然後跟內存中的主庫地址進行比對,如果變更了,就斷開所有連接,重新使用新地址建立新連接。如果是舊的主庫掛掉了,那麼所有正在使用的連接都會被關閉,然後在重連時就會用上新地址。

但是這樣還不夠,如果是 sentinel 主動進行主從切換,主庫並沒有掛掉,而之前的主庫連接已經建立了在使用了,沒有新連接需要建立,那這個連接是不是一直切換不了?

繼續深入研究源碼,我發現 redis-py 在另外一個點也做了控制。那就是在處理命令的時 候捕獲了一個特殊的異常 ReadOnlyError,在這個異常裏將所有的舊連接全部關閉了,後續指令就會進行重連。

主從切換後,之前的主庫被降級到從庫,所有的修改性的指令都會拋出 ReadonlyError。 如果沒有修改性指令,雖然連接不會得到切換,但是數據不會被破壞,所以即使不切換也沒關係。

集羣 2:分而治之 —— Codis

在大數據高併發場景下,單個 Redis 實例往往會顯得捉襟見肘。首先體現在內存上,單個 Redis 的內存不宜過大,內存太大會導致 rdb 文件過大,進一步導致主從同步時全量同步時間過長,在實例重啓恢復時也會消耗很長的數據加載時間,特別是在雲環境下,單個實例內存往往都是受限的。其次體現在 CPU 的利用率上,單個 Redis 實例只能利用單個核心,這單個核心要完成海量數據的存取和管理工作壓力會非常大。

正是在這樣的大數據高併發的需求之下,Redis 集羣方案應運而生。它可以將衆多小內 存的 Redis 實例綜合起來,將分佈在多臺機器上的衆多 CPU 核心的計算能力聚集到一起, 完成海量數據存儲和高併發讀寫操作。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

Codis 是 Redis 集羣方案之一,令我們感到驕傲的是,它是中國人開發並開源的,來自前豌豆莢中間件團隊。絕大多數國內的開源項目都不怎麼靠譜,但是 Codis 非常靠譜。

從 Redis 的廣泛流行到 RedisCluster 的廣泛使用之間相隔了好多年,Codis 就是在這樣的市場空缺的機遇下發展出來的。大型公司有明確的 Redis 在線擴容需求,但是市面上沒有特別好的中間件可以做到這一點。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

Codis 使用 Go 語言開發,它是一個代理中間件,它和 Redis 一樣也使用 Redis 協議 對外提供服務,當客戶端向 Codis 發送指令時,Codis 負責將指令轉發到後面的 Redis 實例來執行,並將返回結果再轉回給客戶端。

Codis 上掛接的所有 Redis 實例構成一個 Redis 集羣,當集羣空間不足時,可以通過動態增加 Redis 實例來實現擴容需求。

客戶端操縱 Codis 同操縱 Redis 幾乎沒有區別,還是可以使用相同的客戶端 SDK,不需要任何變化。

因爲 Codis 是無狀態的,它只是一個轉發代理中間件,這意味着我們可以啓動多個 Codis 實例,供客戶端使用,每個 Codis 節點都是對等的。因爲單個 Codis 代理能支撐的 QPS 比較有限,通過啓動多個 Codis 代理可以顯著增加整體的 QPS 需求,還能起到容災功 能,掛掉一個 Codis 代理沒關係,還有很多 Codis 代理可以繼續服務。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

Codis 分片原理

Codis 要負責將特定的 key 轉發到特定的 Redis 實例,那麼這種對應關係 Codis 是如 何管理的呢? Codis 將所有的 key 默認劃分爲 1024 個槽位(slot),它首先對客戶端傳過來的 key 進行 crc32 運算計算哈希值,再將 hash 後的整數值對 1024 這個整數進行取模得到一個餘 數,這個餘數就是對應 key 的槽位。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

每個槽位都會唯一映射到後面的多個 Redis 實例之一,Codis 會在內存維護槽位和 Redis 實例的映射關係。這樣有了上面 key 對應的槽位,那麼它應該轉發到哪個 Redis 實例就很明確了。

hash = crc32(command.key)
slot_index = hash % 1024
redis = slots[slot_index].redis
redis.do(command)

 

槽位數量默認是 1024,它是可以配置的,如果集羣節點比較多,建議將這個數值配置大一些,比如 2048、4096。

不同的 Codis 實例之間槽位關係如何同步?

如果 Codis 的槽位映射關係只存儲在內存裏,那麼不同的 Codis 實例之間的槽位關係就無法得到同步。所以 Codis 還需要一個分佈式配置存儲數據庫專門用來持久化槽位關係。 Codis 開始使用 ZooKeeper,後來連 etcd 也一塊支持了。

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

Codis 將槽位關係存儲在 zk 中,並且提供了一個 Dashboard 可以用來觀察和修改槽位 關係,當槽位關係變化時,Codis Proxy 會監聽到變化並重新同步槽位關係,從而實現多個 Codis Proxy 之間共享相同的槽位關係配置。

擴容

剛開始 Codis 後端只有一個 Redis 實例,1024 個槽位全部指向同一個 Redis。然後一 個 Redis 實例內存不夠了,所以又加了一個 Redis 實例。這時候需要對槽位關係進行調整, 將一半的槽位劃分到新的節點。這意味着需要對這一半的槽位對應的所有 key 進行遷移,遷 移到新的 Redis 實例。

那 Codis 如果找到槽位對應的所有 key 呢?

Codis 對 Redis 進行了改造,增加了 SLOTSSCAN 指令,可以遍歷指定 slot 下所有的 key。Codis 通過 SLOTSSCAN 掃描出待遷移槽位的所有的 key,然後挨個遷移每個 key 到新的 Redis 節點。

在遷移過程中,Codis 還是會接收到新的請求打在當前正在遷移的槽位上,因爲當前槽 位的數據同時存在於新舊兩個槽位中,Codis 如何判斷該將請求轉發到後面的哪個具體實例呢?

Codis 無法判定遷移過程中的 key 究竟在哪個實例中,所以它採用了另一種完全不同的思路。當 Codis 接收到位於正在遷移槽位中的 key 後,會立即強制對當前的單個 key 進行遷移,遷移完成後,再將請求轉發到新的 Redis 實例。

slot_index = crc32(command.key) % 1024
if slot_index in migrating_slots:
do_migrate_key(command.key) # 強制執行遷移
redis = slots[slot_index].new_redis
else:
redis = slots[slot_index].redis
redis.do(command)

 

我們知道 Redis 支持的所有 Scan 指令都是無法避免重複的,同樣 Codis 自定義的 SLOTSSCAN 也是一樣,但是這並不會影響遷移。因爲單個 key 被遷移一次後,在舊實例中它就徹底被刪除了,也就不可能會再次被掃描出來了。

自動均衡

Redis 新增實例,手工均衡 slots 太繁瑣,所以 Codis 提供了自動均衡功能。自動均衡會在系統比較空閒的時候觀察每個 Redis 實例對應的 Slots 數量,如果不平衡,就會自動進行遷移。

Codis 的代價

Codis 給 Redis 帶來了擴容的同時,也損失了其它一些特性。因爲 Codis 中所有的 key 分散在不同的 Redis 實例中,所以事務就不能再支持了,事務只能在單個 Redis 實例中完成。同樣 rename 操作也很危險,它的參數是兩個 key,如果這兩個 key 在不同的 Redis 實例中,rename 操作是無法正確完成的。Codis 的官方文檔中給出了一系列不支持的命令列 表。

同樣爲了支持擴容,單個 key 對應的 value 不宜過大,因爲集羣的遷移的最小單位是 key,對於一個 hash 結構,它會一次性使用 hgetall 拉取所有的內容,然後使用 hmset 放置 到另一個節點。如果 hash 內部的 kv 太多,可能會帶來遷移卡頓。官方建議單個集合結構 的總字節容量不要超過 1M。如果我們要放置社交關係數據,例如粉絲列表這種,就需要注意了,可以考慮分桶存儲,在業務上作折中。

Codis 因爲增加了 Proxy 作爲中轉層,所有在網絡開銷上要比單個 Redis 大,畢竟數據 包多走了一個網絡節點,整體在性能上要比單個 Redis 的性能有所下降。但是這部分性能損 耗不是太明顯,可以通過增加 Proxy 的數量來彌補性能上的不足。

Codis 的集羣配置中心使用 zk 來實現,意味着在部署上增加了 zk 運維的代價,不過大部分互聯網企業內部都有 zk 集羣,可以使用現有的 zk 集羣使用即可。

Codis 的優點

Codis 在設計上相比 Redis Cluster 官方集羣方案要簡單很多,因爲它將分佈式的問題交 給了第三方 zk/etcd 去負責,自己就省去了複雜的分佈式一致性代碼的編寫維護工作。而 Redis Cluster 的內部實現非常複雜,它爲了實現去中心化,混合使用了複雜的 Raft 和 Gossip 協議,還有大量的需要調優的配置參數,當集羣出現故障時,維護人員往往不知道從 何處着手。

MGET 指令的操作過程

Redis進階實戰來了!論Redis的集羣應用,你還不懂的都在這裏

 

mget 指令用於批量獲取多個 key 的值,這些 key 可能會分佈在多個 Redis 實例中。 Codis 的策略是將 key 按照所分配的實例打散分組,然後依次對每個實例調用 mget 方法, 最後將結果彙總爲一個,再返回給客戶端。

架構變遷

Codis 作爲非官方 Redis 集羣方案,近幾年來它的結構一直在不斷變化,一方面當官方 的 Redis 有變化的時候它要實時去跟進,另一方面它作爲 Redis Cluster 的競爭方案之一, 它還得持續提高自己的競爭力,給自己增加更多的官方集羣所沒有的便捷功能。 比如 Codis 有個特色的地方在於強大的 Dashboard 功能,能夠便捷地對 Redis 集羣進 行管理。這是 Redis 官方所欠缺的。另外 Codis 還開發了一個 Codis-fe(federation 聯邦) 工 具,可以同時對多個 Codis 集羣進行管理。在大型企業,Codis 集羣往往會有幾十個,有這 樣一個便捷的聯邦工具可以降低不少運維成本。

Codis 不是 Redis 官方項目,這意味着它的命運會無比曲折,它總是要被官方 Redis 牽 着牛鼻子走。當 Redis 官方提供了什麼功能它欠缺時,Codis 就會感到恐懼,害怕自己被市 場甩掉,所以必須實時保持跟進。

同時因爲 Codis 總是要比 Redis 官方慢一拍,Redis 官方提供的最新功能,Codis 往往 要等很久才能同步。比如現在 Redis 已經進入到 4.0 階段,提供了插件化 Redis-Module 支 持,目前 Codis 還沒有提供解決方案。

現在 Redis-Cluster 在業界已經逐漸流行起來,Codis 能否持續保持競爭力是個問題,我們看到 Codis 在不斷的差異化競爭,競爭的方法就體現在工具上,而不是內核,這個和官方的路線真是相反的,官方對工具無暇顧及,只提供基本的工具,其它完全交給第三方去開發。

以上就是小編整理的Redis集羣介紹,有寫的不準確的地方,還請各位大佬多多批評指出,咱們共同進步。

感覺小編的整理還不錯的,請多多點贊評論分享,關注小編,後續小編會在更來更多學習內容,咱們每天進步一點點!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章