1.分布式存储算法

1.1 哈希取余算法

2亿条记录即2亿个k,v,单机无法满足数据存储,需要集群。假设集群由3台机器组成,用户每次读写操作都是根据公式(hash(key)%n 台机器数),通过计算出哈希取余值,来决定数据存储到哪个节点上,或者到那个节点取数据。

优点:简单并且有效,只需要提前预估数据量,规划数据节点数量,比如3、8、10台机器。使用哈希取余算法可以让固定的有i部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求信息),起到负载聚恒+分而治之的作用。

缺点:如果对预先规划好的节点,进行扩容或者缩容,数据与服务器映射关系需要重新计算, 哈希取余公式会发生变化:hash(key)%3变成hash(key)%?。此时地址经过取余运算的结果发生变化,根据此公式获取的服务器地址也会发生变化。例如某台redis服务器宕机,由于服务器数量发生变化,会导致hash取余数据发生变化。

1.2 一致性哈希算法

背景:一致性哈希算法在1997年由麻省理工学院提出的,设计目标是为了解决分布式缓存数据变动和映射问题。例如某台机器宕机了,分母数量改变,取余算法就会发生变化。此算法就是为了解决当服务器数量发生变动时,尽量减少影响客户端到服务器的映射关系。

分为三大步骤

(1)算法构建一致性哈希环

一致性哈希环:一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间[0, 2^32-1],这是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首位相连(0=2^32),这样让它逻辑上形成了一个环形空间。

它也是按照使用哈希取余算法,哈希取余算法是对节点(服务器)的数量进行取模。而一致性hash算法是对2^32取模。简单来说,一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环。假设某hash函数H的值空间为0~2^32-1(即哈希值是一个32为无符号整型),整个哈希环:整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,依次类推,2、3、4......直到2^32-1,也就是说0点左侧的第一个点代表2^32-1,0和2^32-1在零点钟方向重合,我们把这个由2^32个点组成的圆环成为Hash环

docker 伪分布式到完全分布式 docker 分布式存储_redis

(2)服务器IP节点映射

节点映射:将集群中各个服务器IP节点映射到换上的某一个位置。

将各个服务器取hash值,具体可以选择服务器的IP或者主机名作为关键字进行hash。这样每台机器就能确定其在哈希环上的位置。假如4个节点分别为NodeA、B、C、D,经过IP地址的哈希函数计算(hash(IP)),使用IP地址哈希后在哈希环中就有了自己的位置。

 

docker 伪分布式到完全分布式 docker 分布式存储_docker 伪分布式到完全分布式_02

(3)Key落到服务器的落键规则

当我们需要存储一个kv键值对时,首先计算key的hash值,即hash(key),将这个key使用相同的函数hash计算出哈希值并确定此数据在环上的位置。从此位置沿着环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。

如果我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间的位置如下:根据一致性Hash算法,数据A会被定为到Node A上,数据B会被定为到Node B上,数据C会被定为到Node C上,数据D会被定为到Node D上

docker 伪分布式到完全分布式 docker 分布式存储_docker_03

优点:解决了哈希取余算法的容错性和扩展性问题

容错性:假设Node C宕机,可以看到此时对象A、B、C、D不会受到影响,只有C对象被重定位到Node D。一般在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中钱一台服务器(即沿着逆时针方向行走,遇到的第一台服务器)之间数据,其他不会受到影响。简单说,C挂掉,收i到影响的只是B、C之间的数据,并且这些数据会转移到D进行存储。

docker 伪分布式到完全分布式 docker 分布式存储_redis_04

扩展性:如果数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那受到影响的也就是A到X之间的数据,重新把A到X的数据记录到X上接口,不会导致Hash取余全部数据重新洗牌

docker 伪分布式到完全分布式 docker 分布式存储_docker_05

缺点:一致性哈希算法的数据倾斜问题

Hash环的数据倾斜问题:一致性hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓在某一台服务器上)的问题,例如系统中只有两台服务器:

docker 伪分布式到完全分布式 docker 分布式存储_算法_06

总结:为了在节点数目发生改变时尽可能烧的迁移数据,将所有的存储节点排列在首位相接的hash环上,每个Key在计算hash后会顺时针找到临近的存储节点存放。而当有节点加入或者推出时仅仅影响该节点在hash环上顺时针相邻的后续节点。优点:加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。缺点:数据的分布和节点位置有关,因为这些节点不是均匀分布在哈希环上的,所以数据在进行存储时到不到均匀分布的效果

1.3 哈希槽分区算法

由于一致性哈希算法的数据倾斜问题,继而出现了哈希槽分区算法。哈希槽实质就是一个数组,数组[0, 2^32-1]形成 hash slot空间,解决了数据均匀分配的问题,即在数据和节点之间又加入了一层,把这层成为哈希槽,用户管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。

docker 伪分布式到完全分布式 docker 分布式存储_docker 伪分布式到完全分布式_07

槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。

一个集群只能由16384个槽,编号0-16383(0~2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给那个主节点。集群会记录节点和槽的对应关系。解决了节点和槽的关系后,接下来就需要对key求hash值,然后对16384取余,余数是几,key就落入对应的槽里。slot = CRC6(key)%16384。以槽为单位移动数据,因为槽的数据是固定的,处理起来比较容易,这样数据移动问题就解决了。

为什么redis集群的最大槽数是16384个?

redis集群并没有使用一致性hash,而是引入了哈希槽的概念。Redis集群有16384个哈希槽,每个key通过CRC16校验后,对16384取模来决定放到哪个槽,集群的每个节点负责一部分hash槽。但为什么哈希槽的数量是16384(2^14)个呢?这是由于CRC16算法产生的hash值由16bit,该算法可以产生2^16=65536个值。换句话说值是分布在0-65535之间。那作者在做mod运算时,为什么不mod65536,而选择了mod16384?

说明:

docker 伪分布式到完全分布式 docker 分布式存储_redis_08

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。因此16K处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够烧,可以轻松的将插槽配置作为原始位图传播。请注意,在小型集群中,位图将难以压缩,因为当N较小时,位图将设置的slot/N位占设置位的很大百分比。

docker 伪分布式到完全分布式 docker 分布式存储_算法_09

(1)如果槽位为65536,发送心跳信息的消息头达8K,发送的心跳包过于庞大。在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。当槽位为65536时,这块的大小是:65536/8/1024=8kb。因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。

(2)redis集群主节点数量基本不可能超过1000个。

集群节点越多,心跳包的消息体内携带的数据越多。如果节点超过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。那么,对于节点数在1000以内的redis cluster集群,16384个槽位就够用了。没必要拓展到65536个。

(3)槽位越小,节点少的情况下,压缩比高,容易传输

Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots/N很高的话(N代表节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。

哈希槽计算

Redis集群中内置了16384个哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在redis集群中防止一个key-value时,redis先对key使用CRC16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,也就是映射到莫格节点上。如下代码,key值A、B在Node2,key值C落在Node3节点上。

docker 伪分布式到完全分布式 docker 分布式存储_docker_10