常见的轮询算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ServerIps {
public static final Map<String, Integer> WEIGHT_LIST = new HashMap<>();

static {
// total weight is 50
WEIGHT_LIST.put("192.168.0.1", 2);
WEIGHT_LIST.put("192.168.0.2", 8);
WEIGHT_LIST.put("192.168.0.3", 3);
WEIGHT_LIST.put("192.168.0.4", 6);
WEIGHT_LIST.put("192.168.0.5", 5);
WEIGHT_LIST.put("192.168.0.6", 5);
WEIGHT_LIST.put("192.168.0.7", 4);
WEIGHT_LIST.put("192.168.0.8", 7);
WEIGHT_LIST.put("192.168.0.9", 2);
WEIGHT_LIST.put("192.168.0.10", 9);
}

}
Random–完全随机算法

缺点:所有服务器的访问概率都是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
public class RandomLoadBalancing {
public static String getServer() {
int randomPos = ThreadLocalRandom.current().nextInt(ServerIps.LIST.size());
return ServerIps.LIST.get(randomPos);
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(getServer());
}
}
}

WeightedRandom–加权随机算法

缺点:权重低的服务器可能很长一段时间都访问不到

  • 复制法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WeightRandomLoadBalancing {

/**
* 主要的思路就是对服务进行一个复制
* 当权重设置过大时,list容易被撑爆
* @return
*/
private static String getServer() {
ArrayList<String> iplist = new ArrayList<>();
ServerIps.WEIGHT_LIST.forEach((k, v) -> {
while (v != 0) {
iplist.add(k);
v--;
}
});
// 随机返回一个数组的值
return iplist.get(ThreadLocalRandom.current().nextInt(iplist.size()));
}

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
  • 累加

    平时,经常会遇到权重随机算法,从不同权重的N个元素中随机选择一个,并使得总体选择结果是按照权重分布的。如广告投放、负载均衡等。如有4个元素A、B、C、D,权重分别为1、2、3、4,随机结果中A:B:C:D的比例要为1:2:3:4。
    总体思路:累加每个元素的权重A(1)-B(3)-C(6)-D(10),则4个元素的的权重管辖区间分别为[0,1)、[1,3)、[3,6)、[6,10)。然后随机出一个[0,10)之间的随机数。落在哪个区间,则该区间之后的元素即为按权重命中的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WeightRandomLoadBalancingV2 {
public static String getServer() {
Map<String, Integer> map = ServerIps.WEIGHT_LIST;
//计算总权值
int totalWeight = map.values().stream().mapToInt(p -> p).sum();

//随机一个随机数
int index = ThreadLocalRandom.current().nextInt(totalWeight);

//遍历 服务 map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
//如果权重大于 索引
if (entry.getValue() >= index) {
// 返回这个服务
return entry.getKey();
}
//否则,索引 = 当前索引 - 当前服务的权重
index = index - entry.getValue();
}
return "";
}
RoundRobin–完全轮询算法

缺点:从头到尾轮询一遍,不能根据服务器性能设置权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RandomRobin {
public static String[] ipList = (String[]) ServerIps.WEIGHT_LIST.keySet().toArray();

static int index;

public static String getServer() {

if (index == ipList.length) {
index = 0;
}
return ipList[index++];
}

}
WeightedRoundRobin-加权轮询算法

优点:可以根据服务器性能设置访问权重

缺点:可能某个服务器权重大,长时间执行,遇到耗时大的请求,压力会很大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ServerIps {

private static int index;
public static String getServer() {
int totalweigth = WEIGHT_LIST.values().stream().mapToInt(v -> v).sum();

int number = (index++) % totalweigth;

for (Map.Entry<String, Integer> ip : WEIGHT_LIST.entrySet()) {
if (ip.getValue() >= number) {
return ip.getKey();
}
number -= ip.getValue();
}
return "";
}

public static void main(String[] args) {
Stream.iterate(0, x -> x + 1).limit(20).forEach(x -> {
System.out.println(getServer());
});
}
}

但是这种算法有⼀个缺点: -台服务器的权重特别⼤的时候,他需要连续的的处理请求,但是实际.上我们想达 到的效果是,对于100次请求,只要有100*8/50=16次就够了,这16次不⼀定要连续的访问,⽐如假设我们有三 台服务器servers= [A, B, C],对应的权重为weights=[5, 1, 1] ,总权重为7,那么上述这个算法的结果是: AAAAABC,那么如果能够是这么⼀个结果呢: AABACAA,把B和C平均插⼊到5个A中间,这样是⽐较均衡的 了。

SmoothWeightedRoundRobin–平滑加权轮询算法

参考文章

优点:根据权重分配服务,同时又保证权重低的服务可以被访问到

缺点:集群环境下,同一个用户访问无法分流到固定一台机器

每个服务器对应两个权重,分别为weight 和currentWeight。其中weight是固定的,currentWeight 会动态 调整,初始值为0。当有新的请求进来时,遍历服务器列表,让它的currentWeight加上⾃身权重。遍历完 成后,找到最⼤的currentWeight,并将其减去权重总和,然后返回相应的服务器即可。 服务器servers= [A, B, C],对应的权重为weights=[5, 1, 1] ,总权重为7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Data
class SmoothWeight {
private int weight;
private int currentWeight;
private String address;

public SmoothWeight(int weight, int currentWeight, String address) {
this.weight = weight;
this.currentWeight = currentWeight;
this.address = address;
}
}
class ServerIps {
public static Map<String, SmoothWeight> map = new HashMap<>();

static {
map.put("10.180.11.126:8888", new SmoothWeight(5, 5, "10.180.11.126:8888"));
map.put("10.180.11.128:8888", new SmoothWeight(2, 2, "10.180.11.128:8888"));
map.put("10.180.11.130:8888", new SmoothWeight(9, 9, "10.180.11.130:8888"));
}

public static String getServer() {

SmoothWeight maxSmoothWeight = null;

int totalWeigth = map.values().stream().mapToInt(SmoothWeight::getWeight).sum();

for (Map.Entry<String, SmoothWeight> entry : map.entrySet()) {

SmoothWeight currentSmoothWeight = entry.getValue();

if (maxSmoothWeight == null || currentSmoothWeight.getCurrentWeight() > maxSmoothWeight.getCurrentWeight()) {
maxSmoothWeight = currentSmoothWeight;
}
}
assert maxSmoothWeight != null;
maxSmoothWeight.setCurrentWeight(maxSmoothWeight.getCurrentWeight() - totalWeigth);
for (Map.Entry<String, SmoothWeight> entry : map.entrySet()) {

SmoothWeight currentSmoothWeight = entry.getValue();

currentSmoothWeight.setCurrentWeight(currentSmoothWeight.getCurrentWeight() + currentSmoothWeight.getWeight());
}

return maxSmoothWeight.getAddress();
}

public static void main(String[] args) {

Stream.iterate(0, x -> x + 1).limit(15).forEach(v -> {
System.out.println(getServer());
});
}

}
ConsistentHash–⼀致性哈希算法

服务器集群接收到⼀次请求调⽤时,可以根据根据请求的信息,⽐如客户端的ip地址,或请求路径与请求参数 等信息进⾏哈希,可以得出⼀个哈希值,特点是对于相同的ip地址,或请求路径和请求参数哈希出来的值是样的,只要能再增加⼀个算法,能够把这个哈希值映射成⼀个服务端ip地址,就可以使相同的请求(相同 的ip 地址,或请求路径和请求参数)落到同⼀服务器上。 因为客户端发起的请求情况是⽆穷⽆尽的(客户端地址不同,请求参数不同等等),所以对于的哈希值也是⽆ 穷⼤的,所以我们不可能把所有的哈希值都进⾏映射到服务端ip上,所以这⾥需要⽤到哈希环。如下图

image-20211029170319777

  • 哈希值如果需要ip1和ip2之间的,则应该选择ip2作为结果;

  • 哈希值如果需要ip2和ip3之间的,则应该选择ip3作为结果;

  • 哈希值如果需要ip3和ip4之间的,则应该选择ip4作为结果;

  • 哈希值如果需要ip4和ip1之间的,则应该选择ip1作为结果;

    上⾯这情况是⽐较均匀情况,如果出现ip4服务器不存在,那就是这样了:

image-20211029170522157

会发现,ip3和ip1 直接的范围是⽐较⼤的,会有更多的请求落在ip1上,这是不“公平的”,解决这个问题需要加 ⼊虚拟节点,⽐如:

image-20211029170632619

其中ip2-1, ip3-1就是虚拟结点,并不能处理节点,⽽是等同于对应的ip2和ip3服务器。 实际上,这只是处理这种不均衡性的⼀种思路,实际上就算哈希环本身是均衡的,你也可以增加更多的虚拟节 点来使这个环更加平滑,⽐如:

image-20211029170703312

这个彩环也是“公平的”,并且只有ip1,2,3,4是实际的服务器ip,其他的都是虚拟ip。 那么我们怎么来实现呢? 对于我们的服务端ip地址,我们肯定知道总共有多少个,需要多少个虚拟节点也有我们⾃⼰控制,虚拟节点越 多则流量越均衡,另外哈希算法也是很关键的,哈希算法越散列流量也将越均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class ServerIps {

public static final List<String> LIST = Arrays.asList(
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5",
"192.168.0.6",
"192.168.0.7",
"192.168.0.8",
"192.168.0.9",
"192.168.0.10");
}

class ConsistentHash {
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
private static final int VIRTUAL_NODES = 160;

static {
for (String ip : ServerIps.LIST) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
int hash = getHash(ip + "VN" + i);
virtualNodes.put(hash, ip);
}
}
}

