Redis 高可用篇:Cluster 集羣能支持的數據量有多大?

碼哥出品,必屬精品。關注公衆號「碼哥字節」並加碼哥微信(MageByte1024),窺探硬核文章背後的男人的另外一面。node

本文將對集羣的節點、槽指派、命令執行、從新分片、轉向、故障轉移、消息等各個方面進行深刻拆解。c++

Redis 集羣原理總覽

目的在於掌握什麼是 Cluster ?Cluster 分片原理,客戶端定位數據原理、故障切換,選主,什麼場景使用 Cluster,如何部署集羣 …...程序員

[toc]redis

爲何須要 Cluster

65 哥:碼哥,自從用上了你說的哨兵集羣實現故障自動轉移後,我終於能夠開心的跟女友麼麼噠也不怕 Redis 宕機深夜宕機了。算法

但是最近遇到一個糟心的問題,Redis 須要保存 800 萬個鍵值對,佔用 20 GB 的內存。數據庫

我就使用了一臺 32G 的內存主機部署,可是 Redis 響應有時候很是慢,使用 INFO 命令查看 latest_fork_usec 指標(最近一次 fork 耗時),發現特別高。緩存

主要是 Redis RDB 持久化機制致使的,Redis 會 Fork 子進程完成 RDB 持久化操做,fork 執行的耗時與 Redis 數據量成正相關。服務器

而 Fork 執行的時候會阻塞主線程,因爲數據量過大致使阻塞主線程過長,因此出現了 Redis 響應慢的表象。微信

65 哥:隨着業務規模的拓展,數據量愈來愈大。主從架構升級單個實例硬件難以拓展,且保存大數據量會致使響應慢問題,有什麼辦法能夠解決麼?

保存大量數據,除了使用大內存主機的方式,咱們還可使用切片集羣。俗話說「衆人拾材火焰高」,一臺機器沒法保存全部數據,那就多臺分擔。網絡

使用 Redis Cluster 集羣,主要解決了大數據量存儲致使的各類慢問題,同時也便於橫向拓展。

兩種方案對應着 Redis 數據增多的兩種拓展方案:垂直擴展(scale up)、水平擴展(scale out)。

  1. 垂直拓展:升級單個 Redis 的硬件配置,好比增長內存容量、磁盤容量、使用更強大的 CPU。
  2. 水平拓展:橫向增長 Redis 實例個數,每一個節點負責一部分數據。

好比須要一個內存 24 GB 磁盤 150 GB 的服務器資源,有如下兩種方案:

水平拓展與垂直拓展

在面向百萬、千萬級別的用戶規模時,橫向擴展的 Redis 切片集羣會是一個很是好的選擇。

65 哥:那這兩種方案都有什麼優缺點呢?
  • 垂直拓展部署簡單,可是當數據量大而且使用 RDB 實現持久化,會形成阻塞致使響應慢。另外受限於硬件和成本,拓展內存的成本太大,好比拓展到 1T 內存。
  • 水平拓展便於拓展,同時不須要擔憂單個實例的硬件和成本的限制。可是,切片集羣會涉及多個實例的分佈式管理問題,須要解決如何將數據合理分佈到不一樣實例,同時還要讓客戶端能正確訪問到實例上的數據

什麼是 Cluster 集羣

Redis 集羣是一種分佈式數據庫方案,集羣經過分片(sharding)來進行數據管理(「分治思想」的一種實踐),並提供複製和故障轉移功能。

將數據劃分爲 16384 的 slots,每一個節點負責一部分槽位。槽位的信息存儲於每一個節點中。

它是去中心化的,如圖所示,該集羣有三個 Redis 節點組成,每一個節點負責整個集羣的一部分數據,每一個節點負責的數據多少可能不同。

Redis 集羣架構

三個節點相互鏈接組成一個對等的集羣,它們之間經過 Gossip協議相互交互集羣信息,最後每一個節點都保存着其餘節點的 slots 分配狀況。

開篇寄語

技術不是萬能的,程序員也不是最厲害的,必定要搞清楚,不要以爲「老子天下第一」。一旦有了這個意識,可能會耽誤咱們的成長。

技術是爲了解決問題的,若是說一個技術不能解決問題,那這個技術就一文不值。

不要去炫技,沒有意義。

集羣安裝

點擊 -> 《 Redis 6.X Cluster 集羣搭建》查看

一個 Redis 集羣一般由多個節點(node)組成,在剛開始的時候,每一個節點都是相互獨立的,它們都處於一個只包含本身的集羣當中,要組建一個真正可工做的集羣,咱們必須將各個獨立的節點鏈接起來,構成一個包含多個節點的集羣。

