什么是一致性hash
可能听到这个词还是比较陌生的,一致性hash就是在集群环境下,我们分配哪些用户访问集群中哪个服务器的一种策略算法。
在一些高并发量的场景下,我们的负载均衡有很多算法。而hash负载均衡也是其中的一种负载均衡策略。那hash算法实现的好的话,会将我们的所有用户请求均匀的分配到集群中的机器上面,且不会因为集群中服务器的宕机而引发一系列问题。
我们通过模拟场景来实践一下吧。比如现在我们一个服务的集群有三台服务器,有六个用户想要请求我们的服务器。我们可以按照最简单的对用户的ip进行hash并跟集群数量取模的方式来确定该用户所访问的服务器。代码如下:
/**
* 普通hash
* 缺点是一台服务器宕机会导致所有客户端重新分配服务器
*/
public class NormalHash {
public static void main(String[] args) {
String[] servers = new String[] {"10.10.8.1", "10.10.8.2", "10.10.8.3"};
String[] clients = "192.168.0.1,192.168.0.2,192.168.0.3,192.168.0.4,192.168.0.5,192.168.0.6".split(",");
for (int i = 0; i < clients.length; i++) {
String client = clients[i];
int serverIndex = Math.abs(client.hashCode()) % servers.length;
String server = servers[serverIndex];
System.out.println("客户端:" + client + " 被路由到服务器: " + server);
}
}
}
这种方式看起来没什么问题,但是集群中某个机器宕机的情况下会引发所有用户访问机器的重新计算。这在大并发下极易引发问题。那么,为了解决这个问题,我们引入了hash环来解决这个问题。所谓的hash环就是将0到int的最大值作为一个环,我们将服务器的ip求hash之后,将其hash计算的值记录到hash环上。这样,我们的用户请求到来时,我们根据相同的算法对用户的ip进行hash运算,并以此结果为起点找到离此hash值最近的服务器即可。如下图:

这样的话,既是我们的某台服务器宕机,只会影响该服务器节点到上个节点之间的用户,而不至于全部用户都发生服务器重新分配。我们的代码简单实现如下所示:
/**
* 一致性hash
* 一台服务器宕机只会影响一小部分客户端的服务器分配
* 缺点是可能发生节点之间压力分配不均衡
*/
public class ConsistencyHash {
public static void main(String[] args) {
String[] servers = new String[] {"10.10.8.1", "10.10.8.2", "10.10.8.3"};
String[] clients = "192.168.0.1,192.168.0.2,192.168.0.3,192.168.0.4,192.168.0.5,192.168.0.6".split(",");
// 先给服务器分配hash环的节点
SortedMap<Integer, String> hashCircle = new TreeMap<>();
for (int i = 0; i < servers.length; i++) {
hashCircle.put(Math.abs(servers[i].hashCode()), servers[i]);
}
// 给客户端分配服务器
for (int j = 0; j < clients.length; j++) {
String client = clients[j];
int clientHash = Math.abs(client.hashCode());
SortedMap<Integer, String> pickedServerChain = hashCircle.tailMap(clientHash);
String pickedServer;
if (pickedServerChain.isEmpty()) {
pickedServer = hashCircle.get(hashCircle.firstKey());
} else {
pickedServer = pickedServerChain.get(pickedServerChain.firstKey());
}
System.out.println("客户端:" + client + " 被路由到服务器: " + pickedServer);
}
}
}
上述算法算是完美吗?其实答案是不是的。我们假如服务器的数量不是很多,机器hash之后在hash环上距离比较近。这样会造成大量用户都会分配到某一台机器上,而其他机器用户量很少的情况。那为了解决这种情况,我们通过添加虚拟节点的方式,来将我们的机器均匀的分配到hash环上。所谓的虚拟节点,就是模拟出真实机器的虚拟ip,并将该节点指向真是机器的ip。如下图所示:

算法的简单实现如下:
/**
* 一致性hash 添加了虚拟节点
* 一台服务器宕机只会影响一小部分客户端的服务器分配
* 通过虚拟节点来平衡各个服务器之间的压力
*/
public class ConsistencyHashWithVirtualNode {
public static void main(String[] args) {
String[] servers = new String[] {"10.10.8.1", "10.10.8.2", "10.10.8.3"};
String[] clients = new String[] {"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
// 先给服务器分配hash环的节点
SortedMap<Integer, String> hashCircle = new TreeMap<>();
for (int i = 0; i < servers.length; i++) {
hashCircle.put(Math.abs(servers[i].hashCode()), servers[i]);
}
// 每个服务器分配三个虚拟节点
int virtualNodeNum = 3;
for (String server : servers) {
for (int j = 0; j < virtualNodeNum; j++) {
hashCircle.put(Math.abs((server + "#" + j).hashCode()), server);
}
}
// 给客户端分配服务器
for (int k = 0; k < clients.length; k++) {
String client = clients[k];
int clientHash = Math.abs(client.hashCode());
SortedMap<Integer, String> pickedServerChain = hashCircle.tailMap(clientHash);
String pickedServer;
if (pickedServerChain.isEmpty()) {
pickedServer = hashCircle.get(hashCircle.firstKey());
} else {
pickedServer = pickedServerChain.get(pickedServerChain.firstKey());
}
System.out.println("客户端:" + client + " 被路由到服务器: " + pickedServer);
}
}
}
Nginx配置一致性hash负载均衡器
我们在Nginx讲解的博客中知道,Nginx默认的负载均衡策略有轮询策略和ip hash策略。默认的ip hash算法就像我们上述第一种算法一样对用户ip进行的普通hash跟服务器的数量取余来挑选服务。我们Nginx的ngx_http_upstream_consistent_hash
扩展模块实现了一致性hash的负载均衡策略,是通过一致性hash环的方式来增强普通hash负载均衡的算法。
插件安装
我们在github上下载或者下载附件。

将下载的压缩包上传到Nginx服务器并解压。如果我们之前编译过Nginx的话,就需要重新编译一下。进入Nginx的源码目录,执行以下命令:
// 后面的路径根据真实情况修改
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
重新编译后,我们就可以使用了。
插件使用
该插件可以根据配置参数采取不同的方式将请求均匀映射到后端机器,支持的方式如下:
consistent_hash $remote_addr
:可以根据客户端ip映射consistent_hash $request_uri
:根据客户端请求的uri映射consistent_hash $args
:根据客户端携带的参数进行映射
配置示例如下:
文章评论