一 哈希简介
1.1 简介
我们首先来简单介绍一下什么是哈希(以下简称hash),hash本质来说就是映射,或者说是键值对key-value,不同的hash之间不过就是实现key-value映射的算法不同,例如java中计算对象的hashcode值会有不同的算法,常用于各种分布式存储分片的id取模算法等,都属于hash算法。
分布式系统中,假设有 n 个节点,传统方案使用 mod(key, n) 映射数据和节点。
当扩容或缩容时(哪怕只是增减1个节点),映射关系变为 mod(key, n+1) / mod(key, n-1),绝大多数数据的映射关系都会失效。

1.2算法原理:
映射方案

1.2.1公用哈希函数和哈希环
设计哈希函数 Hash(key),要求取值范围为 [0, 2^32)
各哈希值在上图 Hash 环上的分布:时钟12点位置为0,按顺时针方向递增,临近12点的左侧位置为2^32-1。

1.2.2 节点(Node)映射至哈希环
如图哈希环上的绿球所示,四个节点 Node A/B/C/D,
其 IP 地址或机器名,经过同一个 Hash() 计算的结果,映射到哈希环上。

1.2.3 对象(Object)映射于哈希环
如图哈希环上的黄球所示,四个对象 Object A/B/C/D,
其键值,经过同一个 Hash() 计算的结果,映射到哈希环上。

1.2.4 对象(Object)映射至节点(Node)
在对象和节点都映射至同一个哈希环之后,要确定某个对象映射至哪个节点,
只需从该对象开始,沿着哈希环顺时针方向查找,找到的第一个节点,即是。
可见,Object A/B/C/D 分别映射至 Node A/B/C/D。
删除节点
现实场景:服务器缩容时删除节点,或者有节点宕机。如下图,要删除节点 Node C:
只会影响欲删除节点(Node C)与上一个(顺时针为前进方向)节点(Node B)与之间的对象,也就是 Object C,
这些对象的映射关系,按照 2.1.4 的规则,调整映射至欲删除节点的下一个节点 Node D。
其他对象的映射关系,都无需调整。


增加节点
现实场景:服务器扩容时增加节点。比如要在 Node B/C 之间增加节点 Node X:
只会影响欲新增节点(Node X)与上一个(顺时针为前进方向)节点(Node B)与之间的对象,也就是 Object C,
这些对象的映射关系,按照 2.1.4 的规则,调整映射至新增的节点 Node X。
其他对象的映射关系,都无需调整。

虚拟节点
对于前面的方案,节点数越少,越容易出现节点在哈希环上的分布不均匀,导致各节点映射的对象数量严重不均衡(数据倾斜);相反,节点数越多越密集,数据在哈希环上的分布就越均匀。
但实际部署的物理节点有限,我们可以用有限的物理节点,虚拟出足够多的虚拟节点(Virtual Node),最终达到数据在哈希环上均匀分布的效果:
如下图,实际只部署了2个节点 Node A/B,
每个节点都复制成3倍,结果看上去是部署了6个节点。
可以想象,当复制倍数为 2^32 时,就达到绝对的均匀,通常可取复制倍数为32或更高。
虚拟节点哈希值的计算方法调整为:对“节点的IP(或机器名)+虚拟节点的序号(1~N)”作哈希。
算法实现
一致性哈希算法有多种具体的实现,包括 Chord 算法,KAD 算法等,都比较复杂。
这里给出一个简易实现及其演示,可以看到一致性哈希的均衡性和单调性的优势。
单调性在本例中没有统计数据,但根据前面原理可知,增删节点后只有很少量的数据需要调整映射关系。