private static String getServer(String client) {

int hash = getHash(client);
// 等到⼤于该hash值的排好序的Map
SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
// ⼤于该Hash值的第⼀个元素的位置
Integer nodeIndex = subMap.firstKey();
//如果不存在⼤于该hash值的元素,则返回根节点
if (nodeIndex == null) {
nodeIndex = virtualNodes.firstKey();
}

return subMap.get(nodeIndex);
}

private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++) {
hash = (hash ^ str.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
if (hash < 0) {
hash = Math.abs(hash);
}
return hash;
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(getServer("client" + i));
}
}
}
LeastActive–最⼩活跃算法

前⾯⼏种⽅法主要⽬标是使服务端分配到的调⽤次数尽量均衡,但是实际情况是这样吗?调⽤次数相同,服务器的 负载就均衡吗?当然不是,这⾥还要考虑每次调⽤的时间,⽽最⼩活跃数算法则是解决这种问题的。 活跃调⽤数越⼩,表明该服务提供者效率越⾼,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提 供者。在具体实现中,每个服务提供者对应⼀个活跃数。初始情况下,所有服务提供者活跃数均为0。每收到⼀个 请求,活跃数加1,完成请求后则将活跃数减1。在服务运⾏⼀段时间后,性能好的服务提供者处理请求的速度更 快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最⼩活跃数负载均衡 算法的基本思想。除了最⼩活跃数,最⼩活跃数算法在实现上还引⼊了权重值。所以准确的来说,最⼩活跃数算 法是基于加权最⼩活跃数算法实现的。举个例⼦说明⼀下,在⼀个服务提供者集中,有两个性能优异的服务提供 者。某⼀时刻它们的活跃数相同,则会根据它们的权重去分配请求,权重越⼤,获取到新请求的概率就越⼤。如果 两个服务提供者权重相同,此时随机选择-个即可。 实现: 因为活跃数是需要服务器请求处理相关逻辑配合的,⼀次调⽤开始时活跃数+1,结束时活跃数-1,所以这⾥就不对 这部分逻辑进⾏模拟了,直接使⽤⼀个map来进⾏模拟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class ServerIps {
// 服务器当前活跃数
public static final Map<String, Integer> ACTIVITY_LIST = new LinkedHashMap<>();

static {
ACTIVITY_LIST.put("192.168.0.1", 2);
ACTIVITY_LIST.put("192.168.0.2", 0);
ACTIVITY_LIST.put("192.168.0.3", 1);
ACTIVITY_LIST.put("192.168.0.4", 3);
ACTIVITY_LIST.put("192.168.0.5", 0);
ACTIVITY_LIST.put("192.168.0.6", 1);
ACTIVITY_LIST.put("192.168.0.7", 4);
ACTIVITY_LIST.put("192.168.0.8", 2);
ACTIVITY_LIST.put("192.168.0.9", 7);
ACTIVITY_LIST.put("192.168.0.10", 3);
}
}

