负载均衡你了解多少
Posted bswc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了负载均衡你了解多少相关的知识,希望对你有一定的参考价值。
负载均衡
Wikipedia:
负载平衡(Load balancing)是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。使用带有负载平衡的多个服务器组件,取代单一的组件,可以通过冗余提高可靠性。负载平衡服务通常是由专用软件和硬件来完成。主要作用是将大量作业合理地分摊到多个操作单元上进行执行,用于解决互联网架构中的高并发和高可用的问题
从负载均衡设备的角度来看,分为硬件负载均衡和软件负载均衡:
硬件负载均衡:比如最常见的F5,Array等,这些是商业的负载均衡器,性能比较好,有非常成熟的团队,可以提供各种解决方案,但价格比较昂贵,所以没有充足的¥一般是不会考虑的。
软件负载均衡:包括我们耳熟能详的nginx,LVS,Tengine(阿里对Nginx进行的改造)等。优点就是成本比较低,但是也需要有比较专业的团队去维护。
下面了解下几种常用的软件负载均衡算法及实现
Tips :手机上看代码不直观的话建议手机横屏看:)
常用负载均衡算法
随机
完全随机
加权随机
轮询
完全轮询
加权轮询
平滑加权轮询
Hash
普通Hash
一致性Hash
最小连接数
算法实现
//接口
public interface LoadBalance<E> {
public E select();
}
//父类初始化列表
publicpublic abstract class AbstractLoadBalance<E> implements LoadBalance {
protected List<E> elements = null;
protected Integer[] weight;
public AbstractLoadBalance(List<E> elements) {
Assert.notEmpty(elements, "elements is null!");
this.elements = elements;
}
public AbstractLoadBalance(List<E> elements, Integer[] weight) {
Assert.notEmpty(elements, "elements is null!");
Assert.notEmpty(weight, "weight is null");
Assert.isTrue(elements.size() == weight.length, "weight not fit");
this.elements = elements;
this.weight = weight;
}
}
随机
将服务器放进数组或者列表当中,通过系统产生随机数,获取一个在数组有效范围内的下标,根据这个随机下标访问对应服务器。由概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到每一台后端服务器。
完全随机
直接根据随机数返回列表索引对应的server
//普通随机
public class RandomLoadBalance<E> extends AbstractLoadBalance<E> {
public RandomLoadBalance(List<E> list) {
super(list);
}
@Override
public E select() {
int size=elements.size();
return size==1? elements.get(0):
elements.get(ThreadLocalRandom.current().nextInt(size));
}
}
//测试
public class Main {
private 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");
public static void main(String[] args) {
LoadBalance<String> loadBanlance = new RandomLoadBalance<String>(list);
for (int i = 0; i < 5; i++) {
System.out.println(loadBanlance.select());
}
}
}
//output
192.168.0.2
192.168.0.1
192.168.0.3
192.168.0.2
192.168.0.1
加权随机
根据权重进行随机,最简单的按照服务器的权重,增大列表中的个数。比如服务器A的权重是7,服务器B的权重是3,那么服务器列表中就添加7个A服务器,添加3个B服务器。这时候进行随机算法的话,就会有加权的效果了。
public class WeightRandomLoadBalancce<E> extends AbstractLoadBalance {
protected List<E> weightElements = new ArrayList<E>();
public WeightRandomLoadBalancce(List<E> elements, Integer[] weight) {
super(elements, weight);
if (1 == elements.size()) return;
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight[i]; j++) {
weightElements.add(elements.get(i));
}
}
}
@Override
public E select() {
int size = elements.size();
return size == 1 ? (E) elements.get(0) :
weightElements.get(ThreadLocalRandom.current().nextInt(size));
}
}
//测试
public static void testWeightRandomLoad(){
List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2");
Integer[] weight = {5, 2};
LoadBalance<String> loadBanlance = new WeightRandomLoadBalancce<>(list, weight);
for (int i = 0; i < 10; i++) {
System.out.println(loadBanlance.select());
}
}
//output
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.2
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
很明显上面的这个算法在权重值很大的时候,权重服务器列表就会过大。还有一种方式是将所有权重值进行相加,然后根据这个总权重值为随机数上界,进行随机抽取服务器。(原理:最大公约数派上用场了)
最大公约数算法将计算所有服务器的权重值的最大公约数,然后将每台服务器的权重值和它们的最大公约数的比值相加得到数字
x
,然后继续使用公式算出一个值:i = n % x
,最后将i
映射到对应的区间即为当前的server
比如A服务器的权重是2,B服务器的权重是3,C服务器的权重是5。总的权重值是10。在10当中取随机数。如果随机数0到2之间的话,选择A服务器,随机数在3到5之间的话,选择B服务器,随机数在5到10之间的话,选择C服务器。
public class WeightRandomLoadBalancce2<E> extends AbstractLoadBalance {
protected int totalWeight = 0;
public WeightRandomLoadBalancce2(List<E> elements, Integer[] weight) {
super(elements, weight);
//求总权重
totalWeight = Arrays.stream(weight).mapToInt(value -> value).sum();
}
@Override
public E select() {
// 获取一个根据总权重生成的随机权重值
int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight);
//随机权重值如果小于0的话,代表落入对应区间
for (int i = 0; i < weight.length; i++) {
randomWeight -= weight[i];
if (randomWeight < 0) {
return (E) elements.get(i);
}
}
return null;
}
}
//测试
public static void testWeightRandomLoad2(){
List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2");
Integer[] weight = {5, 2};
LoadBalance<String> loadBanlance = new WeightRandomLoadBalancce2<>(list, weight);
for (int i = 0; i < 10; i++) {
System.out.println(loadBanlance.select());
}
}
//output
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.1
192.168.0.2
192.168.0.1
192.168.0.1
192.168.0.2
轮询
轮询法,将请求按照顺序轮流的分配到服务器上,不关心服务器的的连接数和负载情况,依次按顺序调用服务器列表中的服务器。
完全轮询
例如服务器列表中有ABC三台服务器,一个自增数字,每次自增完取3的余数,0的话取服务器A,1的话取服务器B,2的话取服务器C即可。
public class RoundRobinLoadBalance<E> extends AbstractLoadBalance<E> {
AtomicInteger index = new AtomicInteger(0);
public RoundRobinLoadBalance(List<E> elements) {
super(elements);
}
@Override
public E select() {
int size = elements.size();
return size == 1 ? elements.get(0) :
elements.get(index.getAndIncrement() % size);
}
}
//测试
public static void testRoundRobinLoad() {
LoadBalance<String> loadBanlance = new RoundRobinLoadBalance(list);
for (int i = 0; i < 10; i++) {
System.out.println(loadBanlance.select());
}
}
//output
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4
192.168.0.5
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4
192.168.0.5
加权轮询
与加权随机类似,加权轮询就是在上面轮询的基础上加上权重进行顺序调度。同样的有两种,一种是通过增大列表个数(权重数大的时候有弊病),另一种通过最大公约数求权重总数然后分区进行映射,具体的实现不一一列举。
平滑加权轮询
上面的加权轮询其实是有一定的问题的,虽然都是按权重进行轮询的,但如果某个权重比较高的话有问题的话会一直用这个有问题的节点,比如权重为{A:7,B:2,C:1},输出的前7个全是A,接着是2个B,如果B有问题,这时就2B了(持续输出2个B服务)。有没有办法让这个2B错开一下呢,下面重点看下平滑加权轮询实现,Nginx和Dubbo都是使用的这种方式(好像是Nginx某位大佬首创的算法)。
整个实现非常巧妙,大概思想是每一个Server的原始权重(不变权重),原始权重都是动态可改变的(可变权重),在遍历过程中对每一个Server的权重做累加(加上原始权重),然后选出权重最高的作为selected,选中后再对selected做降权(没有选中的不降权),以此达到平滑。以{A:7,B:2,C:1}作为输入,选择10次,其输出结果为{ A A B A A C A A B A},显然2B的问题没有了(不要杠2A的问题)。
服务器A权重:7
服务器B权重:2
服务器C权重:1
总权重:10
每轮各个服务器都会加上各自的配置权重
轮次 | 获取服务前权重 | 选中服务器 | 获取服务后权重 |
---|---|---|---|
1 | { 7 ,2 ,1 } | 7最大,选A | {7-10=-3,2,1 } |
2 | { 4 ,4 ,2 } | 4最大,选A | {4-10=-6,4,2 } |
3 | { 1 ,6 ,3 } | 6最大,选B | {1,6-10=-4,3 } |
4 | { 8 ,-2 ,4 } | 8最大,选A | {8-10=-2,-2,4 } |
5 | { 5 ,0 ,5 } | 5最大,选A | {5-10=-5,0,5 } |
6 | { 2 ,2 ,6 } | 6最大,选C | {2,2,6-10=-4 } |
7 | { 9 ,4 ,-3 } | 9最大,选A | {9-10=-1,4,-3 } |
8 | { 6 ,6 ,-2 } | 6最大,选A | {6-10=-4,6,-2 } |
9 | { 3 ,8 ,-1 } | 8最大,选B | {3,8-10=-2,-1 } |
10 | { 10 ,0 ,0 } | 10最大,选A | {10-10=0,0,0 } |
其中第1行的最后一列{-3,2,1}由来:A被选中了,所以A-最大权重10,B,C没选中的权重继续保持不变 其中第2行的第2列{4,4,2}由来:第1行的最后的可变权重+不变权重,即:
{ -3,2,1 } + { 7,2,1 } = { 4,4,2 }
后面以此类推,最后结果一个轮询下来刚好是按权重轮询来的,而且是分散的,这算法高。具体实现代码如下
public class WeightRoundRobinLoadBalance<E> extends AbstractLoadBalance<E> {
int totalWeight = 0;
AtomicInteger count = new AtomicInteger(0);
//thread safe
private final ConcurrentMap<E, AtomicInteger> currentWeightMap = new ConcurrentHashMap<>(elements.size() << 2);
public WeightRoundRobinLoadBalance2(List<E> elements, Integer[] weight) {
super(elements, weight);
totalWeight = Arrays.stream(weight).mapToInt(value -> value).sum();
}
@Override
protected E doSelect() {
E selected = elements.get(0);
int maxWeightIndex = 0;
for (int i = 0; i < weight.length; i++) {
int index = i;
//init
AtomicInteger cw = currentWeightMap.computeIfAbsent(elements.get(i), k -> new AtomicInteger(0));
//加权
cw.getAndAdd(weight[i]);
//select the Max weight element(Server)
if (cw.get() > currentWeightMap.get(selected).get()) {
selected = elements.get(i);
maxWeightIndex = i;
}
}
//System.out.print(String.format("index: %s \t current: %s \t select: %s \t ",
//count.incrementAndGet(), currentWeightMap, selected));
//降权
currentWeightMap.get(selected).getAndAdd(-totalWeight);
//System.out.println(String.format("currentAfter: %s", currentWeightMap));
return selected;
}
}
//测试
public static void testWeightRoundRobinLoad() {
List<String> list = Arrays.asList("A", "B", "C");
Integer[] weight = {7, 2, 1};
LoadBalance<String> loadBanlance = new WeightRoundRobinLoadBalance(list, weight);
for (int i = 0; i < 10; i++) {
//new Thread(() -> System.out.println("thread:\t" + Thread.currentThread().getName() + "\t" + loadBanlance.select())).start();
System.out.println(loadBanlance.select());
}
}
//output,可以看到与上面的输出一致(可打开System.out注释输出详细的运行过程)
A
A
B
A
A
C
A
A
B
A
//多线程版本输出(打开测试里面的线程输出注释)
thread: Thread-8 A
thread: Thread-2 A
thread: Thread-1 B
thread: Thread-4 A
thread: Thread-9 A
thread: Thread-7 A
thread: Thread-3 A
thread: Thread-6 B
thread: Thread-0 A
thread: Thread-5 C
Hash
可以根据server ip或者URL等生成一个哈希值,然后对应到某个服务上面,如果是根据用户ID哈希,可以解决session共享的问题。
普通Hash
普通hash,最简单的直接:
hash(server) % N
,N为server数量。这种方案存在一个问题就是如果N增加或者减少,将会导致所有节点重新分配,这样Hash的效果将特别差。下面的一致性hash在很大程度上能解决这个问题。
//普通 Hash 简单上个菜
public class HashLoadBalance<E> extends AbstractLoadBalance<E> {
public HashLoadBalance(List<E> elements) {
super(elements);
}
public E doSelect(String ip) {
return elements.get(hash(ip) % elements.size());
}
public static void main(String[] args) {
List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2", "192.168.0.3");
HashLoadBalance<String> loadBalance = new HashLoadBalance<>(list);
for (int i = 0; i < 10; i++) {
System.out.println(loadBalance.doSelect("10.58.0." + i));
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : ((h = key.hashCode()) ^ (h >>> 16)) & 0x7FFF;
}
}
//output
192.168.0.2
192.168.0.1
192.168.0.1
192.168.0.3
192.168.0.2
一致性Hash
一致性Hash 是将所有列表中的服务串成一个环形,每次将
hash(server)
的结果在环上顺时针找一个与其相邻的Server节点做映射。这样在有节点变动的时候,只会影响到与该节点相关的映射,其他节点不受影响。具体的原理不表了,网上很多图,下面给出一种loadBalance
实现(talk is cheep, show u the code)
public class ConsistentHashLoadBalance<E> extends HashLoadBalance<E> {
private final TreeMap<Integer, E> selectors = new TreeMap();
public ConsistentHashLoadBalance(List<E> elements) {
super(elements);
elements.forEach(item -> {
selectors.put(hash(item.hashCode()), item);
});
}
public E doSelect(String ip) {
SortedMap<Integer, E> subMap = selectors.tailMap(hash(ip.hashCode()));
int key = subMap.size() > 0 ? subMap.firstKey() : selectors.firstKey();
return selectors.get(key);
}
public static void main(String[] args) {
List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2", "192.168.0.3");
ConsistentHashLoadBalance<String> loadBalance = new ConsistentHashLoadBalance<>(list);
for (int i = 0; i < 10; i++) {
System.out.println(loadBalance.doSelect("10.58.0." + i));
}
}
}
//ouput
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.2
这就完了?这么简单?是的,这就完了,最简单的实现。不过在实际应用中确实有些问题,看输出结果就知道了,如果列表分散不够均匀的话,会造成有的sever负载很大,有的很小,所以这里需要改造,引进虚拟节点(也可以按权重分配)。按权重进行分配可以使节点产生多个虚拟节点,根据虚拟节点的数量可以达到按权重的调用效果(类似上面的通过增加节点数量的方式实现加权轮询)。
比如{A:7, B:2, C:1} 这样在环上产生7个A节点,2个B节点,1个C节点。
public class ConsistentHashLoadBalance<E> extends HashLoadBalance<E> {
private final TreeMap<Integer, E> selectors = new TreeMap();
//虚拟节点数
private int virtualNode = 1 << 10;
//hash,这里是随便写的一个质数,严谨的话可以写一个hash扰动函数,主要起分散节点作用
private static int hash = 1111111;
public ConsistentHashLoadBalance(List<E> elements) {
super(elements);
elements.forEach(item -> {
for (int i = 0; i < virtualNode; i++) {
selectors.put(hash(item.hashCode() + i * hash), item);
}
});
}
public E doSelect(String ip) {
SortedMap<Integer, E> subMap = selectors.tailMap(hash(ip.hashCode()));
int key = subMap.size() > 0 ? subMap.firstKey() : selectors.firstKey();
return selectors.get(key);
}
public static void main(String[] args) {
System.out.println();
List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2", "192.168.0.3");
ConsistentHashLoadBalance<String> loadBalance = new ConsistentHashLoadBalance<>(list);
for (int i = 0; i < 10; i++) {
System.out.println(loadBalance.doSelect("10.58." + i * hash));
}
}
}
//output
192.168.0.2
192.168.0.2
192.168.0.1
192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.3
192.168.0.1
192.168.0.2
192.168.0.1
最小连接数
最小连接数是根据服务器当前的连接情况进行负载均衡的,在请求后会进行连接数的统计,选取当前连接数最少的一台服务器来处理请求。
如果A服务器有100个请求,B服务器有5个请求,而C服务器只有3个请求,那么毫无疑问会选择C服务器,原理比较简单,算法就不过多的叙述了。
尾声
本文列举了常用的几种负载算法,在实际应用中可以根据不同的场景选择,当然也可以混合使用,如先根据最小活动连接数进行,当连接数接近的时候可以选用随机等等。
reference:
https://zh.wikipedia.org/wiki/负载均衡
今 日 话 题
也欢迎你在留言区跟我们分享你的看法
推荐阅读
以上是关于负载均衡你了解多少的主要内容,如果未能解决你的问题,请参考以下文章