分布式系统中一致性哈希

问题场景

近年来B2C、O2O等商业概念的提出和移动端的发展,使得分布式系统流行了起来。分布式系统相对于单系统,解决了流量大、系统高可用和高容错等问题。功能强大也意味着实现起来须要更多技术的支持。例如系统访问层的负载均衡,缓存层的多实例主从复制备份,数据层的分库分表等。java

咱们以负载均衡为例,常见的负载均衡方法有不少,可是它们的优缺点也都很明显:web

  • 随机访问策略。 系统随机访问,缺点:可能形成服务器负载压力不均衡,俗话讲就是撑的撑死,饿的饿死。
  • 轮询策略。 请求均匀分配,若是服务器有性能差别,则没法实现性能好的服务器可以多承担一部分。
  • 权重轮询策略。 权值须要静态配置,没法自动调节,不适合对长链接和命中率有要求的场景。
  • Hash取模策略。 不稳定,若是列表中某台服务器宕机,则会致使路由算法产生变化,由此致使命中率的急剧降低。
  • 一致性哈希策略。

以上几个策略,排除本篇介绍的一致性哈希,可能使用最多的就是 Hash取模策略了。Hash取模策略的缺点也是很明显的,这种缺点也许在负载均衡的时候不是很明显,可是在涉及数据访问的主从备份和分库分表中就体现明显了。算法

使用Hash取模的问题

负载均衡sql

负载均衡时,假设现有3台服务器(编号分别为0、一、2),使用哈希取模的计算方式则是:对访问者的IP,经过固定算式hash(IP) % N(N为服务器的个数),使得每一个IP均可以定位到特定的服务器。缓存

例如现有IP地址 10.58.34.31,对IP哈希取模策时,计算结果为2,即访问编号为2的服务器:bash

String ip = "10.58.34.31";
int v1 = hash(ip) % 3;
System.out.println("访问服务器:" + v1);// 访问服务器:2
复制代码

若是此时服务器2宕机了,则会致使全部计算结果为2的 IP 对应的用户都访问异常(包括上例中的IP)。或者你新增了一台服务器3,这时不修改N值的话那么服务器3永远不会被访问到。服务器

固然若是你能动态获取到当前可用服务器的个数,亦即N值是根据当前可用服务器个数动态来变化的,则可解决此问题。可是对于特定地区或特定IP访问特定服务器类的需求会形成访问误差。负载均衡

分库分表运维

负载均衡中有这种问题,那么分库分表中一样也有这样的问题。例如随着业务的飞速增加,咱们的注册用户也愈来愈多,单个用户表数量已经达到千万级甚至更大。因为Mysql的单表建议百万级数据存储,因此这时为了保证系统查询和运行效率,确定会考虑到分库分表。分布式

对于分库分表,数据的分配是个重要的问题,你须要保证数据分配在这个服务器,那么在查询时也须要到该服务器上来查询,不然会形成数据查询丢失的问题。

一般是根据用户的 ID 哈希取模获得的值而后路由到对应的存储位置,计算公式为:hash(userId) % N,其中N为分库或分表的个数。

例如分库数为2时,计算结果为1,则ID为1010的用户存储在编号为1对应的库中:

String userId = "1010";
int v1 = hash(userId) % 2;
System.out.println("存储:" + v1);// 存储:1
复制代码

以后业务数量持续增加,又新增一台用户服务库,当咱们根据ID=1010去查询数据时,路由计算方式为:

int v2 = hash(userId) % 3;
System.out.println("存储:" + v2);// 存储:0
复制代码

咱们获得的路由值是0,最后的结果就不用说了,存在编号1上的数据咱们去编号为0的库上去查询确定是得不到查询结果的。

为了数据可用,你须要作数据迁移,按照新的路由规则对全部用户从新分配存储地址。每次的库或表的数量改变你都须要作一次所有用户信息数据的迁移。不用想这其中的工做量是有多费时费力了。

是否有某种方法,有效解决这种分布式存储结构下动态增长或删除节点所带来的问题,能保证这种不受实例数量变化影响而准确路由到正确的实例上的算法或实现机制呢?解决这些问题,一致性哈希算法诞生了。

基本原理

一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分相似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT能够在P2P环境中真正获得应用。

上面说的哈希取模方法,它是针对一个点的,业务布局严重依赖于这个计算的点值结果。你结算的结果是2,那么就对应到编号为2的服务器上。这样的映射就形成了业务容错性和可扩展性极低。

咱们思考下,是否能够将这个计算结果的点值赋予范围的意义?咱们知道Hash取模以后获得的是一个 int 型的整值。

//Objects 类中默认的 hash 方法
 public static int hash(Object... values) {
    return Arrays.hashCode(values);
}
复制代码

既然 hash的计算结果是 int 类型,而 java 中 int 的最小值是-2^31,最大值是2^31-1。意味着任何经过哈希取模以后的无符号值都会在 0 ~ 2^31-1范围之间,共2^32个数。那咱们是否能够不对服务器的数量进行取模而是直接对2^32取模。这就造成了一致性哈希的基本算法思想,什么意思呢?

这里须要注意一点:

默认的 hash 方法结果是有负值的状况,所以须要咱们重写hash方法,保证哈希值的非负性。

简单来讲,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H 的值空间为 0 ~ 2^32-1(即哈希值是一个32位无符号整形),整个哈希环以下:

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

那么,一致性哈希算法与上图中的圆环有什么关系呢?仍然以以前描述的场景为例,假设咱们有4台服务器,服务器0、服务器一、服务器2,服务器3,那么,在生产环境中,这4台服务器确定有本身的 IP 地址或主机名,咱们使用它们各自的 IP 地址或主机名做为关键字进行哈希计算,使用哈希后的结果对2^32取模,可使用以下公式示意:

