高手過招, 為什么 Redis Cluster 是16384個(gè)槽位?
大家好,我是Tom哥~
我們都知道Redis的集群有三種方案:
1、主從復(fù)制模式 2、Sentinel(哨兵)模式 3、Redis Cluster模式
當(dāng)然使用隨著海量數(shù)據(jù)的存儲要求,單臺Redis配置有限,已經(jīng)滿足不了我們的需求。我們考慮采用分布式集群方案。
Redis Cluster 采用數(shù)據(jù)分片機(jī)制,定義了 16384個(gè) Slot槽位,集群中的每個(gè)Redis 實(shí)例負(fù)責(zé)維護(hù)一部分槽以及槽所映射的鍵值數(shù)據(jù)。
客戶端可以連接集群中任意一個(gè)Redis 實(shí)例,發(fā)送讀寫命令,如果當(dāng)前Redis 實(shí)例收到不是自己負(fù)責(zé)的Slot的請求時(shí),會將該slot所在的正確的Redis 實(shí)例地址返回給客戶端。
客戶端收到后,自動將原請求重新發(fā)到這個(gè)新地址,自動操作,外部透明。

★是不是有點(diǎn)似曾相識的感覺,HTTP 協(xié)議也有重定向功能。玩法跟這個(gè)差不多。HTTP 響應(yīng)頭有一個(gè)
”Location字段,當(dāng)狀態(tài)碼是301或者302時(shí),客戶端會自動讀取?Location中的新地址,自動重定向發(fā)送請求。
Redis key的路由計(jì)算公式:slot ?= CRC16(key) % 16384
添加、刪除或者修改某一個(gè)節(jié)點(diǎn),都不會造成集群不可用的狀態(tài)。使用哈希槽的好處就在于可以方便的添加或移除節(jié)點(diǎn)。
當(dāng)需要增加節(jié)點(diǎn)時(shí),只需要把其他節(jié)點(diǎn)的某些哈希槽挪到新節(jié)點(diǎn);當(dāng)需要移除節(jié)點(diǎn)時(shí),只需要把移除節(jié)點(diǎn)上的哈希槽挪到其他節(jié)點(diǎn)。

CRC16的算法原理:
根據(jù)CRC16的標(biāo)準(zhǔn)選擇初值CRCIn的值 將數(shù)據(jù)的第一個(gè)字節(jié)與CRCIn高8位異或 判斷最高位,若該位為 0 左移一位,若為 1 左移一位再與多項(xiàng)式Hex碼異或 重復(fù)3直至8位全部移位計(jì)算結(jié)束。 重復(fù)將所有輸入數(shù)據(jù)操作完成以上步驟,所得16位數(shù)即16位CRC校驗(yàn)碼。
CRC16 算法最大值
CRC16 算法,產(chǎn)生的hash值有 16 bit 位,可以產(chǎn)生 65536(2^16)個(gè)值 ,也就是說值分布在 0 ~ 65535 之間
這時(shí)候,疑問來了,槽位總數(shù)為什么是 16384 ?65536 不可以嗎?

這個(gè)問題,Redis 官方 Issues 也有朋友提出來過
地址:
https://github.com/redis/redis/issues/2576

antirez 大神對這個(gè)問題做了回復(fù),簡單歸納起來,有以下原因:
正常的心跳數(shù)據(jù)包攜帶節(jié)點(diǎn)的完整配置,它能以冪等方式來更新配置。如果采用 16384 個(gè)插槽,占空間 2KB (16384/8);如果采用 65536 個(gè)插槽,占空間 8KB (65536/8)。
Redis Cluster 不太可能擴(kuò)展到超過 1000 個(gè)主節(jié)點(diǎn),太多可能導(dǎo)致網(wǎng)絡(luò)擁堵。
16384 個(gè)插槽范圍比較合適,當(dāng)集群擴(kuò)展到1000個(gè)節(jié)點(diǎn)時(shí),也能確保每個(gè)master節(jié)點(diǎn)有足夠的插槽,
8KB 的心跳包看似不大,但是這個(gè)是心跳包每秒都要將本節(jié)點(diǎn)的信息同步給集群其他節(jié)點(diǎn)。比起 16384 個(gè)插槽,頭大小增加了4倍,ping消息的消息頭太大了,浪費(fèi)帶寬。
Redis主節(jié)點(diǎn)的哈希槽配置信息是通過 bitmap 來保存的