3.1 源码
public class ConsistentHashing {
// 物理节点
private Set physicalNodes = new TreeSet() {
{
add(“192.168.1.101”);
add(“192.168.1.102”);
add(“192.168.1.103”);
add(“192.168.1.104”);
}
};

//虚拟节点
private final int VIRTUAL_COPIES = 1048576; // 物理节点至虚拟节点的复制倍数
private TreeMap<Long, String> virtualNodes = new TreeMap<>(); // 哈希值 => 物理节点// 32位的 Fowler-Noll-Vo 哈希算法
// https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
private static Long FNVHash(String key) {final int p = 16777619;Long hash = 2166136261L;for (int idx = 0, num = key.length(); idx < num; ++idx) {hash = (hash ^ key.charAt(idx)) * p;}hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;if (hash < 0) {hash = Math.abs(hash);}return hash;
}// 根据物理节点,构建虚拟节点映射表
public ConsistentHashing() {for (String nodeIp : physicalNodes) {addPhysicalNode(nodeIp);}
}// 添加物理节点
public void addPhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.put(hash, nodeIp);}
}// 删除物理节点
public void removePhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.remove(hash);}
}// 查找对象映射的节点
public String getObjectNode(String object) {long hash = FNVHash(object);SortedMap<Long, String> tailMap = virtualNodes.tailMap(hash); // 所有大于 hash 的节点Long key = tailMap.isEmpty() ? virtualNodes.firstKey() : tailMap.firstKey();return virtualNodes.get(key);
}// 统计对象与节点的映射关系
public void dumpObjectNodeMap(String label, int objectMin, int objectMax) {// 统计Map<String, Integer> objectNodeMap = new TreeMap<>(); // IP => COUNTfor (int object = objectMin; object <= objectMax; ++object) {String nodeIp = getObjectNode(Integer.toString(object));Integer count = objectNodeMap.get(nodeIp);objectNodeMap.put(nodeIp, (count == null ? 0 : count + 1));}// 打印double totalCount = objectMax - objectMin + 1;System.out.println("======== " + label + " ========");for (Map.Entry<String, Integer> entry : objectNodeMap.entrySet()) {long percent = (int) (100 * entry.getValue() / totalCount);System.out.println("IP=" + entry.getKey() + ": RATE=" + percent + "%");}
}public static void main(String[] args) {ConsistentHashing ch = new ConsistentHashing();// 初始情况ch.dumpObjectNodeMap("初始情况", 0, 65536);// 删除物理节点ch.removePhysicalNode("192.168.1.103");ch.dumpObjectNodeMap("删除物理节点", 0, 65536);// 添加物理节点ch.addPhysicalNode("192.168.1.108");ch.dumpObjectNodeMap("添加物理节点", 0, 65536);
}

}

3.2 复制倍数为 1 时的均衡性
修改代码中 VIRTUAL_COPIES = 1(相当于没有虚拟节点),运行结果如下(可见各节点负荷很不均衡):

======== 初始情况 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.103: RATE=28%
IP=192.168.1.104: RATE=22%
======== 删除物理节点 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.104: RATE=51%
======== 添加物理节点 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.104: RATE=32%
IP=192.168.1.108: RATE=18%
3.2 复制倍数为 32 时的均衡性
修改代码中 VIRTUAL_COPIES = 32,运行结果如下(可见各节点负荷比较均衡):

======== 初始情况 ========
IP=192.168.1.101: RATE=29%
IP=192.168.1.102: RATE=21%
IP=192.168.1.103: RATE=25%
IP=192.168.1.104: RATE=23%
======== 删除物理节点 ========
IP=192.168.1.101: RATE=39%
IP=192.168.1.102: RATE=37%
IP=192.168.1.104: RATE=23%
======== 添加物理节点 ========
IP=192.168.1.101: RATE=35%
IP=192.168.1.102: RATE=20%
IP=192.168.1.104: RATE=23%
IP=192.168.1.108: RATE=20%

3.2 复制倍数为 1M 时的均衡性
修改代码中 VIRTUAL_COPIES = 1048576,运行结果如下(可见各节点负荷非常均衡):

======== 初始情况 ========
IP=192.168.1.101: RATE=24%
IP=192.168.1.102: RATE=24%
IP=192.168.1.103: RATE=25%
IP=192.168.1.104: RATE=25%
======== 删除物理节点 ========
IP=192.168.1.101: RATE=33%
IP=192.168.1.102: RATE=33%
IP=192.168.1.104: RATE=33%
======== 添加物理节点 ========
IP=192.168.1.101: RATE=25%
IP=192.168.1.102: RATE=24%
IP=192.168.1.104: RATE=24%
IP=192.168.1.108: RATE=24%

二 面临的问题
一个算法的出现一定是为了解决某个问题或者是某类问题,理解算法解决了什么样的问题非常有助于我们理解算法本身,那么一致性哈希是为了解决什么样的问题呢?我们首先来看一下普通的hash算法会遇到什么样的问题,我们以id取模算法为例,这种算法经常被用到分布式存储的分片算法中:

如图所示,假如我们以id % 3作为分片条件,有1-20这些元素,这样这20个元素会按照与3取模的结果分布在0、1、2这三个片中,一切看起来都简单又和谐,但随着业务的发展,我们可能需要扩容,需要再加一个片,我们需要把算法换成id%4,这个时候会发生什么样的变化呢?

