一致性哈希算法

我们在使用Redis的时候,为了保证Redis的高可用,提高Redis的读写性能,最简单的方式我们会做主从复制,组成Master-Master或者Master-Slave的形式,或者搭建Redis集群,进行数据的读写分离,类似于数据库的主从复制和读写分离

比如现在有大概2000W左右的数据,按照我们约定的规则进行分库,规则就是随机分配,我们可以部署8台缓存服务器,每台服务器大概含有500W条数据,并且进行主从复制,示意图如下:
 

640

背景介绍

在了解一致性哈希算法之前,先了解一下一致性哈希算法的应用场景,在做缓存集群时,为了缓解服务器的压力,会部署多台缓存服务器,把数据资源均匀的分配到每个服务器上,分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。

数据分布通常有哈希分区和顺序分区两种方式

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

哈希分区:数据分散度高、键值分布业务无关、无法顺序访问、支持批量操作   

 

哈希分布的方法有那些

节点取余

普通哈希算法,使用特定的数据,如Redis的键或用户ID,再根据节点数量N进行取模:hash(key)% N 计算出哈希值,用来决定数据映射到哪一个节点上。

优点

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

缺点 

当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。所以扩容时通常采用翻倍扩容,避免 数据映射全部被打乱,导致全量迁移的情况,这样只会发生50%的数据迁移。

 

一致性哈希算法

一致性哈希的目的就是:为了在节点数目发生改变时尽可能少的迁移数据将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash 后会顺时针找到临接的存储节点存放。而当有节点加入或退 时,仅影响该节点在Hash环上顺时针相邻的后续节点。   

优点

加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。

缺点 

数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。

 

虚拟槽分区

本质上还是第一种的普通哈希算法,把全部数据离散到指定数量的哈希槽中,把这些哈希槽按照节点数量进行了分区。这样因为哈希槽的数量的固定的,添加节点也不用把数据迁移到新的哈希槽,只要在节点之间互相迁移就可以了,即保证了数据分布的均匀性,又保证了在添加节点的时候不必迁移过多的数据。

Redis的集群模式使用的就是虚拟槽分区,一共有16384个槽位平均分布到节点上

      一致性哈希算法_第1张图片     

理解一致性哈希算法

一致性Hash算法也是使用取模的方法,只是,刚才描述的取模法是对节点(服务器)的数量进行取模,而一致性Hash算法是对2^32取模,简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下: 

640

整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32-1在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。

然后将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用IP地址哈希后在环空间的位置如下:  
 

640

接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器!

如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:640

根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

 

一致性哈希算法的容错性和扩展性

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

640

可扩展性

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

640

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

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

Hash环的数据倾斜问题

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

640

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

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

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“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关联了起来,问题就这样被轻松愉快地解决了。

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

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

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

 

你可能感兴趣的:(redis)