hash(服务器的IP地址) %  2^32
复制代码

最后会获得一个 [0, 2^32-1]之间的一个无符号整形数,这个整数就表明服务器的编号。同时这个整数确定处于[0, 2^32-1]之间,那么,上图中的 hash 环上一定有一个点与这个整数对应。那么这个服务器就能够映射到这个环上。

多个服务器都经过这种方式进行计算,最后都会各自映射到圆环上的某个点,这样每台机器就能肯定其在哈希环上的位置,以下图所示。

容错性和可扩展性

那么用户访问,如何分配访问的服务器呢?咱们根据用户的 IP 使用上面相同的函数 Hash 计算出哈希值,并肯定此数据在环上的位置,今后位置沿环 顺时针行走,遇到的第一台服务器就是其应该定位到的服务器。

从上图能够看出 用户1 顺时针遇到的第一台服务器是 服务器3 ,因此该用户被分配给服务器3来提供服务。同理能够看出用户2被分配给了服务器2。

1. 新增服务器节点

若是这时须要新增一台服务器节点,一致性哈希策略是如何应对的呢?以下图所示,咱们新增了一台服务器4,经过上述一致性哈希算法计算后得出它在哈希环的位置。

能够发现,原来访问服务器3的用户1如今访问的对象是服务器4,用户能正常访问且服务不须要停机就能够自动切换。

2. 删除服务器节点

若是这时某台服务器异常宕机或者运维撤销了一台服务器,那么这时会发生什么状况呢?以下图所示,假设咱们撤销了服务器2。

能够看出,咱们服务仍然能正常提供服务,只不过这时用户2会被分配到服务1上了而已。

经过一致性哈希的方式,咱们提升了咱们系统的容错性和可扩展性,分布式节点的变更不会影响整个系统的运行且不须要咱们作一些人为的调整策略。

Hash环的数据倾斜问题

一致性哈希虽然为咱们提供了稳定的切换策略,可是它也有一些小缺陷。由于 hash取模算法获得的结果是随机的,咱们并不能保证各个服务节点能均匀的分配到哈希环上。

例如当有4个服务节点时,咱们把哈希环认为是一个圆盘时钟,咱们并不能保证4个服务节点恰好均匀的落在时钟的 十二、三、六、9点上。

分布不均匀就会产生一个问题,用户的请求访问就会不均匀,同时4个服务承受的压力就会不均匀。这种问题现象咱们称之为,Hash环的数据倾斜问题

如上图所示,服务器0 到 服务器1 之间的哈希点值占据比例最大,大量请求会集中到 服务器1 上,而只有极少许会定位到 服务器0 或其余几个节点上,从而出现 hash环偏斜的状况。

若是想要均衡的将缓存分布到每台服务器上,最好能让这每台服务器尽可能多的、均匀的出如今hash环上,可是如上图中所示,真实的服务器资源只有4台,咱们怎样凭空的让它们多起来呢?

既然没有多余的真正的物理服务器节点,咱们就只能将现有的物理节点经过虚拟的方法复制出来。

这些由实际节点虚拟复制而来的节点被称为 "虚拟节点",即对每个服务节点计算多个哈希,每一个计算结果位置都放置一个此服务节点,称为虚拟节点。具体作法能够在服务器IP或主机名的后面增长编号来实现。

如上图所示,假如 服务器1 的 IP 是 192.168.32.132,那么原 服务器1 节点在环形空间的位置就是hash("192.168.32.132") % 2^32

咱们基于 服务器1 构建两个虚拟节点,Server1-A 和 Server1-B,虚拟节点在环形空间的位置能够利用(IP+后缀)计算,例如:

hash("192.168.32.132#A") % 2^32
hash("192.168.32.132#B") % 2^32
复制代码

此时,环形空间中再也不有物理节点 服务器1,服务器2,……,替代的是只有虚拟节点 Server1-A,Server1-B,Server2-A,Server2-B,……。

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到 “Server1-A”、“Server1-B” 两个虚拟节点的数据均定位到 服务器1上。这样就解决了服务节点少时数据倾斜的问题。

在实际应用中,一般将虚拟节点数设置为32甚至更大,所以即便不多的服务节点也能作到相对均匀的数据分布。因为虚拟节点数量较多,与虚拟节点的映射关系也变得相对均衡了。

总结

一致性哈希通常在分布式缓存中使用的也比较多,本篇只介绍了服务的负载均衡和分布式存储,对于分布式缓存其实原理是相似的,读者能够本身触类旁通来思考下。

其实,在分布式存储和分布式缓存中,当服务节点发生变化时(新增或减小),一致性哈希算法并不能杜绝数据迁移的问题,可是能够避免数据的全量迁移,须要迁移的只是更改的节点和它的上游节点它们两个节点之间的那部分数据。

另外,咱们都知道 hash算法 有一个避免不了的问题,就是哈希冲突。对于用户请求IP的哈希冲突,其实只是不一样用户被分配到了同一台服务器上,这个没什么影响。可是若是是服务节点有哈希冲突呢?这会致使两个服务节点在哈希环上对应同一个点,其实我感受这个问题也不大,由于一方面哈希冲突的几率比较低,另外一方面咱们能够经过虚拟节点也可减小这种状况。


我的公众号:JaJian

欢迎长按下图关注公众号:JaJian!

按期为你奉上分布式,微服务等一线互联网公司相关技术的讲解和分析。


1557975294786730.png

相关文章
相关标签/搜索