开篇结论
直接上结论:大量的key过于“接近”,是导致Redis负载不均衡的本质。
这里的“接近” means: 各个key的 CRC16(key)值过于相近。
那有同学就可能会问了:
- 为什么跟key的CRC16值有关呢?
- 如果我在业务层和数据库层写一个代理,通过代理指定分发到redis的不同分片,是不是不管我的key的CRC16值多么相近,都可以做到负载均衡了?
下面我就来介绍一下这里面的原理,看完原理你就懂了。
Redis分片集群Key的分布原理
1. Redis集群中的Slot是什么
在Redis的架构中,Redis集群会划分出16384个插槽(Hash Slot,如果对为什么是16384感兴趣,可见“延伸知识”),数据库中的每个键都属于这16384个插槽的其中一个,即每个Key会被分配到这16384个Slot中的一个,槽位号0~16383。集群中每个分片(shard)会负责处理16384/N个插槽(N等于该redis集群的分片数量)。例如:
集群中有3个分片a\b\c,那么分片a负责处理0~5460号slot,分片b负责处理5461~10922号slot,分片c负责处理10923~16383号slot。如果key被计算出属于500号slot,那么它将被存放在分片a上。计算方式见“2. Redis如何决定Key分配到哪个Slot”。
Redis引入Slot主要是用于管理集群模式下数据与分片相联,更直观点,可以将Slot看做是redis内部的索引来管理数据。
2. Redis如何决定Key分配到哪个Slot
CRC16(key)%16384
如“1. Redis集群中的Slot是什么”中举的例子,key被计算出属于500号slot,那么该key就会被路由到负责NO.500 slot的分片a上存放,业务读写该key的流量都会达到分片a上。
点题总结
根据redis分片集群的key分布原理,不难发现,key最终归属到哪个slot和哪个分片,都是由CRC(key)%16384计算出的值决定,所以大家在使用redis的时候,业务设计key需要注意CRC(key)的值不要太接近。
之前我使用YCSB默认的脚本做测试,其中zadd大量相同key,导致某个分片被迅速写满之后出现写timeout的问题。详情见:
延伸知识:为什么redis是16384个槽位
按道理来说,CRC16算法产生的hash值有16bits,也就能够产生2^16=65536个不同的值。那为啥Redis不把这65536个值都用上、设定65536个slot呢?
The reason is:
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 with16k 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.
其实本质就是slot数太大会带来节点之间通信的带宽消耗大,再加上本身Redis作者建议集群的分片个数不超过1000个,所以最终选定了16384个slot。具体分析如下:
1. 插槽数越多,那么节点之间心跳消息越大,占用的空间和消耗的带宽不划算
- (1)为什么插槽数越多,消息头越大:
Cluster_Slots/8 )个字符
- (2)又由于每秒钟,redis集群中都有一定数量的ping消息作为心跳包在通信,那么8KB/节点的消息头消耗的带宽比2KB/节点消息头大得多;
2. 在节点规模小的情况下,槽位越多,bitmap在传输过程中的压缩比越小:
这点其实很好理解,举个例子,如果节点数为4个,那么:
- 当槽位数为65536个时,每个节点将承担16384个slots;
- 当槽位数为16384个时,每个节点将承担4096个slots。
对应到每个节点中哈希槽2^16位的bitmap,前者比后者有更多的1,所以更难压缩。
延伸只是总结:
总的来说,插槽数太多会带来【更大的消息头】+【更低的bitmap压缩率】,从而导致节点通信的带宽消耗大,所以需要控制在尽可能小的插槽数;而redis本身在设计时,被认为不会超过1000个节点数,所以16384个Slots是足够的了。