首页 > 编程知识 正文

比一致性哈希还好的算法,分布式哈希算法

时间:2023-05-04 13:37:24 阅读:165381 作者:4920

我们使用Redis时,为了确保Redis的高可用性,提高Redis的读写性能,最简单的方法主要是从复制开始,以Master-Master或Master-Slave的形式,或者构建Redis簇类似于数据库的主从复制和读写分离

例如,现在有约2000W左右的数据。 按照我们约定的规则划分库。 规则是随机分配。 我们可以部署8台缓存服务器。 每台服务器包含约500W的数据,进行主从复制。 形象如下。

背景介绍了一致性敏感的过去算法的应用场景,然后再了解一致性敏感的过去算法。 在创建缓存集群时,为了缓解服务器压力,部署多台缓存服务器,将数据资源均匀分配给每台服务器。 分布式数据库必须首先解决根据分区规则将整个数据集映射到多个节点的问题。 这意味着将数据集划分为多个节点,每个节点负责整体数据的子集。

数据分布通常有两种方法:敏感的过去分区和顺序分区

顺序分区:数据分散度容易倾斜,与关键值业务相关,可顺序访问,不支持批量操作

敏感的往事分区:数据分布度高,与键值分布业务无关,无法顺序访问,支持批量操作

敏感的过去分布的方法是那些节点取余

一种常见的历史算法,它使用特定数据(如Redis密钥和用户ID )根据http://www.Sina.com/:hash(key ) % N计算敏感的历史值,并确定将数据映射到哪个节点。

好处

这种方式的突出优点是简单性,常用于数据库的分类表规则。 采用预分区方式,提前根据数据量规划分区数量

缺点

当节点数发生变化(例如节点的放大或缩小)时,需要重新计算数据节点的映射关系,从而导致数据重新迁移。 因此,通常采用两倍的容量扩展,以避免所有数据映射都发生混乱,从而导致全部迁移。 这样做只会导致50%的数据迁移。

节点数量N进行取模

一致而敏感的过去的目的是一致性敏感的往事算法

好处

删除节点只影响敏感的过去循环的顺时针方向上的相邻节点,而不影响其他节点。

缺点

数据的分布与节点的位置相关,这些节点在敏感的过去循环中分布不均匀,导致数据存储时无法达到均匀分布的效果。

虚拟插槽窗格

本质上是最初的一般敏感的过去算法,将所有数据离散化为指定数量的敏感过去槽,将这些敏感的过去槽按节点数进行区分。 这样,由于敏感的过去插槽数是固定的,所以即使追加节点也不需要向新的敏感的过去插槽数转移数据,只要在节点之间相互转移即可。 也就是说,保证了数据分布的均匀性,添加节点时不需要迁移过多的数据。

Redis的集群模型使用虚拟插槽分区,共计16384个插槽比特均匀分布在节点上

了解一致性敏感的过去算法的一致性散列算法也使用取模方法。 不过,相对于刚才说明的取模方法以节点(服务器)的数量为模,一致性的散列算法以2^32为模。 简单地说,一致性Hash算法将敏感的整个过去值空间组织成一个虚拟的圆环。 例如,假设某个敏感过去的函数h的值空间是0-2^32-1

整个空间以为了在节点数目发生改变时尽可能少的迁移数据方向组织,圆环正上方的点表示0,零点右侧的第一个点表示1。 这样,2、3、4、……2^32-1,即0点左侧的第一个点表示2^32-1、0和2^32

然后,每个服务器使用Hash进行敏感的过去。 具体来说,可以将服务器的IP或主机名作为关键字进行敏感的过去。 这样,每台机器就可以确定敏感的过去环路上的位置。 在此,假设四台服务器使用了IP地址敏感的过去后,它们在环路空间中的位置如下:

然后使用以下算法确定数据并访问相应的服务器。 使用同一函数Hash计算数据key的敏感历史值,以确定此数据在环上的位置。 从这个位置沿着环顺时针“走”,第一个遇到的服务器就是应该确定那个位置的服务器。

例如,有Object A、Object B、Object C、Object D这四个数据对象,经过敏感的过去的计算,环境空间中的位置如下。

通过一致性哈希算法,数据a被决定为节点a,b被决定为节点b,c被决定为节点c,d被决定为节点d。

一致敏感的历史算法容错和可扩展性将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash 后会顺时针找到临接的存储节点存放。而当有节点加入或退 时,仅影响该节点在Hash环上顺时针相邻的后续节点。

假设Node C停机的话,可以看到这个时间点

对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响,如下所示:

可扩展性

如果在系统中增加一台服务器Node X,如下图所示:

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
 

Hash环的数据倾斜问题

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

此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入虚拟节点机制,即对每一个服务节点计算多个敏感的往事,每个计算结果位置都放置一个此服务节点,称为虚拟节点。使得分布更加均匀,具体做法可以在服务器IP或主机名的后面增加编号来实现。