对比两个图,我们发现,扩容了一个分片之后,百分之七八十的的数据都发生了迁移,大规模的数据迁移就是这个算法的缺点所在。如果是我们示例中的这种小规模数据,可能影响还不是很大,但是在企业级应用中,可能需要操作的是十亿百亿规模的数据,这时候要迁移它们当中百分之七八十的数据,复杂度和危险性都是非常高的。

有一种方法能够减小数据迁移的规模,就是成倍扩容,例如示例中的3个片我们直接扩容成6个片,这样可以将数据迁移的规模减小到50%,如果读者阅读过HashMap的源码,会发现,HashMap在扩容时调用的resize方法就是将容量扩容为原来的2倍,笔者当时在阅读HashMap源码时就没搞懂为什么一定要扩容两倍,原因就是在这了,就是为了减少数据迁移的规模。

但是这种方式又会引入另外两个问题,一个是资源浪费,可能我们的业务发展和体量暂时不需要扩容一倍,所以直接扩容一倍之后会造成一定的资源浪费。另一个是成本问题,扩容意味着增加服务器,成倍扩容无疑意味着需要更多的服务器,成本还是很高的。这两个问题在大规模集群中尤为明显。
一致性哈希
下面我们来看一致性哈希是如何解决这些问题的,首先我们来看网上经常能看到的有关一致性哈希的一张图:

idmod = id % 100;
if (idmod >= 0 && idmod < 25) {
return db1;
} else if (idmod >= 25 && idmod < 50) {
return db2;
} else if (idmod >= 50 && idmod < 75) {
return db3;
} else {
return db4;
}
我们用id % 100的结果作为分片的依据,并将集群分为四个片,每个片对应一段区间,这时,假如我们发现db3对应的区间也就是idmod在50-75之间的数据发生了热点情况,我们需要对这个片进行扩容,那我们可以将算法改造成这样:

idmod = id % 100;
if (idmod >= 0 && idmod < 25) {
return db1;
} else if (idmod >= 25 && idmod < 50) {
return db2;
} else if (idmod >= 50 && idmod < 65) {
return db3;
} else if (idmod >= 65 && idmod < 75) {
return db5;
} else {
return db4;
}
我们扩容了一个分片db5,并将原来的热点分片db3中的一部分区间中的数据迁移到db5中,这样就可以在不影响其他分片的情况下完成数据迁移,扩容的节点数量也可以进行控制,这就是一致性哈希。

我们再回头看一下上面这样图,大概的意思就是这样,原本有四个节点node1-node4,图中粉色的点就是我们id取模之后的值落到这个环上的位置,这就是所谓的哈希环,然后扩容了一个蓝色的节点node5,扩容只会影响原来node2-node4之间的数据。

一致性哈希也同样有它的问题所在,我们上面提到,一致性哈希可以解决热点问题,那如果我们的数据分布的很均匀,没有热点问题,还是需要扩容,怎么办,按照上文的理解就需要为每个节点都扩容一个节点,这不又是成倍的扩容了么,又遇到了这个n -> 2n的问题,该怎么解决呢?

虚拟节点
我们来做这样一个映射,首先将id % 65536,这样可以得到0-65535这样一个区间,然后做一个这样的映射:

hash id node id
0 0
1 1
2 2
3 3
4 0
5 1
… …
65535 3
这个时候如果我们需要扩容节点,增加一个节点node id为4,我们只需要调整这张虚拟节点的映射表,随意的按照我们的需求来调整,比如我们可以将hash id为5、6、7的数据映射到node id为4的节点上,所以虚拟节点的关键就是我们要维护好这张映射表。这里id与多少取模选择了65536,实际应用中取多少合适呢?很显然,这个值越大,分布就会越均匀,我们可以调整的空间也越大,但是实现和维护的难度也会上升,所以实际应用中到底应该取什么值还是需要结合实际业务来做出权衡。

总结
无论是哪种算法,它们要解决的问题都是尽量的减少数据迁移的规模,还有就是减少扩容的成本,那是不是说我们就一定要选择虚拟节点的这种算法呢?恰恰相反,我们推荐尽量使用的简单的方法来解决问题,不要一开始就使用复杂的方式,这样很容易产生过度设计,虚拟节点的算法虽然可以解决n -> 2n和数据迁移规模的问题,但它的缺点就是比较复杂,实现复杂,维护也复杂,所以我们推荐应用一开始尽量优先选用id取模的算法也就是n -> 2n的方式进行扩容,当集群到达一定规模之后,我们可以做一张如上的虚拟节点映射表,将原来的取模算法平滑的切换为虚拟节点算法,对应用没有任何影响,然后再按照虚拟节点的方式进行扩容,这是我们最推荐的方式。

