- 本篇博客从哈希算法的介绍和基本的哈希思想开始,介绍了哈希函数是啥,常见的哈希冲突的解决办法,然后说明了哈希算法在分布式集群架构中的应用。
- 哈希算法在分布式集群架构中一般用作负载均衡,和数据分片。
- 同时也介绍了负载均衡中,一般的哈希算法的局限性,并以此介绍了一致性哈希算法的原理并且用java语言实现了简单的案例。还列举了nginx配置一致性哈希负载均衡的例子。帮助大家理解各个分布式框架的哈希原理
哈希思想
将需要被查找的数据中的,可以表示某个具体数据对象的关键字,转化为数组下标的映射,利用数组按照下标访问元素时事件复杂度为O(1)的特性,实现快速访问。这个将关键字转换为数组下标的方法,就称为哈希函数。通过哈希函数计算得到的值,就是哈希值。
Hash算法常见应用
为什么使用Hash算法
前面两点都可以理解,但是第三个要求,实现起来非常有难度,要找一个没有冲突的Hash函数几乎是不可能。即使时业界知名的MD5、SHA、CRC等Hash算法也没办法解决Hash冲突的问题,因为数组的存储空间总归是有限的。好的Hash函数,应该是计算过程简单,并且数据Hash后分布均匀。
开放寻址法的核心思想:一旦出现哈希冲突,就通过重新探测新位置来解决冲突。
重新探测位置的方法:线性探测法、二次探测法、双重哈希法
线性探测法
二次探测法
hash(key)+0²,hash(key)+1²,hash(key)+2²,......
双重哈希法
hash1(key)、hash2(key)、hash3key)
链表法比开放寻址法要更简单
Hash算法在很多分布式集群产品中都有应用,例如分布式集群架构的Redis、Hadoop、ElasticSearch、Mysql分库分表、Nginx负载均衡等。
主要的应用场景:
会话粘滞:属于同一会话的请求,都会被路由到同一个服务器上。
如果不使用哈希算法,要实现一个带会话粘滞的负载均衡我们一般会维护一个会话ID/IP和机器的映射关系。每次收到一个请求,都根据ip或者会话ID去查询这个路由表。但是这种策略会存在一些弊端
哈希算法实现的负载均衡,虽然能实现会话粘滞,但是在新增服务器时,同一个会话,可能会被分配到新增的服务器上,这时候客户端保存的Cookie在新加的服务器上就是失效的,用户可能就会掉出登录状态。这就是Session丢失。
解决方法:Session保持、Session复制、Session共享
方案 | 说明 | 优略 |
---|---|---|
Session保持(原来在哪儿还去哪儿) | 用nginx举例,nginx对Session保存的支持,需要引入nginx-sticky-module模块。nginx会在新的客户端加入为其分配一个节点的同时,会将节点的唯一标识进行md5后,作为一个cookie一并返回客户端,如果再次收到这个客户端的请求,就根据这个特殊的cookie来将其转发到对应的节点。 | 缺点1. 如果浏览器关闭了cookie就凉了2. 破坏了负载均衡的初衷,如果新增了服务器节点,只要没有新的客户端加入,那么这个节点永远收不到请求。3. 减少节点,仍然会丢失会话。 优点1. 简单 |
Session复制(随便你哪儿都数据都一样) | cookie在每个服务器节点间复制同步,这样部分节点宕机都没事。适用于会话数据小的场景。 | 优点:天然高可用,宕机一部分都不会session丢失。增加节点也不会。 缺点:1. 需要多个节点间来回复制同步数据,带来了数据一致性的问题。2. 会话数据的增多,节点的增多,都会使得数据同步、延迟、等性能的消耗增多。 |
Session共享(随便去哪儿咱都是一份数据) | 中心化思想,将多个节点的会话数据同一保存,这样不论节点增多还是减小都没有影响。100%保持了会话。 | 优点:会话100%保持 缺点:1. 单独的共享介质,增加了额外的成本2. 数据保存的介质需要解决单点问题。同样引入了系统性风险。 |
在分布式缓存的场景中,缓存的数据原本按照普通的哈希算法,通过取模确定好了每个数据保存的服务器。此时如果增加/减少服务器,那么原有的缓存数据,和新的数据保存规则就无法对应了。
举例:查询缓存数据时,由于规则的改变,旧数据可能存在于服务器1,但是按照新的查询规则查询后,由于新增了服务器可能去服务器2查询缓存数据,这时候就无法查询到缓存,出现了缓存穿透问题。对于分布式系统中的节点数量变化时,就需要用到一致性哈希算法。
接着上一段的思路,看似没有问题。但是如果节点数量太少,那么在环上就可能分布的不够均匀,会导致
各个节点收到的请求数量不够均衡也就偏离了负载均衡的本意。于是就引入了虚拟节点的概念。就是确定好某个bucket的哈希标识后,给每个bucket再额外分配一定数量的副本,并且将其也映射到环上。tiem找到了某个副本的话,请求也会被指向对应的bucket。
思路
public class ConsistenHashNoVitureNode {
public static void main(String[] args) {
//1.将节点映射到hash环 bucket(桶)
//定义服务器ip
String[] servers=new String[]{"127.222.1.1","127.153.1.2","127.168.1.3","127.168.1.0"};
SortedMap<Integer,String> serverHashMap=new TreeMap<>();
for (String server:servers){
//求出ip地址与其hash值,并保存映射。此处简单用hashCode,仅作演示
int serverHash = Math.abs(server.hashCode());
//存储映射关系,并且自动按照了Key排序
serverHashMap.put(serverHash,server);
}
//2.针对客户端的ip求出对应的hash值 --项(item)
//这里定义了相同的客户端IP,可以看结果是否被路由到同样的server
String[] clients=new String[]{"127.165.1.1","127.153.1.2","127.222.1.3","127.333.1.4","127.153.1.2","127.333.1.4"};
for (String client : clients) {
Integer clientHash = Math.abs(client.hashCode());
//按照传入key大小,截取比key打的部分
SortedMap<Integer, String> tailMap = serverHashMap.tailMap(clientHash);
//3.针对客户端找到其对应的bucket节点
Integer bucketServerKey=tailMap.isEmpty()?serverHashMap.firstKey():tailMap.firstKey();
String bucketServerIp=serverHashMap.get(bucketServerKey);
System.out.println("客户端ip:"+client+" ===>>> 服务端:"+bucketServerIp);
}
}
}
增加虚拟节点的不同之处就在于,略微修改了真实server的ip后再生成hashCode 作为虚拟节点,这些节点的value仍对应之前的serverip
public class ConsistenHashWithNoVitureNode {
public static void main(String[] args) {
//1.将节点映射到hash环 bucket(桶)
//定义服务器ip
String[] servers = new String[]{"127.222.1.1", "127.153.1.2", "127.168.1.3", "127.168.1.0"};
//定义服务器的hashcode和真实ip的映射
SortedMap<Integer, String> serverHashMap = new TreeMap<>();
//定义虚拟节点的个数
int vitureBucketNum = 3;
for (String server : servers) {
//求出ip地址与其hash值,并保存映射。此处简单用hashCode,仅作演示
int serverHash = Math.abs(server.hashCode());
//存储映射关系,并且自动按照了Key排序
serverHashMap.put(serverHash, server);
//增加虚拟节点
for (int i = 0; i < vitureBucketNum; i++) {
int vitureServerHashCode = new String(server + "#" + i).hashCode();
serverHashMap.put(vitureServerHashCode, "虚拟节点映射:" + server);
}
}
//2.针对客户端的ip求出对应的hash值 --项(item)
String[] clients = new String[]{"127.165.1.1", "127.153.1.2", "127.222.1.3", "127.333.1.4", "127.153.1.2", "127.333.1.4"};
for (String client : clients) {
Integer clientHash = Math.abs(client.hashCode());
//按照传入key大小,截取比key打的部分
SortedMap<Integer, String> tailMap = serverHashMap.tailMap(clientHash);
//3.针对客户端找到其对应的bucket节点
Integer bucketServerKey = tailMap.isEmpty() ? serverHashMap.firstKey() : tailMap.firstKey();
String bucketServerIp = serverHashMap.get(bucketServerKey);
System.out.println("客户端ip:" + client + " ===>>> 服务端:" + bucketServerIp);
}
}
}
nginx是一个模块化的程序,初始安装时是不支持一致性哈希算法的,需要引入额外的模块。
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install