简介
负载均衡(Load Balance),本文主要介绍软件层面负载均衡常用的几种方案.负载均衡的目的主要是将服务能力分摊到多个操作单元上分别进行执行,从而共同完成工作作务. 从不同的视角,来采用负载均衡的方案是不同的,这些视角包含如下两种:
- 从如何均匀分发服务请求到对称结构中的某一台服务器上.
- 从后端服务器的视角来观察系统的负载,尽量让请求分发现负载较小的机器上.
本文将着重从第一个视角来讲解常见的几种负载均衡方案.
轮询方式
概念
轮询方式主要是指,将请求均匀的依次分布到各个服务节点上去,简单说就是将服务节点的机器按某顺序不断循环,每来一个请求,将依次分发到不同的机器上去.
代码示例
模拟有多台服务机器,放到在一个列表里面,每个机器设有权重.
public class IpMap
{
// 待路由的Ip列表,Key代表Ip,Value代表该Ip的权重
public static HashMap<String, Integer> serverWeightMap =
new HashMap<String, Integer>();
static
{
serverWeightMap.put("192.168.1.100", 1);
serverWeightMap.put("192.168.1.101", 1);
serverWeightMap.put("192.168.1.103", 1);
serverWeightMap.put("192.168.1.104", 1);
serverWeightMap.put("192.168.1.106", 1);
serverWeightMap.put("192.168.1.108", 1);
serverWeightMap.put("192.168.1.109", 1);
serverWeightMap.put("192.168.1.110", 1);
// 权重为4
serverWeightMap.put("192.168.1.102", 4);
// 权重为3
serverWeightMap.put("192.168.1.105", 3);
// 权重为2
serverWeightMap.put("192.168.1.107", 2);
}
}
轮询代码模拟实现
public class RoundRobin
{
private static Integer pos = 0;
public static String getServer()
{
// 重建一个Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);
// 取得Ip地址List
Set<String> keySet = serverMap.keySet();
ArrayList<String> keyList = new ArrayList<String>();
keyList.addAll(keySet);
String server = null;
synchronized (pos)
{
if (pos >= keySet.size())
pos = 0;
server = keyList.get(pos);
pos ++;
}
return server;
}
}
从上面代码可以看出,该方式主要思想是当一个请求过来,需要转发到某服务机器上去时,轮询方式就是把当前服务器列表循环一次,依据之前分配过请求的次数顺序取出当次应被分发的机器IP名称.
这里须要注意的时,每来一个请求,都需要把现可以提供服务的机器遍历一次,也就是说在使用该方式的时候,就需要考虑维护 可用服务机器的IP列表. 同时,为解决并发访问 在获取机器列表时,添加重量级的锁,来保证每一个请求(线程)请求转移的绝对均衡.
优点
实现很简单, 运行也很高效, 易水平扩展,如果用做于路由策略的话,非常适合数据库或服务中只有只读的场景.
缺点
当可用服务列表更新不及时,该方式会出现服务不可用情况,这就需要服务端有重试机制,并无法保证服务轮询的绝对顺序性. 并且在大吞吐量的请求并发现 负载路由性能会明显下降. 还存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
随机方式
概念
类似轮询方式,只是在分配可用服务器节点是随机的,并不像轮询方式那样是依次的,顺序的.主要是依据可服务节点的大小 来计算随机数,由概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到每一台后端服务器,也就是轮询的效果.
代码示例
public class Random
{
public static String getServer()
{
// 重建一个Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);
// 取得Ip地址List
Set<String> keySet = serverMap.keySet();
ArrayList<String> keyList = new ArrayList<String>();
keyList.addAll(keySet);
java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(keyList.size());
return keyList.get(randomPos);
}
}
优点
实现简单,方便水平扩展,基于概率统计的理论,吞吐量越大,随机算法的效果越接近于轮询算法的效果.
缺点
如果用做于路由策略的话,受限于可用服务节点列表变更,很适合只读的场景,不适合写入的场景.
源地址哈希值方式
概念
源地址哈希值是指, 先获取到请求过来的源地址的IP,取的该IP字符串的hashcode值,最终与可用服务器列表的大小取摸便得到该请求被分发到机器上的可用服务器列表的序号.如果可用服务器列表一直不变,那么该序号也会一直不变,即同源地址的请求会永远落到同一台服务节点上.但若服务节点发生变更,但不会出现同源请求会被分发到另一节点上.
代码示例
public class Hash
{
public static String getServer()
{
// 重建一个Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);
// 取得Ip地址List
Set<String> keySet = serverMap.keySet();
ArrayList<String> keyList = new ArrayList<String>();
keyList.addAll(keySet);
// 在Web应用中可通过HttpServlet的getRemoteIp方法获取
String remoteIp = "127.0.0.1";
int hashCode = remoteIp.hashCode();
int serverListSize = keyList.size();
int serverPos = hashCode % serverListSize;
return keyList.get(serverPos);
}
}
优点
保证了相同客户端IP地址将会被哈希到同一台后端服务器,直到后端服务器列表变更。根据此特性可以在服务消费者与服务提供者之间建立有状态的session会话。
缺点
除非集群中服务器的非常稳定,基本不会上下线,否则一旦有服务器上线、下线,那么通过源地址哈希算法路由到的服务器是服务器上线、下线前路由到的服务器的概率非常低,如果是session则取不到session,如果是缓存则会引入缓存命中率下降,带来数据库压力突增,在高访问量下的情况下很有可能引发”雪崩”。
加权轮询
概念
可用服务器节点,由于硬件配制不一或是对外提供服务数量差异,必然会带来某服务节点处理能强,一些节点处理能弱,抗压能力都不一样. 这样就希望服务能力强的节点多处理些作务,被分发到的请求多些, 服务能力弱的节点少处理些任务. 为标识这种能力强若的状态,采用加权重的方式,权值越高,表示被分发的概率越大. 加权轮询理解起来便是 负载均衡的方式还是轮询方式来,通过节点的权重值来多次重负该节点以达到增加节点被轮询的机率
代码示例
public class WeightRoundRobin
{
private static Integer pos;
public static String getServer()
{
// 重建一个Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);
// 取得Ip地址List
Set<String> keySet = serverMap.keySet();
Iterator<String> iterator = keySet.iterator();
List<String> serverList = new ArrayList<String>();
while (iterator.hasNext())
{
String server = iterator.next();
int weight = serverMap.get(server);
for (int i = 0; i < weight; i++)
serverList.add(server);
}
String server = null;
synchronized (pos)
{
if (pos > keySet.size())
pos = 0;
server = serverList.get(pos);
pos ++;
}
return server;
}
}
实现与轮询法类似,只是在获取服务器地址之前增加了一段权重计算的代码,根据权重的大小,将地址重复地增加到服务器地址列表中,权重越大,该服务器每轮所获得的请求数量越多。 主要思想依然是轮询方式的实现,但在权重高的节点上变的不可用,并在服务节点没有及时更新的情况下,会把轮询方式的缺点给放大化.
加权随机方式
概念
加权随机法是根据后端服务器不同的配置和负载情况来配置不同的权重。不同的是,它是按照权重来随机选择服务器的,而不是顺序。加权随机法的代码实现如下
代码示例
public class WeightRandom
{
public static String getServer()
{
// 重建一个Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);
// 取得Ip地址List
Set<String> keySet = serverMap.keySet();
Iterator<String> iterator = keySet.iterator();
List<String> serverList = new ArrayList<String>();
while (iterator.hasNext())
{
String server = iterator.next();
int weight = serverMap.get(server);
for (int i = 0; i < weight; i++)
serverList.add(server);
}
java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(serverList.size());
return serverList.get(randomPos);
}
}
从上代码与之前随机方式比较看出,加权就是在可用服务表集合中依据权重值使当前服务节点多次重复添加.使得最终被选取的概率变大.
最小连接方式
概念
最小连接方式,其实是从文章开篇说的视角2来定义的. 主要是从服务机群中选出当前这一刻正在处理请求的数量最小的某服务节点. 从在各节点处理性能相差不太大的前提下思考,认为当前节点处理请求的数量最小,说明闲置的处理资源最多,请求就应分发到该节点上,这样才能达到资源充分利用的状态. 如前面5种方式, 若是费劲心思,怎样让请求分配的更加均衡,但却忽视当前各节点的负载情况,很有可能出现 服务节点各自处理资源分配不均的情况, 忙的更忙,闲着更闲. 最小连接方式就是让将请求分发现当前处理请求数最少的可用机器节点上.