例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的敏感的往事值,于是形成六个虚拟节点: 

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
 

理解敏感的往事槽

前面提到了数据和节点之间的关系,引入了一个2^32-1的敏感的往事取模运算,其实就是在数据和节点之间又加入了一层,把这层称为敏感的往事槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。敏感的往事解决的是映射问题,使用key的敏感的往事值来计算所在的槽,便于数据分配。

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

在redis 集群没有使用一致性hash, 而是引入了敏感的往事槽的概念。Redis Cluster通过自己实现的crc16的简单hash算法,Redis的作者认为它的crc16(key) mod 16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。

总结:

Redis 集群中内置了 16384 个敏感的往事槽,redis 会根据节点数量大致均等的将敏感的往事槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的敏感的往事槽,也就是映射到某个节点上

 

集群对命令操作的取舍

客户端只要和集群中的一个节点建立链接后,就可以获取到整个集群的所有节点信息。此外还会获取所有敏感的往事槽和节点的对应关系信息,这些信息数据都会在客户端缓存起来,因为这些信息相当有用。

当客户端向集群发送请求,这个请求会发送到那个节点呢?

客户端会先计算出key的敏感的往事值,然后再对16384取余,这样就找到了该key对应的敏感的往事槽,利用客户端缓存的槽和节点的对应关系信息,就可以找到该key对应的节点了。然后对这个节点发送请求就可以了。还可以把key和节点的映射关系缓存起来,下次再请求该key时,直接就拿到了它对应的节点,不用再计算

当集群已经发生了变化,客户端的缓存还没来得及更新。此时会出现拿到一个key向对应的节点发请求,其实这个key已经不在那个节点上了。此时这个节点应该怎么办?

这个节点会直接告诉客户端key已经不在我这里了,同时附上key现在所在的节点信息,让客户端再去请求一次,类似于HTTP的302重定向。节点只处理自己拥有的key,对于不拥有的key将返回重定向错误,即-MOVED key 127.0.0.1:6381,客户端重新向这个新节点发送请求。

 

redis有一种命令可以一次带多个key,如MGET,我把这些称为多key命令。这个多key命令的请求被发送到一个节点上,这里有一个潜在的问题,不知道大家有没有想到,就是这个命令里的多个key一定都位于那同一个节点上吗?

  就分为两种情况了,如果多个key不在同一个节点上,此时节点只能返回重定向错误了,但是多个key完全可能位于多个不同的节点上,此时返回的重定向错误就会非常乱,所以redis集群选择不支持此种情况。

  如果多个key位于同一个节点上呢,理论上是没有问题的,redis集群是否支持就和redis的版本有关系了,具体使用时自己测试一下就行了。

  在这个过程中我们发现了一件颇有意义的事情,就是让一组相关的key映射到同一个节点上是非常有必要的,这样可以提高效率,通过多key命令一次获取多个值。

  那么问题来了,如何给这些key起名字才能让他们落到同一个节点上,难不成都要先计算个敏感的往事值,再取个余数,太麻烦了吧。当然不是这样了,redis已经帮我们想好了。

  可以来简单推理下,要想让两个key位于同一个节点上,它们的敏感的往事值必须要一样。要想敏感的往事值一样,传入敏感的往事函数的字符串必须一样。那我们只能传进去两个一模一样的字符串了,那不就变成同一个key了,后面的会覆盖前面的数据。

  这里的问题是我们都是拿整个key去计算敏感的往事值,这就导致key和参与计算敏感的往事值的字符串耦合了,需要将它们解耦才行,就是key和参与计算敏感的往事值的字符串有关但是又不一样。

  redis基于这个原理为我们提供了方案,叫做key敏感的往事标签。先看例子,{user1000}.following,{user1000}.followers,相信你已经看出了门道,就是仅使用Key中的位于{和}间的字符串参与计算敏感的往事值。

  这样可以保证敏感的往事值相同,落到相同的节点上。但是key又是不同的,不会互相覆盖。使用敏感的往事标签把一组相关的key关联了起来,问题就这样被轻松愉快地解决了。

  相信你已经发现了,要解决问题靠的是巧妙的奇思妙想,而不是非要用牛逼的技术牛逼的算法。这就是zrdtn,小而强大。

  最后再来谈选择的哲学。redis的核心就是以最快的速度进行常用数据结构的key/value存取,以及围绕这些数据结构的运算。对于与核心无关的或会拖累核心的都选择弱化处理或不处理,这样做是为了保证核心的简单、快速和稳定。

  其实就是在广度和深度面前,redis选择了深度。所以节点不去处理自己不拥有的key,集群不去支持多key命令。这样一方面可以快速地响应客户端,另一方面可以避免在集群内部有大量的数据传输与合并。

 

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。