鏈接各個節點的工做能夠經過 CLUSTER MEET 命令完成:CLUSTER MEET <ip> <port>

向一個節點 node 發送 CLUSTER MEET 命令,可讓 node 節點與 ip 和 port 所指定的節點進行握手(handshake),當握手成功時,node 節點就會將 ip 和 port 所指定的節點添加到 node 節點當前所在的集羣中。

CLUSTER MEET

就好像 node 節點說:「喂,ip = xx,port = xx 的老哥,要不要加入「碼哥字節」技術羣,加入集羣就找到了一條大神成長之路,關注「碼哥字節」公衆號回覆「加羣」,是兄弟就跟我一塊兒來!」

關於 Redis Cluster 集羣搭建詳細步驟,請點擊文末左下角「閱讀原文」或者點擊 -> 《Redis 6.X Cluster 集羣搭建》查看,官方關於 Redis Cluster 的詳情請看:https://redis.io/topics/clust...

Cluster 實現原理

65 哥:數據切片後,須要將數據分佈在不一樣實例上,數據和實例之間如何對應上呢?

Redis 3.0 開始,官方提供了 Redis Cluster 方案實現了切片集羣,該方案就實現了數據和實例的規則。Redis Cluster 方案採用哈希槽(Hash Slot,接下來我會直接稱之爲 Slot),來處理數據和實例之間的映射關係。

跟着「碼哥字節」一塊兒進入 Cluster 實現原理探索之旅…...

將數據分紅多份存在不一樣實例上

集羣的整個數據庫被分爲 16384 個槽(slot),數據庫中的每一個鍵都屬於這 16384 個槽的其中一個,集羣中的每一個節點能夠處理 0 個或最多 16384 個槽。

Key 與哈希槽映射過程能夠分爲兩大步驟:

  1. 根據鍵值對的 key,使用 CRC16 算法,計算出一個 16 bit 的值;
  2. 將 16 bit 的值對 16384 執行取模,獲得 0 ~ 16383 的數表示 key 對應的哈希槽。

Cluster 還容許用戶強制某個 key 掛在特定槽位上,經過在 key 字符串裏面嵌入 tag 標記,這就能夠強制 key 所掛在的槽位等於 tag 所在的槽位。

哈希槽與 Redis 實例映射

65 哥:哈希槽又是如何映射到 Redis 實例上呢?

部署集羣的樣例中經過 cluster create 建立,Redis 會自動將 16384 個 哈希槽平均分佈在集羣實例上,好比 N 個節點,每一個節點上的哈希槽數 = 16384 / N 個。

除此以外,能夠經過 CLUSTER MEET 命令將 7000、700一、7002 三個節點連在一個集羣,可是集羣目前依然處於下線狀態,由於三個實例都沒有處理任何哈希槽。

可使用 cluster addslots 命令,指定每一個實例上的哈希槽個數。

65 哥:爲啥要手動制定呢?

能者多勞嘛,加入集羣中的 Redis 實例配置不同,若是承擔同樣的壓力,對於垃圾機器來講就太難了,讓牛逼的機器多支持一點。

三個實例的集羣,經過下面的指令爲每一個實例分配哈希槽:實例 1負責 0 ~ 5460 哈希槽,實例 2 負責 5461~10922 哈希槽,實例 3 負責 10923 ~ 16383 哈希槽。

redis-cli -h 172.16.19.1 –p 6379 cluster addslots 0,5460
redis-cli -h 172.16.19.2 –p 6379 cluster addslots 5461,10922
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 10923,16383

鍵值對數據、哈希槽、Redis 實例之間的映射關係以下:

數據、Slot與實例的映射

Redis 鍵值對的 key 「碼哥字節」「牛逼」通過 CRC16 計算後再對哈希槽總個數 16394 取模,模數結果分別映射到實例 1 與實例 2 上。

切記,當 16384 個槽都分配徹底,Redis 集羣才能正常工做

複製與故障轉移

65 哥:Redis 集羣如何實現高可用呢?Master 與 Slave 仍是讀寫分離麼?

Master 用於處理槽,Slave 節點則經過《Redis 主從架構數據同步》方式同步主節點數據。

當 Master 下線,Slave 代替主節點繼續處理請求。主從節點之間並無讀寫分離, Slave 只用做 Master 宕機的高可用備份。

Redis Cluster 能夠爲每一個主節點設置若干個從節點,單主節點故障時,集羣會自動將其中某個從節點提高爲主節點。

若是某個主節點沒有從節點,那麼當它發生故障時,集羣將徹底處於不可用狀態

