本文將對集群的節點、槽指派、命令執行、重新分片、轉向、故障轉移、消息等各個方面進行深入拆解。
目的在于掌握什么是 Cluster ?Cluster 分片原理,客戶端定位數據原理、故障切換,選主,什么場景使用 Cluster,如何部署集群 …...
“65 哥:碼哥,自從用上了你說的哨兵集群實現故障自動轉移后,我終于可以開心的跟女朋友么么噠也不怕 Redis 宕機深夜宕機了。 可是最近遇到一個糟心的問題,Redis 需要保存 800 萬個鍵值對,占用 20 GB 的內存。 我就使用了一臺 32G 的內存主機部署,但是 Redis 響應有時候非常慢,使用 INFO 命令查看 latest_fork_usec 指標(最近一次 fork 耗時),發現特別高。 ”
“65 哥:隨著業務規模的拓展,數據量越來越大。主從架構升級單個實例硬件難以拓展,且保存大數據量會導致響應慢問題,有什么辦法可以解決么? ”
保存大量數據,除了使用大內存主機的方式,我們還可以使用切片集群。俗話說「眾人拾材火焰高」,一臺機器無法保存所有數據,那就多臺分擔。
使用 Redis Cluster 集群,主要解決了大數據量存儲導致的各種慢問題,同時也便于橫向拓展。
兩種方案對應著 Redis 數據增多的兩種拓展方案:垂直擴展(scale up)、水平擴展(scale out)。
垂直拓展:升級單個 Redis 的硬件配置,比如增加內存容量、磁盤容量、使用更強大的 CPU。
水平拓展:橫向增加 Redis 實例個數,每個節點負責一部分數據。
水平拓展與垂直拓展
“65 哥:那這兩種方案都有什么優缺點呢?
”
Redis 集群是一種分布式數據庫方案,集群通過分片(sharding)來進行數據管理(「分治思想」的一種實踐),并提供復制和故障轉移功能。
將數據劃分為 16384 的 slots,每個節點負責一部分槽位。槽位的信息存儲于每個節點中。
它是去中心化的,如圖所示,該集群有三個 Redis 節點組成,每個節點負責整個集群的一部分數據,每個節點負責的數據多少可能不一樣。
Redis 集群架構
Gossip
協議相互交互集群信息,最后每個節點都保存著其他節點的 slots 分配情況。一個 Redis 集群通常由多個節點(node)組成,在剛開始的時候,每個節點都是相互獨立的,它們都處于一個只包含自己的集群當中,要組建一個真正可工作的集群,我們必須將各個獨立的節點連接起來,構成一個包含多個節點的集群。
CLUSTER MEET
命令完成:CLUSTER MEET <ip> <port>
。CLUSTER MEET
命令,可以讓 node 節點與 ip 和 port 所指定的節點進行握手(handshake),當握手成功時,node 節點就會將 ip 和 port 所指定的節點添加到 node 節點當前所在的集群中。CLUSTER MEET
“65 哥:數據切片后,需要將數據分布在不同實例上,數據和實例之間如何對應上呢?
”
集群的整個數據庫被分為 16384 個槽(slot),數據庫中的每個鍵都屬于這 16384 個槽的其中一個,集群中的每個節點可以處理 0 個或最多 16384 個槽。
Key 與哈希槽映射過程可以分為兩大步驟:
根據鍵值對的 key,使用 CRC16 算法,計算出一個 16 bit 的值;
將 16 bit 的值對 16384 執行取模,得到 0 ~ 16383 的數表示 key 對應的哈希槽。
Cluster 還允許用戶強制某個 key 掛在特定槽位上,通過在 key 字符串里面嵌入 tag 標記,這就可以強制 key 所掛在的槽位等于 tag 所在的槽位。
“65 哥:哈希槽又是如何映射到 Redis 實例上呢?
”
在 部署集群的樣例中通過 cluster create
創建,Redis 會自動將 16384 個 哈希槽平均分布在集群實例上,比如 N 個節點,每個節點上的哈希槽數 = 16384 / N 個。
除此之外,可以通過 CLUSTER MEET
命令將 7000、7001、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與實例的映射
“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 哥:我知道哨兵通過監控、自動切換主庫、通知客戶端實現故障自動切換,
”Cluster
又如何實現故障自動轉移呢?
Gossip
協議來廣播自己的狀態以及自己對整個集群認知的改變。比如一個節點發現某個節點失聯了 (PFail),它會將這條信息向整個集群廣播,其它節點也就可以收到這點失聯信息。“65 哥:新的主節點如何選舉產生的?
”
集群的配置紀元 +1,是一個自曾計數器,初始值 0 ,每次執行故障轉移都會 +1。
檢測到主節點下線的從節點向集群廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到這條消息、并且具有投票權的主節點向這個從節點投票。
這個主節點尚未投票給其他從節點,那么主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示這個主節點支持從節點成為新的主節點。
參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,如果收集到的票 >= (N/2) + 1 支持,那么這個從節點就被選舉為新主節點。
如果在一個配置紀元里面沒有從節點能收集到足夠多的支持票,那么集群進入一個新的配置紀元,并再次進行選舉,直到選出新的主節點為止。
集群Leader選舉
“65 哥,我來考考你:“Redis Cluster 方案通過哈希槽的方式把鍵值對分配到不同的實例上,這個過程需要對鍵值對的 key 做 CRC 計算并對 哈希槽總數取模映射到實例上。如果用一個表直接把鍵值對和實例的對應關系記錄下來(例如鍵值對 1 在實例 2 上,鍵值對 2 在實例 1 上),這樣就不用計算 key 和哈希槽的對應關系了,只用查表就行了,Redis 為什么不這么做呢?”
”
“65 哥:客戶端又怎么確定訪問的數據到底分布在哪個實例上呢?
”
Redis 客戶端定位數據所在節點
“65 哥:哈希槽與實例之間的映射關系由于新增實例或者負載均衡重新分配導致改變了咋辦?
”
“65 哥:Redis 如何告知客戶端重定向訪問新實例呢?
”
GET 公眾號:碼哥字節
(error) MOVED 16330 172.17.18.2:6379
MOVED 指令
“65 哥:如果某個 slot 的數據比較多,部分遷移到新實例,還有一部分沒有遷移咋辦?
”
GET 公眾號:碼哥字節
(error) ASK 16330 172.17.18.2:6379
ASK 錯誤
172.17.18.1
實例發送請求,只不過節點會響應 ASK 命令讓客戶端給新實例發送一次請求。MOVED
指令則更新客戶端本地緩存,讓后續指令都發往新實例。“65 哥:有了 Redis Cluster,再也不怕大數據量了,我可以無限水平拓展么?
”
“65 哥:到底是什么限制了集群規模呢?
”
Gossip
協議傳播節點的數據,Gossip
協議工作原理大概如下:PING
消息發送給挑選出來的實例,用于檢測實例狀態以及交換彼此的信息。PING
消息中封裝了發送者自身的狀態信息、部分其他實例的狀態信息、Slot 與實例映射表信息。PING
消息后,響應 PONG
消息,消息包含的信息跟 PING
消息一樣。Gossip
協議可以在一段時間之后每個實例都能獲取其他所有實例的狀態信息。PING
,PONG
的消息傳播完成集群狀態在每個實例的傳播同步。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。Bitmap
。PING
消息大約 12KB。PONG
與PING
消息一樣,一發一回兩個消息加起來就是 24 KB。集群規模的增加,心跳消息越來越多就會占據集群的網絡通信帶寬,降低了集群吞吐量。“65 哥:碼哥,發送 PING 消息的頻率也會影響集群帶寬吧?
”
Redis Cluster 的實例啟動后,默認會每秒從本地的實例列表中隨機選出 5 個實例,再從這 5 個實例中找出一個最久沒有收到 PING 消息的實例,把 PING 消息發送給該實例。
“65 哥:隨機選擇 5 個,但是無法保證選中的是整個集群最久沒有收到 PING 通信的實例,有的實例可能一直沒有收到消息,導致他們維護的集群信息早就過期了,咋辦呢?
”
PONG
消息的時間 > cluster-node-timeout / 2
。那么就立刻給這個實例發送 PING
消息,更新這個節點的集群狀態信息。PING
消息,降低這個頻率可能會導致集群每個實例的狀態信息無法及時傳播。PONG
消息接收是否超過 cluster-node-timeout / 2
,這個是 Redis 實例默認的周期性檢測任務頻率,我們不會輕易修改。cluster-node-timeout
的值:集群中判斷實例是否故障的心跳時間,默認 15 S。cluster-node-timeout
調成 20 秒或者 30 秒,這樣 PONG
消息接收超時的情況就會緩解。cluster-node-timeout
時長才能檢測出這個故障,影響集群正常服務。Gossip
協議傳播集群實例信息,所以通信頻率是限制集群大小的主要原因,主要可以通過修改 cluster-node-timeout
調整頻率。