负载均衡算法 — 轮询算法
一、前言
学习负载均衡的轮询算法简单记录一下…
这里使用的服务实例集群如下:
服务实例 | 权重值 |
---|---|
192.168.0.1:8100 | 3 |
192.168.0.1:8200 | 1 |
192.168.0.1:8300 | 1 |
二、轮询算法
1.简单轮询
简单轮询是轮询算法中最简单的一种,由于其不支持灵活的负载均衡配置,因此在大部分情况下应用较少,只适用于简单的业务场景
算法描述:假设有N个实例S={S1,S2,S3,...,Sn}
,定义轮询到当前实例的指针cur,初始化为-1,算法过程描述为:
- 轮询到下一个实例
- 若所有实例已经调度完,则从头开始调度
- 重复1、2步骤
Java代码实现
轮询算法的核心方法有初始化方法init()
和调度方法next()
//服务节点实例
class ServerNode{
public String host;
public String port;
public Integer value; //节点权重值
public ServerNode(String host, String port, Integer value) {
this.host = host;
this.port = port;
this.value = value;
}
@Override
public String toString() {
return host + ":" + port+ "("+ value +")";
}
}
//轮询算法接口
interface RobinInterface{
/**
* 初始化服务节点
**/
public void init(List<ServerNode> list);
/**
* 调度下一个节点
**/
public ServerNode next();
}
上面通过一个类ServerNode
来代表服务节点,接着实现上述接口
class SimpleRobin implements RobinInterface{
private List<ServerNode> arr;
private int cur = -1;
private int cnt;
@Override
public void init(List<ServerNode> list) {
this.arr = list;
this.cnt = list.size();
}
@Override
public ServerNode next() {
this.cur = (this.cur + 1) % this.cnt;
return this.arr.get(cur);
}
}
这里arr
表示服务节点列表,cur
表示当前调度的节点的位置,cnt
表示服务节点个数
接着发送10个请求测试轮询结果
ArrayList<ServerNode> nodes = new ArrayList<>();
nodes.add(new ServerNode("192.168.0.1","8100",3));
nodes.add(new ServerNode("192.168.0.1","8200",1));
nodes.add(new ServerNode("192.168.0.1","8300",1));
SimpleRobin simpleRobin = new SimpleRobin();
simpleRobin.init(nodes); //初始化
for (int i = 0; i < 10; i++) {
System.out.println("第"+ i +"个请求发送到" + simpleRobin.next() + "节点");
}
结果如下:
**分析:**从结果可以看出,简单轮询不管节点的权重是如何,其都是按顺序一个一个节点进行访问的。在实际应用中,不同的服务节点性能肯定会有所不同,若直接使用简单轮询的话,给各个服务实例赋予相同的负载,那必然会出现资源浪费的情况,因此为了更好地改善这种算法的不足,就提出了下面的算法—加权轮询
2.普通加权轮询
加权轮询与简单轮询相比,引入了“权值”这一概念,我们可以通过配置不同性能的服务节点有不同的权重,合理分配各节点的负载,达到资源的合理利用。
算法描述:假设有N个实例S={S1,S2,S3,...,Sn}
,对应的权重W={W1,W2,W3,...,Wn}
,定义轮询到当前实例的指针cur,初始化为-1,curWeight表示当前权重,初始化为max(W),gcd(W)表示所有权重的最大公约数,算法过程描述为:
- 从上一个调度的实例开始,遍历后面的所有实例
- 如果已经遍历完所有实例,则当前权重的值减少
this.curWeight -= gcd(W)
,减少的大小为这些实例的最大公约数的值,并重头开始遍历;如果当前权重值curWeight <= 0
则重置该值为max(W) - 如果当前遍历的节点的权重大于等于
curWeight
,则调度的就是该节点 - 每次调度的时候重复1、2、3步骤
Java代码实现
class WeightedRobin implements RobinInterface {
private List<ServerNode> arr;
private int cur = -1;
private int cnt;
private int curWeight; //当前权重
private int maxWeight; //所有实例的最大权重
private int gcd; //最大公约数
@Override
public void init(List<ServerNode> list) {
for (ServerNode serverNode : list) {
curWeight = Math.max(curWeight,serverNode.value);
}
this.cnt = list.size();
this.arr = list;
this.maxWeight = curWeight;
this.gcd = findGcd(list);
}
@Override
public ServerNode next() {
int i = cur;
while(true){
i = (i + 1) % cnt;
//已全部遍历完
if(i == 0) {
this.curWeight -= this.gcd;
if(this.curWeight <= 0){
this.curWeight = this.maxWeight;
}
}
if(arr.get(i).value >= curWeight){
this.cur = i;
return arr.get(cur);
}
}
}
}
其中findGcd()
方法是获取所有权重的最大公约数(使用辗转相除法),实现如下:
//找出最大公约数
public int findGcd(List<ServerNode> list){
List<Integer> nums = list.stream().map(x -> x.value).collect(Collectors.toList());
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (Integer num : nums) {
max = Math.max(max,num);
min = Math.min(min,num);
}
//辗转相除法
while(min != 0){
int tmp = max % min;
max = min;
min = tmp;
}
return max;
}
接着发送10个请求测试轮询结果
ArrayList<ServerNode> nodes = new ArrayList<>();
nodes.add(new ServerNode("192.168.0.1","8100",3));
nodes.add(new ServerNode("192.168.0.1","8200",1));
nodes.add(new ServerNode("192.168.0.1","8300",1));
WeightedRobin simpleRobin = new WeightedRobin();
weightedRobin.init(nodes); //初始化
for (int i = 0; i < 10; i++) {
System.out.println("第"+ i +"个请求发送到" + weightedRobin.next() + "节点");
}
结果如下:
分析:由结果可以看出,不同权重的实例轮询的负载也会不同,解决了简单轮询的资源利用问题,但是还是存在一个问题,即不均匀的负载,例如:
如果有服务S={a,b,c}
权重W={10,1,1}
,则使用加权轮询调度时生成的调度序列为{a,a,a,a,a,a,a,a,a,a,b,c}
,那么会有连续10个请求都被负载到同一个节点a,这种连续的请求突然加重a的实例,有可能会造成节点宕机
为了解决这种不均匀的负载,又提出了下面的算法—平滑加权轮询
3.平滑加权轮询
由于加权轮询会生成不均匀的调度序列,而这种不均匀的负载会令权重较高的实例瞬时出现高负载的情况,导致宕机。而平滑加权轮询通过每次调度修改所有实例的有效权重来解决这种不均匀的调度。
**配置权重:**所有实例对应的权重
**有效权重:**实例的有效权重初始化为配置权重的值,会在每次的调度中改变
算法描述:假设有N个实例S={S1,S2,S3,...,Sn}
,对应的配置权重W={W1,W2,W3,...,Wn}
,对应的有效权重RW={RW1,RW2,...,RWn}
每个实例的有效权重的值初始化为对应配置权重的值,定义轮询到当前实例的指针cur,初始化为-1,所有实例的权重和为totalWeight,算法过程描述为:
- 选出当前有效权重最大的实例,将cur指向当前实例,并将当前实例的
有效权重
减去权重和
- 所有实例的有效权重加上对应的配置权重
- 调度cur指向的实例节点
- 重复上述的1、2、3步骤
Java代码实现
class SmoothWeightedRobin implements RobinInterface{
private int cur = -1;
private List<ServerNode> arr; //实例列表
private int[] realWeight; //有效权重
private int totalWeight; //所有实例权重和
@Override
public void init(List<ServerNode> list) {
realWeight = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
realWeight[i] = list.get(i).value;
this.totalWeight += list.get(i).value;
}
this.arr = list;
}
@Override
public ServerNode next() {
int maxRwIndex = getMaxRw();
this.cur = maxRwIndex;
realWeight[maxRwIndex] -= this.totalWeight;
for (int i = 0; i < realWeight.length; i++) {
realWeight[i] += arr.get(i).value;
}
return arr.get(cur);
}
}
其中 getMaxRw()
方法表示获取有效权重最大的实例节点,实现如下:
public int getMaxRw(){
int max = Integer.MIN_VALUE;
int index = -1;
for (int i = 0; i < this.realWeight.length; i++) {
if(i > max){
max = realWeight[i];
index = i;
}
}
return index;
}
接着发送20个请求测试轮询结果
ArrayList<ServerNode> nodes = new ArrayList<>();
nodes.add(new ServerNode("192.168.0.1","8100",6));
nodes.add(new ServerNode("192.168.0.1","8200",2));
nodes.add(new ServerNode("192.168.0.1","8300",2));
SmoothWeightedRobin smoothWeightedRobin = new SmoothWeightedRobin();
smoothWeightedRobin.init(nodes);
for (int i = 0; i < 20; i++) {
System.out.println("第"+ i +"个请求发送到" + smoothWeightedRobin.next() + "节点");
}
这里三个节点的实例权重分别为{6,2,2}
,结果如下:
分析:从结果可以看出,即便有权重较高的节点,此时的负载序列也很均匀,解决了加权轮询负载不均衡的问题
尽管平滑加权轮询解决了负载不均匀的问题,避免了实例突然加重负载导致宕机的情况出现,但是还是无法做到动态调度
只能由一开始配置的权重去进行调度,这也是轮询算法的不足。当需要满足更加灵活的调度时,就需要使用其他的负载均衡算法了
三、总结
轮询算法是负载均衡中最常见也是最简单的算法,其无需记录实例节点的状态,只需要知道有哪些节点和对应的权重值,就能通过固定的逻辑进行调度,因此是一种无状态的调度算法。
也正是因为其是“无状态”的,无法动态感知实例节点的连接数和负载,有可能节点的负载已经很大了,调度还是会将请求转发到该节点上,因此容易造成服务负载过大导致节点宕机。
所以该算法只能满足一些简单的场景,如果需要满足能够动态感知服务状态进行负载的场景,则应该选择其他的负载均衡算法