Hash一致性算法(分片机制)相关推荐

  1. 使用.Net Core实现FNV分布式hash一致性算法

    说到FNV哈希算法不得不提Memcached,我们先简单介绍一下Memcached. Memcached Memcached分为客户端与服务端,Memcached是服务端,服务端本身不提供分布式实现, ...

  2. 面试高频题:Hash一致性算法是如何解决数据倾斜问题的?

    一致性Hash是一种特殊的Hash算法,由于其均衡性.持久性的映射特点,被广泛的应用于负载均衡领域,如nginx和memcached都采用了一致性Hash来作为集群负载均衡的方案. 本文将介绍一致性H ...

  3. jedis ShardedJedisPool的 HASH一致性算法(一)从String 的hashcode说起

    jedis 的shard使用的是MurmurHash算法(一种非加密型哈希函数),该算法已在nginx,hadoop等开源上使用. 回顾String的hashcode()方法 //把char型数字转换 ...

  4. hash一致性算法理解

    http://www.zsythink.net/archives/1182 转载于:https://www.cnblogs.com/huangzelin/p/9662563.html

  5. java如何实现redis分片存储_Redis的分片机制

    前言:大家都知道redis单台的默认内存大小一般是10M.如果现在需要我们将1G的数据保存到内存中.这该如何做到呢?比如我们就用单台redis,但是一味的扩大单台redis内存则直接影响执行的效率,会 ...

  6. Redis - Redis分布式算法原理——Hash一致性理解 Hash倾斜性解决方案

    最近有小伙伴跑过来问什么是Hash一致性算法,说面试的时候被问到了,因为不了解,所以就没有回答上,问我有没有相应的学习资料推荐,当时上班,没时间回复,晚上回去了就忘了这件事,今天突然看到这个,加班为大 ...

  7. 算法高级(19)-不得不懂的Redis Cluster数据分片机制

    亲爱的同学们,你是否使用过Redis集群呢?那Redis集群的原理又是什么呢?记住下面两句话: Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续 ...

  8. 从分布式一致性算法到区块链共识机制

    引言 分布式一致性是一个很"古典"的话题,即在分布式系统中,如何保证系统内的各个节点之间数据的一致性或能够就某个提案达成一致.这个问题想必对于很多技术同学而言并不陌生,几乎在所有的 ...

  9. hash算法_hash一致性算法

    一致性hash算法是,1097麻省理工提出的分布式hashDHT实现算法,极倔internet的热点问题 平衡性 hash结果尽可能的分布到所有的缓存中去,缓冲空间利用率最高 单调性 保持已有的缓存能 ...

最新文章

  1. 【mysql错误】用as别名 做where条件,报未知的列 1054 - Unknown column 'name111' in 'field list'...
  2. ----==《在路上》==----
  3. 解决opencv错误AttributeError: module ‘cv2.cv2‘ has no attribute ‘xfeatures2d‘
  4. 传统的Web应用程序和RESTful API
  5. 基于visual Studio2013解决面试题之0702输出数字
  6. 地球十大人类禁区!让人不寒而栗
  7. hadoop--Shuffle机制
  8. 2013.8.4thinkPHp学习
  9. 零基础学python大概要多久-零基础学Python要多久
  10. JNA模拟复杂的C类型——Java映射char*、int*、float*、double*
  11. java xmpp即时通讯_Android基于Xmpp的即时通讯
  12. 文件属性安全组误删恢复
  13. Java开发常用在线工具
  14. 图片按日期批量导入WPS表格
  15. 播放音乐的开始暂停用一张图片
  16. adb 工具源码修改
  17. Negroni源码分析
  18. 初识搜索引擎 —— ElasticSearch
  19. 刚开始有点紧张,闪送第一单
  20. 微信公众平台接口,asp.net实现

热门文章

  1. ClickHouse 来自战斗民族的OLAP利器
  2. 给女朋友写的简单的公众号推送信息程序
  3. python怎么打开h5文件_python中利用h5py模块读取h5文件中的主键方法
  4. “双料王”傍身,极智嘉Geek+成功开辟行业新蓝海
  5. edvac是商用计算机吗,计算机系统发展历程.ppt
  6. iReport制作EXCEL、PDF或者HTML文件
  7. MySQL连接、SSL加密与密码插件
  8. SQL学习(五):lastday函数(返回指定日期所在月份的最后一天)
  9. [转] Boost算法
  10. 基于Xcode8.x的CoreData的使用