一致性Hash问题及解决方案
分布式和集群的区别
分布式一定是集群,但是集群不一定是分布式。集群是多个实例一起工作,分布式啊将一个系统拆分,拆分之后就是多个实例。复制性的集群不是拆分,是复制。
更清晰的解释
https://cloud.tencent.com/developer/article/1579435
大白话的解释
专业案例解释
hash算法回顾
什么是hash算法
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
WikiPedia的解释
A hash function is any function that can be used to map data of arbitrary size to fixed-size values.
为什么要使用hash
Hash算法较多的应用在数据存储和查找领域。最经典的就是Hash表,它的查询效率非常之高,其中的 哈希算法如果设计的比较ok的话,那么Hash表的数据查询时间复杂度可以接近于O(1)。
hash算法实例
需求
提供一组数据 1,5,7,6,3,4,8,对这组数据进行存储,然后随便给定一个数n,请你判断n是否存在于刚才的数据集中
分析与实现
顺序查找法
直接遍历
list:List[1,5,7,6,3,4,8]
// 通过循环判断来实现
for(int element: list) {
if(element == n) {
// 如果相等,说明n存在于数据集中
}
}
代码如下
/**
* @name: FindNum
* @author: terwer
* @date: 2022-01-26 14:57
**/
public class FindNum {
public static void main(String[] args) {
int[] nums = new int[]{1, 5, 7, 6, 3, 4, 8};
int num = 3;
boolean result = isExist1(nums, num);
System.out.println(num + "是否存在:" + result);
}
// 顺序查找
public static boolean isExist1(int[] nums, int n) {
for (int num : nums) {
if (num == n) {
return true;
}
}
return false;
}
}
缺点:通过循环来完成,比较原始,效率不高
折半查找法(二分查找)
// 折半查找
public static boolean isExist2(int[] nums, int target) {
System.out.println("折半查找");
// 先排序,jdk使用的是快速排序
Arrays.sort(nums);
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
if (target == nums[middle]) {
return true;
} else if (target < nums[middle]) {
right = middle - 1;
} else if (target > nums[middle]) {
left = middle - 1;
}
}
return false;
}
排序之后折半查找,相对于顺序查找法会提高一些效率,但是效率也并不是特别好
缺点:还是需要循环
直接寻址法
直接把数据和数组的下标绑定到一起,查找的时候,直接array[n]就取出了数据
优点:速度快,一次查找得到结果
缺点
1、浪费空间。例如,1,2,100
2、有重复数据的时候,存储不下。例如,1,2,3,3,2,1,6
hash寻址法
如果数据是3,5,7,12306,一共4个数据,可以用下面方式保存
对数据求模 (数据%空间位置数) 。
这是一个hash算法,叫做除留余数法
开放寻址法
1放进去了,6再来的时候,向前或者向后找空闲位置存放
缺点:如果数组长度固定,可能出现向前或者向后查找也存储不下的情况
拉链法
可以在元素的地方放一个链表。
Hash表的查询效率高不高取决于Hash算法,hash算法的理想实现是能够让数据平均分布,既能够节省空间又能提高查询效率。
常见的hash算法
[ ] 除留余数法 3%5
[ ] 线性构造Hash算法
直接寻址法也是一种构造Hash的方式,只不过更简单,表达式:H(key)=key 比如H(key)=a*key + b(a,b是常量)
[ ] hashcode其实也是通过一个Hash算法得来的
hash算法应用场景
Hash算法在分布式集群架构中的应用场景
Hash算法在很多分布式集群产品中都有应用,比如分布式集群架构Redis、Hadoop、ElasticSearch,
Mysql分库分表,Nginx负载均衡等
最主要的应用场景
请求的负载均衡(比如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同一个目标服务器上,实现会话粘滞,避免处理session共享问题
如果没有IP_hash策略,那么如何实现会话粘滞? 可以维护一张映射表,存储客户端IP或者sessionid与具体目标服务器的映射关系
<ip,tomcat1>
缺点
1)那么,在客户端很多的情况下,映射表非常大,浪费内存空间
2)客户端上下线,目标服务器上下线,都会导致重新维护映射表,映射表维护成本很大
分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器
那么,在进行数据存储时,<key1,value1>数据存储到哪个服务器当中呢?
针对key进行hash处理 hash(key1)%3=index, 使用余数index锁定存储的具体服务器节点
普通hash算法存在的问题
以ip_hash为例,假定下载用户ip固定没有发生改变,现在tomcat3出现 了问题,down机了,服务器数量由3个变为了2个,之前所有的求模都需要重新计算。
如果在真实生产情况下,后台服务器很多台,客户端也有很多,那么影响是很大的,缩容和扩容都会存
在这样的问题,大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的会话都会丢
失。
一致性hash算法
我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端用户,也根据它的ip进行hash求值,对应到环上某个位置。
然后如何确定一个客户端路由到哪个服务器处理呢?按照顺时针方向找最近的服务器节点。
假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有 影响只是这一小部分受影响(请求的迁移达到了最小,这样的算法对分布式集群来说非常合适的,避免
了大量请求迁移 )
增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这 一小部分受影响(请求的迁移达到了最小,这样的算法对分布式集群来说非常合适的,避免了大量请求迁移 )
每一台服务器负责一段,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
缺点:数据的倾斜问题
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中 只有两台服务器,其环分布如下,节点2只能负责非常小的一段,大量的客户端
请求落在了节点1上,这就是数据(请求)倾斜问题
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后面增加编号来实现。比如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的 ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被 路由到该虚拟节点所对应的真实节点
手写实现一致性hash算法
普通Hash算法实现
/**
* 普通hash算法
*
* @name: GeneraHash
* @author: terwer
* @date: 2022-01-26 15:46
**/
public class GeneraHash {
public static void main(String[] args) {
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
// 定义服务器数量
int serverCount = 5;// (编号对应0,1,2)
// hash(ip)%node_counts=index //根据index锁定应该路由到的tomcat服务器
for (String client : clients) {
int hash = Math.abs(client.hashCode());
int index = hash % serverCount;
System.out.println("客户端:" + client + " 被路由到服务器编号为:" + index);
}
}
}
一致性Hash算法实现(不含虚拟节点)
/**
* @name: 一致性Hash算法不含虚拟节点
* @author: terwer
* @date: 2022-01-26 15:52
**/
public class ConsitanceHashNoVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上 // 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每一个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash, tomcatServer);
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪一个服务器节点能够处理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第一台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
一致性hash包含虚拟节点
/**
* @name: 一致性hash包括虚拟节点
* @author: terwer
* @date: 2022-01-26 15:58
**/
public class ConstanceHashVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
// 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
// 定义针对每个真实服务器虚拟出来⼏个节点
int virtaulCount = 3;
for (String tomcatServer : tomcatServers) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash, tomcatServer);
// 处理虚拟节点
for (int i = 0; i < virtaulCount; i++) {
int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
hashServerMap.put(virtualHash, "----由虚拟节点" + i + "映射过来的请求:" + tomcatServer);
}
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
Nginx配置一致性hash负载均衡策略
ngx_http_upstream_consistent_hash 模块是一个负载均衡器,使用一个内部一致性hash算法来选择
合适的后端节点。
该模块可以根据配置参数采取不同的方式将请求均匀映射到后端机器,
consistent_hash $remote_addr:可以根据客户端ip映射
consistent_hash $request_uri:根据客户端请求的uri映射
consistent_hash $args:根据客户端携带的参数进行映
ngx_http_upstream_consistent_hash 模块是一个第三方模块,需要我们下载安装后使用
1、github下载nginx一致性hash负载均衡模块 https://github.com/replay/ngx_http_consistent_hash
2、将下载的压缩包上传到nginx服务器,并解压
3、我们已经编译安装过nginx,此时进入当时nginx的源码目录,执行如下命令
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
4、在nginx.conf文件中配置即可
# 配置负载均衡 upstream loginServer { consistent_hash $request_uri; server 127.0.0.1:8080; server 127.0.0.1:8081; } SCSS 复制 全屏