hash slot(虛擬桶)
在分布式集群中,如何保證相同請求落到相同的機器上,並且後面的集群機器可以盡可能的均分請求,並且當擴容或down機的情況下能對原有集群影響最小。
round robin算法:是把數據mod後直接映射到真實節點上面,這造成節點個數和數據的緊密關聯、後期缺乏靈活擴展。
一致性哈希算法:多增加一層虛擬映射層,數據與虛擬節點映射、虛擬節點與真實節點再映射。
一般都會采用一致性哈希或者hash slot的方法。一致性哈希的ketama算法實現在擴容或down的情況下,需要重新計算節點,這對之前的分配可能會有一些影響。所以可以引入hash slot的方式,即某些hash slot區間對應一臺機器,對於擴容或down機情況下,就改變某個hash slot區間就可以了,改動比較小,對之前分配的影響也較小。
虛擬桶是取模和一致性哈希二者的折中辦法。
- 采用固定節點數量,來避免取模的不靈活性。
- 采用可配置映射節點,來避免一致性哈希的部分影響。
先來看一下hash slot的基本模型:
記錄和物理機之間引入了虛擬桶層,記錄通過hash函數映射到虛擬桶,記錄和虛擬桶是多對一的關系;第二層是虛擬桶和物理機之間的映射,同樣也是多對一的關系,即一個物理機對應多個虛擬桶,這個層關系是通過內存表實現的。對照抽象模型章節,key-partition是通過hash函數實現的,partition-machine是通過內存表來實現的。註:couchbase就是利用的此技術。
key對虛擬桶層
虛擬桶層采用預設固定數量,比如可以預設N=1024。意味之後這個分布式集群最大擴容到1024個節點,帶來的好處就是mod後的值是不變的(非常重要),這保證了第一層映射不受實際節點變化的影響。 關於最大數量,可根據實現需要預先定義好即可。
虛擬桶對實際節點
舉個例子,項目剛開始使用時配置節點映射:
Redis Server1對應桶的編號為0到500。
Redis Server2對應桶的編號為500到1024。
緩存數據量增長後需要增加新節點,在加之前需要重新分配節點對應虛擬桶的編號。 比如增加server3並配置對應桶的編號400到600,這時對於key映射虛擬桶層完全無影響。 實際上mod 400到600的真實數據還在另外兩臺節點上,請求過來後還會發生無法命中的影響。這就要求在增加新節點前,需要在後臺把另外二臺的400到600編號數據拷貝到新節點上面,完成後再添加配置到映射上面。 因為新來請求會命中到新節點,所以另外2臺的400到600編號數據就無用了,需要進行刪除。這種做法就能最大限度(100%)的保證動態擴容後,對緩存系統無影響,具體實現細節後續還需深入進行研究。
在redis集群的設計中也是采用的這個思路。
Redis 集群沒有並使用傳統的一致性哈希來分配數據,而是采用另外一種叫做哈希槽 (hash slot)
的方式來分配的。redis cluster 默認分配了 16384 個slot,當我們set一個key 時,會用CRC16
算法來取模得到所屬的slot
,然後將這個key 分到哈希槽區間的節點上,具體算法就是:CRC16(key) % 16384
。
所以,我們假設現在有3個節點已經組成了集群,分別是:A, B, C 三個節點,它們可以是一臺機器上的三個端口,也可以是三臺不同的服務器。那麽,采用哈希槽 (hash slot)
的方式來分配16384個slot 的話,它們三個節點分別承擔的slot 區間是:
- 節點A覆蓋0-5460;
- 節點B覆蓋5461-10922;
- 節點C覆蓋10923-16383.
這種將哈希槽分布到不同節點的做法使得用戶可以很容易地向集群中添加或者刪除節點。 比如說:
- 如果用戶將新節點 D 添加到集群中, 那麽集群只需要將節點 A 、B 、 C 中的某些槽移動到節點 D 就可以了。
比如我想新增一個
節點D
,redis cluster的這種做法是從各個節點的前面各拿取一部分slot到D
上。大致就會變成這樣:- 節點A覆蓋1365-5460
- 節點B覆蓋6827-10922
- 節點C覆蓋12288-16383
- 節點D覆蓋0-1364,5461-6826,10923-12287
- 與此類似, 如果用戶要從集群中移除節點 A , 那麽集群只需要將節點 A 中的所有哈希槽移動到節點 B 和節點 C , 然後再移除空白(不包含任何哈希槽)的節點 A 就可以了。
因為將一個哈希槽從一個節點移動到另一個節點不會造成節點阻塞, 所以無論是添加新節點還是移除已存在節點, 又或者改變某個節點包含的哈希槽數量, 都不會造成集群下線。
另外,還有一個問題:為什麽哈希槽的數量固定為16384?(https://github.com/antirez/redis/issues/2576)
由於使用CRC16算法,該算法可以產生2^16-1=65535個值,可是為什麽哈希槽的數量設置成了16384?
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with 16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
總結一下:
1、redis的一個節點的心跳信息中需要攜帶該節點的所有配置信息,而16K大小的槽數量所需要耗費的內存為2K,但如果使用65K個槽,這部分空間將達到8K,心跳信息就會很龐大。
2、Redis集群中主節點的數量基本不可能超過1000個。
3、Redis主節點的配置信息中,它所負責的哈希槽是通過一張bitmap的形式來保存的,在傳輸過程中,會對bitmap進行壓縮,但是如果bitmap的填充率slots / N很高的話,bitmap的壓縮率就很低,所以N表示節點數,如果節點數很少,而哈希槽數量很多的話,bitmap的壓縮率就很低。而16K個槽當主節點為1000的時候,是剛好比較合理的,既保證了每個節點有足夠的哈希槽,又可以很好的利用bitmap。
4、選取了16384是因為crc16會輸出16bit的結果,可以看作是一個分布在0-2^16-1之間的數,redis的作者測試發現這個數對2^14求模的會將key在0-2^14-1之間分布得很均勻,因此選了這個值。
最後,將redis中計算hash slot的源碼貼出來,看一下效果
1 #include <iostream> 2 #include <string.h> 3 #include "crc16.h" 4 5 unsigned int keyHashSlot(char *key, int keylen) { 6 int s, e; /* start-end indexes of { and } */ 7 8 std::cout << "key : " << key << std::endl; 9 10 for (s = 0; s < keylen; s++) 11 if (key[s] == ‘{‘) break; 12 13 /* No ‘{‘ ? Hash the whole key. This is the base case. */ 14 if (s == keylen) return crc16(key,keylen) & 0x3FFF; 15 16 /* ‘{‘ found? Check if we have the corresponding ‘}‘. */ 17 for (e = s+1; e < keylen; e++) 18 if (key[e] == ‘}‘) break; 19 20 /* No ‘}‘ or nothing betweeen {} ? Hash the whole key. */ 21 if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF; 22 23 /* If we are here there is both a { and a } on its right. Hash 24 * * what is in the middle between { and }. */ 25 return crc16(key+s+1,e-s-1) & 0x3FFF; 26 } 27 28 29 30 int main(int argc, char * argv[]) 31 { 32 if (argc != 2) { 33 std::cout << "usage: ./a.out key" << std::endl; 34 } 35 36 char * key = argv[1]; 37 38 std::cout << keyHashSlot(key, strlen(key)) << std::endl; 39 40 return 0; 41 }
運行結果:
本文參考自:
http://blog.csdn.net/baoxifu/article/details/51344786
http://www.cnblogs.com/mushroom/p/4542772.html
https://www.cnblogs.com/wxd0108/p/5798498.html
http://redisdoc.com/topic/cluster-tutorial.html
https://github.com/antirez/redis/issues/2576
http://blog.onlycatch.com/post/60c42de47e9a
https://www.zhihu.com/question/53927336
hash slot(虛擬桶)