不過 Redis 也提供了一個參數cluster-require-full-coverage能夠容許部分節點故障,其它節點還能夠繼續提供對外訪問。

好比 7000 主節點宕機,做爲 slave 的 7003 成爲 Master 節點繼續提供服務。當下線的節點 7000 從新上線,它將成爲當前 70003 的從節點。

故障檢測

65 哥:在《 Redis 高可用篇:Sentinel 哨兵集羣原理》我知道哨兵經過監控、自動切換主庫、通知客戶端實現故障自動切換, Cluster 又如何實現故障自動轉移呢?

一個節點認爲某個節點失聯了並不表明全部的節點都認爲它失聯了。只有當大多數負責處理 slot 節點都認定了某個節點下線了,集羣才認爲該節點須要進行主從切換。

Redis 集羣節點採用 Gossip 協議來廣播本身的狀態以及本身對整個集羣認知的改變。好比一個節點發現某個節點失聯了 (PFail),它會將這條信息向整個集羣廣播,其它節點也就能夠收到這點失聯信息。

關於 Gossip 協議可閱讀悟空哥的一篇文章:《病毒入侵,全靠分佈式

若是一個節點收到了某個節點失聯的數量 (PFail Count) 已經達到了集羣的大多數,就能夠標記該節點爲肯定下線狀態 (Fail),而後向整個集羣廣播,強迫其它節點也接收該節點已經下線的事實,並當即對該失聯節點進行主從切換。

故障轉移

當一個 Slave 發現本身的主節點進入已下線狀態後,從節點將開始對下線的主節點進行故障轉移。

  1. 從下線的 Master 及節點的 Slave 節點列表選擇一個節點成爲新主節點。
  2. 新主節點會撤銷全部對已下線主節點的 slot 指派,並將這些 slots 指派給本身。
  3. 新的主節點向集羣廣播一條 PONG 消息,這條 PONG 消息可讓集羣中的其餘節點當即知道這個節點已經由從節點變成了主節點,而且這個主節點已經接管了本來由已下線節點負責處理的槽。
  4. 新的主節點開始接收處理槽有關的命令請求,故障轉移完成。

選主流程

65 哥:新的主節點如何選舉產生的?
  1. 集羣的配置紀元 +1,是一個自曾計數器,初始值 0 ,每次執行故障轉移都會 +1。
  2. 檢測到主節點下線的從節點向集羣廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求全部收到這條消息、而且具備投票權的主節點向這個從節點投票。
  3. 這個主節點還沒有投票給其餘從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支持從節點成爲新的主節點。
  4. 參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,若是收集到的票 >= (N/2) + 1 支持,那麼這個從節點就被選舉爲新主節點。
  5. 若是在一個配置紀元裏面沒有從節點能收集到足夠多的支持票,那麼集羣進入一個新的配置紀元,並再次進行選舉,直到選出新的主節點爲止。

跟哨兵相似,二者都是基於 Raft 算法來實現的,流程如圖所示:

集羣Leader選舉

用表保存鍵值對和實例的關聯關係可行麼

65 哥,我來考考你:「Redis Cluster 方案經過哈希槽的方式把鍵值對分配到不一樣的實例上,這個過程須要對鍵值對的 key 作 CRC 計算並對 哈希槽總數取模映射到實例上。若是用一個表直接把鍵值對和實例的對應關係記錄下來(例如鍵值對 1 在實例 2 上,鍵值對 2 在實例 1 上),這樣就不用計算 key 和哈希槽的對應關係了,只用查表就好了,Redis 爲何不這麼作呢?」

使用一個全局表記錄的話,假如鍵值對和實例之間的關係改變(從新分片、實例增減),須要修改表。若是是單線程操做,全部操做都要串行,性能太慢。

多線程的話,就涉及到加鎖,另外,若是鍵值對數據量很是大,保存鍵值對與實例關係的表數據所須要的存儲空間也會很大。

而哈希槽計算,雖然也要記錄哈希槽與實例時間的關係,可是哈希槽的數量少得多,只有 16384 個,開銷很小。

客戶端如何定位數據所在實例

65 哥:客戶端又怎麼肯定訪問的數據到底分佈在哪一個實例上呢?

Redis 實例會將本身的哈希槽信息經過 Gossip 協議發送給集羣中其餘的實例,實現了哈希槽分配信息的擴散。

這樣,集羣中的每一個實例都有全部哈希槽與實例之間的映射關係信息。

在切片數據的時候是將 key 經過 CRC16 計算出一個值再對 16384 取模獲得對應的 Slot,這個計算任務能夠在客戶端上執行發送請求的時候執行。