public class LeastActive {
private static String getServer() {
Optional<Integer> minValue =

ServerIps.ACTIVITY_LIST.values().stream().min(Comparator.naturalOrder());
if (minValue.isPresent()) {
List<String> minActivityIps = new ArrayList<>();
ServerIps.ACTIVITY_LIST.forEach((ip, activity) -> {
if (activity.equals(minValue.get())) {
minActivityIps.add(ip);
}
});
if (minActivityIps.size() > 1) {
Map<String, Integer> weightList = new LinkedHashMap<String, Integer>();
ServerIps.WEIGHT_LIST.forEach((ip, weight) -> {
if (minActivityIps.contains(ip)) {
weightList.put(ip, ServerIps.WEIGHT_LIST.get(ip));
}
});
int totalWeight = 0;
boolean sameWeight = true;
Object[] weights = weightList.values().toArray();
for (int i = 0; i < weights.length; i++) {
Integer weight = (Integer) weights[i];
totalWeight += weight;
if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {
sameWeight = false;
}
}
int randomPos = ThreadLocalRandom.current().nextInt(totalWeight);
if (!sameWeight) {
for (String ip : weightList.keySet()) {
Integer value = weightList.get(ip);
if (randomPos < value) {
return ip;
}
randomPos = randomPos - value;
}
}
return (String) weightList.keySet().toArray()
[ThreadLocalRandom.current().nextInt(weightList.size())];
} else {
return minActivityIps.get(0);
}
} else {
return (String) ServerIps.WEIGHT_LIST.keySet().toArray()
[ThreadLocalRandom.current().nextInt(ServerIps.WEIGHT_LIST.size())];
}
}
public static void main(String[] args) {
for (int i = 0; i < 130; i++) {
System.out.println(getServer());
}
}
}