傳輸過程中,會對bitmap進(jìn)行壓縮,bitmap的填充率越低,壓縮率越高。
bitmap 填充率 = slots / N (N表示節(jié)點(diǎn)數(shù)),
所以,插槽數(shù)偏低的話, 填充率會降低,壓縮率會升高。
綜合下來,從心跳包的大小、網(wǎng)絡(luò)帶寬、心跳并發(fā)、壓縮率等維度考慮,16384 個(gè)插槽更有優(yōu)勢且能滿足業(yè)務(wù)需求。
★萬事萬物,都是相互制衡的,”大“ 不一定是最好的,合適最重要。”
接下來,我們看下master節(jié)點(diǎn)間心跳數(shù)據(jù)包格式:
消息格式分為:消息頭和消息體。消息頭包含發(fā)送節(jié)點(diǎn)自身狀態(tài)數(shù)據(jù),接收節(jié)點(diǎn)根據(jù)消息頭就可以獲取到發(fā)送節(jié)點(diǎn)的相關(guān)數(shù)據(jù),
代碼位置:
/usr/src/redis/redis-5.0.7/src/cluster.h

其中,消息頭有一個(gè)myslots的char類型數(shù)組,unsigned char myslots[CLUSTER_SLOTS/8];,數(shù)組長度為 16384/8 = 2048 。底層存儲其實(shí)是一個(gè)bitmap,每一個(gè)位代表一個(gè)槽,如果該位為1,表示這個(gè)槽是屬于這個(gè)節(jié)點(diǎn)。
消息體中,會攜帶一定數(shù)量的其他節(jié)點(diǎn)信息用于交換,約為集群總節(jié)點(diǎn)數(shù)量的1/10,節(jié)點(diǎn)數(shù)量越多,消息體內(nèi)容越大。10個(gè)節(jié)點(diǎn)的消息體大小約1kb。
劃重點(diǎn):
細(xì)心的同學(xué)可能會有疑問,char不是占2個(gè)字節(jié)嗎?數(shù)組長度為什么是 16384/8?不應(yīng)該是 16384/16 嗎?
因?yàn)椋琑edis 是 C 語言開發(fā)的,char 占用一個(gè) 字節(jié);而 Java 語言 char 占用 兩個(gè) 字節(jié)。
master節(jié)點(diǎn)間心跳通訊
Redis 集群采用 Gossip(流言)協(xié)議, Gossip 協(xié)議工作原理就是節(jié)點(diǎn)彼此不斷通信交換信息,一段時(shí)間后所有的節(jié)點(diǎn)都會知道集群完整的信息,類似流言傳播。
集群中每個(gè)節(jié)點(diǎn)通過一定規(guī)則挑選要通信的節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可能知道全部節(jié)點(diǎn),也可能僅知道部分節(jié)點(diǎn),只要這些節(jié)點(diǎn)彼此可以正常通信,最終它們會達(dá)到一致的狀態(tài)。當(dāng)節(jié)點(diǎn)出現(xiàn)故障、新節(jié)點(diǎn)加入、主從角色變化、槽信息變更等事件發(fā)生時(shí),通過不斷的 ping/pong 消息通信,經(jīng)過一段時(shí)間后所有的節(jié)點(diǎn)都會知道整個(gè)集群 全部節(jié)點(diǎn)的最新狀態(tài),從而達(dá)到集群狀態(tài)同步的目的。
具體規(guī)則如下:
1、每秒會隨機(jī)選取5個(gè)節(jié)點(diǎn),找出最久沒有通信的節(jié)點(diǎn)發(fā)送ping消息 2、每隔 100毫秒 都會掃描本地節(jié)點(diǎn)列表,如果發(fā)現(xiàn)節(jié)點(diǎn)最近一次接受pong消息的時(shí)間大于cluster-node-timeout/2 ,則立刻發(fā)送ping消息
因此,每秒單master節(jié)點(diǎn)發(fā)出ping消息數(shù)量:
總結(jié):
1、每秒 redis節(jié)點(diǎn)需要發(fā)送一定數(shù)量的ping消息作為心跳包,如果槽位為 65536,這個(gè)ping消息的消息頭太大了,浪費(fèi)帶寬。
2、業(yè)務(wù)上看,集群主節(jié)點(diǎn)數(shù)量基本不可能超過1000個(gè)。集群節(jié)點(diǎn)越多,心跳包的消息體攜帶的數(shù)據(jù)越多。如果節(jié)點(diǎn)超過1000個(gè),會導(dǎo)致網(wǎng)絡(luò)擁堵。因此redis作者,不建議redis cluster節(jié)點(diǎn)數(shù)量超過1000個(gè)。
3、槽位越小,節(jié)點(diǎn)少的情況下,壓縮率更高