可是,定位到槽之後還須要進一步定位到該 Slot 所在 Redis 實例。

當客戶端鏈接任何一個實例,實例就將哈希槽與實例的映射關係響應給客戶端,客戶端就會將哈希槽與實例映射信息緩存在本地。

當客戶端請求時,會計算出鍵所對應的哈希槽,在經過本地緩存的哈希槽實例映射信息定位到數據所在實例上,再將請求發送給對應的實例。

Redis 客戶端定位數據所在節點

從新分配哈希槽

65 哥:哈希槽與實例之間的映射關係因爲新增實例或者負載均衡從新分配致使改變了咋辦?

集羣中的實例經過 Gossip 協議互相傳遞消息獲取最新的哈希槽分配信息,可是,客戶端沒法感知。

Redis Cluster 提供了重定向機制:客戶端將請求發送到實例上,這個實例沒有相應的數據,該 Redis 實例會告訴客戶端將請求發送到其餘的實例上

65 哥:Redis 如何告知客戶端重定向訪問新實例呢?

分爲兩種狀況:MOVED 錯誤、ASK 錯誤

MOVED 錯誤

MOVED 錯誤(負載均衡,數據已經遷移到其餘實例上):當客戶端將一個鍵值對操做請求發送給某個實例,而這個鍵所在的槽並不是由本身負責的時候,該實例會返回一個 MOVED 錯誤指引轉向正在負責該槽的節點。

GET 公衆號:碼哥字節
(error) MOVED 16330 172.17.18.2:6379

該響應表示客戶端請求的鍵值對所在的哈希槽 16330 遷移到了 172.17.18.2 這個實例上,端口是 6379。這樣客戶端就與 172.17.18.2:6379 創建鏈接,併發送 GET 請求。

同時,客戶端還會更新本地緩存,將該 slot 與 Redis 實例對應關係更新正確

MOVED 指令

ASK 錯誤

65 哥:若是某個 slot 的數據比較多,部分遷移到新實例,還有一部分沒有遷移咋辦?

若是請求的 key 在當前節點找到就直接執行命令,不然時候就須要 ASK 錯誤響應了,槽部分遷移未完成的狀況下,若是須要訪問的 key 所在 Slot 正在從從 實例 1 遷移到 實例 2,實例 1 會返回客戶端一條 ASK 報錯信息:客戶端請求的 key 所在的哈希槽正在遷移到實例 2 上,你先給實例 2 發送一個 ASKING 命令,接着發發送操做命令

GET 公衆號:碼哥字節
(error) ASK 16330 172.17.18.2:6379

好比客戶端請求定位到 key = 「公衆號:碼哥字節」的槽 16330 在實例 172.17.18.1 上,節點 1 若是找獲得就直接執行命令,不然響應 ASK 錯誤信息,並指引客戶端轉向正在遷移的目標節點 172.17.18.2。

ASK 錯誤

注意:ASK 錯誤指令並不會更新客戶端緩存的哈希槽分配信息

因此客戶端再次請求 Slot 16330 的數據,仍是會先給 172.17.18.1 實例發送請求,只不過節點會響應 ASK 命令讓客戶端給新實例發送一次請求。

MOVED指令則更新客戶端本地緩存,讓後續指令都發往新實例。

集羣能夠設置多大?

65 哥:有了 Redis Cluster,不再怕大數據量了,我能夠無限水平拓展麼?

答案是否認的,Redis 官方給的 Redis Cluster 的規模上線是 1000 個實例

65 哥:究竟是什麼限制了集羣規模呢?

關鍵在於實例間的通訊開銷,Cluster 集羣中的每一個實例都保存全部哈希槽與實例對應關係信息(Slot 映射到節點的表),以及自身的狀態信息。

在集羣之間每一個實例經過 Gossip協議傳播節點的數據,Gossip 協議工做原理大概以下:

  1. 從集羣中隨機選擇一些實例按照必定的頻率發送 PING 消息發送給挑選出來的實例,用於檢測實例狀態以及交換彼此的信息。 PING 消息中封裝了發送者自身的狀態信息、部分其餘實例的狀態信息、Slot 與實例映射表信息。
  2. 實例接收到 PING 消息後,響應 PONG 消息,消息包含的信息跟 PING 消息同樣。

集羣之間經過 Gossip協議能夠在一段時間以後每一個實例都能獲取其餘全部實例的狀態信息。

因此在有新節點加入,節點故障,Slot 映射變動均可以經過 PINGPONG 的消息傳播完成集羣狀態在每一個實例的傳播同步。

Gossip 消息

發送的消息結構是 clusterMsgDataGossip結構體組成:

typedef struct {
    char nodename[CLUSTER_NAMELEN];  //40字節
    uint32_t ping_sent; //4字節
    uint32_t pong_received; //4字節
    char ip[NET_IP_STR_LEN]; //46字節
    uint16_t port;  //2字節
    uint16_t cport;  //2字節
    uint16_t flags;  //2字節
    uint32_t notused1; //4字節
} clusterMsgDataGossip;

因此每一個實例發送一個 Gossip消息,就須要發送 104 字節。若是集羣是 1000 個實例,那麼每一個實例發送一個 PING 消息則會佔用 大約 10KB。

除此以外,實例間在傳播 Slot 映射表的時候,每一個消息還包含了 一個長度爲 16384 bit 的 Bitmap

每一位對應一個 Slot,若是值 = 1 則表示這個 Slot 屬於當前實例,這個 Bitmap 佔用 2KB,因此一個 PING 消息大約 12KB。

PONGPING 消息同樣,一發一回兩個消息加起來就是 24 KB。集羣規模的增長,心跳消息愈來愈多就會佔據集羣的網絡通訊帶寬,下降了集羣吞吐量。

實例的通訊頻率

65 哥:碼哥,發送 PING 消息的頻率也會影響集羣帶寬吧?

Redis Cluster 的實例啓動後,默認會每秒從本地的實例列表中隨機選出 5 個實例,再從這 5 個實例中找出一個最久沒有收到 PING 消息的實例,把 PING 消息發送給該實例。

65 哥:隨機選擇 5 個,可是沒法保證選中的是整個集羣最久沒有收到 PING 通訊的實例,有的實例可能一直沒有收到消息,致使他們維護的集羣信息早就過時了,咋辦呢?

這個問題問的好,Redis Cluster 的實例每 100 ms 就會掃描本地實例列表,當發現有實例最近一次收到 PONG 消息的時間 > cluster-node-timeout / 2。那麼就馬上給這個實例發送 PING 消息,更新這個節點的集羣狀態信息。

當集羣規模變大,就會進一步致使實例間網絡通訊延遲怎加。可能會引發更多的 PING 消息頻繁發送。

下降實例間的通訊開銷

  • 每一個實例每秒發送一條 PING消息,下降這個頻率可能會致使集羣每一個實例的狀態信息沒法及時傳播。
  • 每 100 ms 檢測實例 PONG消息接收是否超過 cluster-node-timeout / 2,這個是 Redis 實例默認的週期性檢測任務頻率,咱們不會輕易修改。

因此,只能修改 cluster-node-timeout的值:集羣中判斷實例是否故障的心跳時間,默認 15 S。

因此,爲了不過多的心跳消息佔用集羣寬帶,將 cluster-node-timeout調成 20 秒或者 30 秒,這樣 PONG 消息接收超時的狀況就會緩解。

可是,也不能設置的太大。都則就會致使實例發生故障了,卻要等待 cluster-node-timeout時長才能檢測出這個故障,影響集羣正常服務、

總結

「碼哥字節」不跟風不扯淡,助力程序員成長。

《Redis 系列》至今已發佈 7 篇,每一篇「碼哥字節」都耗費大量精力,精益求精。確保每一篇都給讀者帶來價值,讓你們獲得真正的提高。

  • 哨兵集羣實現故障自動轉移,可是當數據量過大致使生成 RDB 時間過長。而 Fork 執行的時候會阻塞主線程,因爲數據量過大致使阻塞主線程過長,因此出現了 Redis 響應慢的表象。
  • 使用 Redis Cluster 集羣,主要解決了大數據量存儲致使的各類慢問題,同時也便於橫向拓展。在面向百萬、千萬級別的用戶規模時,橫向擴展的 Redis 切片集羣會是一個很是好的選擇。
  • 集羣的整個數據庫被分爲 16384 個槽(slot),數據庫中的每一個鍵都屬於這 16384 個槽的其中一個,集羣中的每一個節點能夠處理 0 個或最多 16384 個槽。
  • Redis 集羣節點採用 Gossip 協議來廣播本身的狀態以及本身對整個集羣認知的改變。
  • 客戶端鏈接到集羣候任何一個實例後,實例會將哈希槽與實例映射信息發送給客戶端,客戶端將信息保存,用於將 key 定位到對應的節點。
  • 集羣並不能無限增長,因爲集羣經過 Gossip協議傳播集羣實例信息,因此通訊頻率是限制集羣大小的主要緣由,主要能夠經過修改 cluster-node-timeout調整頻率。

碼哥字節

原創不易,若是以爲文章不錯,但願讀者朋友點贊、收藏和分享。

相關文章
相關標籤/搜索