主流框架负载均衡

算法 特性 备注
RandomLoadBalance 加权随机 默认算法,默认权重相同
RoundRobinLoadBalance 加权轮询 借鉴于 Nginx 的平滑加权轮询算法,默认权重相 同,
LeastActiveLoadBalance 最少活跃优先 + 加权 随机 背后是能者多劳的思想
ShortestResponseLoadBalance 最短响应优先 + 加权 随机 更加关注响应速度
ConsistentHashLoadBalance ⼀致性 Hash 确定的⼊参,确定的提供者,适⽤于有状态请 求

Random

  • 加权随机,按权重设置随机概率。

  • 在⼀个截⾯上碰撞的概率⾼,但调⽤量越⼤分布越均匀,⽽且按概率使⽤权重后也⽐较均匀,有利于动态调 整提供者权重。

  • 缺点:存在慢的提供者累积请求的问题,⽐如:第⼆台机器很慢,但没挂,当请求调到第⼆台时就卡在那, 久⽽久之,所有请求都卡在调到第⼆台上。

RoundRobin

  • 加权轮询,按公约后的权重设置轮询⽐率,循环调⽤节点

  • 缺点:同样存在慢的提供者累积请求的问题。

LeastActive

  • 加权最少活跃调⽤优先,活跃数越低,越优先调⽤,相同活跃数的进⾏加权随机。活跃数指调⽤前后计数差 (针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供 者处理能⼒越强。

  • 使慢的提供者收到更少请求,因为越慢的提供者的调⽤前后计数差会越⼤;相对的,处理能⼒越强的节点, 处理更多的请求。

ShortestResponse

  • 加权最短响应优先,在最近⼀个滑动窗⼝中,响应时间越短,越优先调⽤。相同响应时间的进⾏加权随机。

  • 使得响应时间越快的提供者,处理更多的请求。

  • 缺点:可能会造成流量过于集中于⾼性能节点的问题。

    这⾥的响应时间 = 某个提供者在窗⼝时间内的平均响应时间,窗⼝时间默认是 30s。

ConsistentHash

  • ⼀致性 Hash,相同参数的请求总是发到同⼀提供者。
  • 当某⼀台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  • 算法参⻅:Consistent Hashing | WIKIPEDIA 缺省只对第⼀个参数 Hash,如果要修改,请配置 <dubbo:parameter key=”hash.arguments” value=”0,1” />
  • 缺省⽤ 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=”hash.nodes” value=”320” />

Ribbon 负载均衡器 LoadBalancer 源码解析 https://blog.51cto.com/u_15077536/2608300