Dubbo++:Dubbo 负载均衡策略
负载均衡简介:👇
负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动的的工作负载分布。
负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。
使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件。
简单解释:👇
这个概念如何理解呢?通俗点来说假如一个请求从客户端发起,比如(查询订单列表),要选择服务器进行处理,但是我们的集群环境提供了5个服务器A\B\C\D\E,每个服务器都有处理这个请求的能力,此
时客户端就必须选择一个服务器来进行处理(不存在先选择A,处理一会又选择C,又跳到D).说白了就是一个选择的问题。当请求多了的话,就要考虑各服务器的负载,一共5个服务器,不可能每次都让一个服
务器都来处理吧,比如把让其他服务器来分压。这就是负载均衡的优点:避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。
dubbo 的 loadBalance 接口:👇
loadBalance:
dubbo 的负载均衡策略,主体向外暴露出来是一个接口,名字叫做 loadBlace,位于com.alibaba.dubbo.rpc.cluster包下,很明显根据包名就可以看出它是用来管理集群的:
这个接口就一个方法,select方法的作用就是从众多的调用的List选择出一个调用者,Invoker可以理解为客户端的调用者,dubbo专门封装一个类来表示,URL就是调用者发起的URL请求链接,
从这个URL中可以获取很多请求的具体信息,Invocation表示的是调用的具体过程,dubbo用这个类模拟调用具体细节过程:
package org.apache.dubbo.rpc.cluster; @SPI("random") public interface LoadBalance { @Adaptive({"loadbalance"}) <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException; }
由 @SPI 注解可以看到,dubbo默认的负载均衡策略是随机调用法。
AbstractLoadBlance:
该接口在下面的子类都会对其进行实现。接口下是一个抽象类 AbstractLoadBlance
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import org.apache.dubbo.common.URL; import org.apache.dubbo.common.utils.CollectionUtils; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.cluster.LoadBalance; public abstract class AbstractLoadBalance implements LoadBalance { public AbstractLoadBalance() { } static int calculateWarmupWeight(int uptime, int warmup, int weight) { int ww = (int)((float)uptime / ((float)warmup / (float)weight)); return ww < 1 ? 1 : Math.min(ww, weight); } public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { if (CollectionUtils.isEmpty(invokers)) { return null; } else { return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation); } } protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3); int getWeight(Invoker<?> invoker, Invocation invocation) { int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100); if (weight > 0) { long timestamp = invoker.getUrl().getParameter("timestamp", 0L); if (timestamp > 0L) { long uptime = System.currentTimeMillis() - timestamp; if (uptime < 0L) { return 1; } int warmup = invoker.getUrl().getParameter("warmup", 600000); if (uptime > 0L && uptime < (long)warmup) { weight = calculateWarmupWeight((int)uptime, warmup, weight); } } } return Math.max(weight, 0); } }
AbstractLoadBlance 抽象类继承自 LoadBalance,其中有个static方法表明它在类加载的时候就会运行。
它表示的含义是计算预热加载权重,参数是uptime,这里可以理解为服务启动的时间,warmup就是预热时间,weight是权重的值。
下面会对比进行详细解释:
1、select 方法:
抽象类方法中有个有方法体的方法 select,先判断调用者组成的List是否为null,如果是null就返回null。
再判断调用者的大小,如果只有一个就返回那个唯一的调用者(试想,如果服务调用另一个服务时,当服务的提供者机器只有一个,那么就可以返回那一个,因为没有选择了!) 如果这些都不成立,就继续往下走,走doSelect方法:
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { if (CollectionUtils.isEmpty(invokers)) { return null; } else { return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation); } }
2、doSelect 方法:
该方法是抽象的,交给具体的子类去实现,由此也可以思考出一个问题就是:dubbo为什么要将一个接口首先做出一个实现抽象类,再由不同的子类去实现?原因是抽象类中的非抽象方法,再子类中都是必须要实现的,而他们子类的不同点就是具体做出选择的策略不同,将公共的逻辑提取出来放在抽象类里,子类不用写多余的代码,只用维护和实现最终要的自己的逻辑。
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3);
3、getWeight 方法:
顾名思义,这个方法的含义就是获取权重,首先通过URL(URL为dubbo封装的一个实体)获取基本的权重,如果权重大于0,会获取服务启动时间,再用当前的时间-启动时间就是服务到目前为止运行了多久,因此这个upTime就可以理解为服务启动时间,再获取配置的预热时间,如果启动时间小于预热时间,就会再次调用获取权重。这个预热的方法其实dubbo针对JVM做出的一个很契合的优化,因为JVM从启动到起来都运行到最佳状态是需要一点时间的,这个时间叫做warmup,而dubbo就会对这个时间进行设定,然后等到服务运行时间和warmup相等时再计算权重,这样就可以保障服务的最佳运行状态!
int getWeight(Invoker<?> invoker, Invocation invocation) { int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100); if (weight > 0) { long timestamp = invoker.getUrl().getParameter("timestamp", 0L); if (timestamp > 0L) { long uptime = System.currentTimeMillis() - timestamp; if (uptime < 0L) { return 1; } int warmup = invoker.getUrl().getParameter("warmup", 600000); if (uptime > 0L && uptime < (long)warmup) { weight = calculateWarmupWeight((int)uptime, warmup, weight); } } } return Math.max(weight, 0); }
dubbo 的几种负载均衡策略:👇
整体架构图:
可以看出抽象的负载均衡下的类分为4个,这4个类表示了4种负载均衡策略,分别是一致性Hash均衡算法、随机调用法、轮询法、最少活动调用法。
RandomLoadBalance:随机调用
随机调用负载均衡,该类实现了抽象的AbstractLoadBalance接口,重写了doSelect方法,看方法的细节就是首先遍历每个提供服务的机器,获取每个服务的权重,然后累加权重值,判断每个服务的提供者权重是否相同,如果每个调用者的权重不相同,并且每个权重大于0,那么就会根据权重的总值生成一个随机数,再用这个随机数,根据调用者的数量每次减去调用者的权重,直到计算出当前的服务提供者随机数小于0,就选择那个提供者!另外,如果每个机器的权重的都相同,那么权重就不会参与计算,直接选择随机算法生成的某一个选择,完全随机。
可以看出,随机调用法
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = "random"; public RandomLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); boolean sameWeight = true; int[] weights = new int[length]; int firstWeight = this.getWeight((Invoker)invokers.get(0), invocation); weights[0] = firstWeight; int totalWeight = firstWeight; int offset; int i; for(offset = 1; offset < length; ++offset) { i = this.getWeight((Invoker)invokers.get(offset), invocation); weights[offset] = i; totalWeight += i; if (sameWeight && i != firstWeight) { sameWeight = false; } } if (totalWeight > 0 && !sameWeight) { offset = ThreadLocalRandom.current().nextInt(totalWeight); for(i = 0; i < length; ++i) { offset -= weights[i]; if (offset < 0) { return (Invoker)invokers.get(i); } } } return (Invoker)invokers.get(ThreadLocalRandom.current().nextInt(length)); } }
RoundRobinLoadBlance:轮询调用
轮询调用,轮询调用的过程主要是维护了局部变量的一个LinkdesHashMap(有顺序的Map)去存储调用者和权重值的对应关系,然后遍历每个调用者,把调用者和当前大于0的权重值放进去,再累加权重值。还有一个全局变量的map,找到第一个服务调用者,首先是找到每个服务的key值和method,这里可以理解为标识第一个调用者的唯一key,然后再给它对应的值保证原子性的+1(AtomicPositiveInteger是原子的),再对这个值取模总权重,再每次对其权重值-1,知道它取模与总权重值等于0就选择该调用者,可以称之为"降权取模"(只是一种的计算层面,而不是真正降权)。
总结:轮询调用并不是简单的一个接着一个依次调用,它是根据权重的值进行循环的。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; public class RoundRobinLoadBalance extends AbstractLoadBalance { public static final String NAME = "roundrobin"; private static final int RECYCLE_PERIOD = 60000; private ConcurrentMap<String, ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap(); private AtomicBoolean updateLock = new AtomicBoolean(); public RoundRobinLoadBalance() { } protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) { String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName(); Map<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (Map)this.methodWeightMap.get(key); return map != null ? map.keySet() : null; } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName(); ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (ConcurrentMap)this.methodWeightMap.get(key); if (map == null) { this.methodWeightMap.putIfAbsent(key, new ConcurrentHashMap()); map = (ConcurrentMap)this.methodWeightMap.get(key); } int totalWeight = 0; long maxCurrent = -9223372036854775808L; long now = System.currentTimeMillis(); Invoker<T> selectedInvoker = null; RoundRobinLoadBalance.WeightedRoundRobin selectedWRR = null; int weight; for(Iterator var13 = invokers.iterator(); var13.hasNext(); totalWeight += weight) { Invoker<T> invoker = (Invoker)var13.next(); String identifyString = invoker.getUrl().toIdentityString(); RoundRobinLoadBalance.WeightedRoundRobin weightedRoundRobin = (RoundRobinLoadBalance.WeightedRoundRobin)map.get(identifyString); weight = this.getWeight(invoker, invocation); if (weightedRoundRobin == null) { weightedRoundRobin = new RoundRobinLoadBalance.WeightedRoundRobin(); weightedRoundRobin.setWeight(weight); map.putIfAbsent(identifyString, weightedRoundRobin); } if (weight != weightedRoundRobin.getWeight()) { weightedRoundRobin.setWeight(weight); } long cur = weightedRoundRobin.increaseCurrent(); weightedRoundRobin.setLastUpdate(now); if (cur > maxCurrent) { maxCurrent = cur; selectedInvoker = invoker; selectedWRR = weightedRoundRobin; } } if (!this.updateLock.get() && invokers.size() != map.size() && this.updateLock.compareAndSet(false, true)) { try { ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> newMap = new ConcurrentHashMap(map); newMap.entrySet().removeIf((item) -> { return now - ((RoundRobinLoadBalance.WeightedRoundRobin)item.getValue()).getLastUpdate() > 60000L; }); this.methodWeightMap.put(key, newMap); } finally { this.updateLock.set(false); } } if (selectedInvoker != null) { selectedWRR.sel(totalWeight); return selectedInvoker; } else { return (Invoker)invokers.get(0); } } protected static class WeightedRoundRobin { private int weight; private AtomicLong current = new AtomicLong(0L); private long lastUpdate; protected WeightedRoundRobin() { } public int getWeight() { return this.weight; } public void setWeight(int weight) { this.weight = weight; this.current.set(0L); } public long increaseCurrent() { return this.current.addAndGet((long)this.weight); } public void sel(int total) { this.current.addAndGet((long)(-1 * total)); } public long getLastUpdate() { return this.lastUpdate; } public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; } } }
LeastActiveLoadBlance:最少活跃数调用
最少活跃数调用法:这个方法的主要作用根据服务的提供者的运行状态去选择服务器,主要的思路就是遍历每个调用者,然后获取每个服务器的运行状态,如果当前运行的运行状态小于最小的状态-1,把它保存在leastIndexs中的第一个位置,并且认定所有的调用者权重都相同,然后直接返回那个调用者(这里的逻辑是:找到最少活跃数(在代码层反应就是:active的值))。如果计算出的权重值和最少的权重值相同,那么把它保存在leastIndexs数组里面,累加权重值,如果当前的权重值不等于初始值firstWeight,那么就认定不是所有的调用者的权重不同。然后再遍历lestIndexs,取权重累加值的随机数生成权重偏移量,在累减它,到它小于0的时候返回那个调用者。如果这些都不符合,就从leastIndexs随机选一个index,返回那个调用者!
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcStatus; public class LeastActiveLoadBalance extends AbstractLoadBalance { public static final String NAME = "leastactive"; public LeastActiveLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); int leastActive = -1; int leastCount = 0; int[] leastIndexes = new int[length]; int[] weights = new int[length]; int totalWeight = 0; int firstWeight = 0; boolean sameWeight = true; int offsetWeight; int leastIndex; for(offsetWeight = 0; offsetWeight < length; ++offsetWeight) { Invoker<T> invoker = (Invoker)invokers.get(offsetWeight); leastIndex = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); int afterWarmup = this.getWeight(invoker, invocation); weights[offsetWeight] = afterWarmup; if (leastActive != -1 && leastIndex >= leastActive) { if (leastIndex == leastActive) { leastIndexes[leastCount++] = offsetWeight; totalWeight += afterWarmup; if (sameWeight && offsetWeight > 0 && afterWarmup != firstWeight) { sameWeight = false; } } } else { leastActive = leastIndex; leastCount = 1; leastIndexes[0] = offsetWeight; totalWeight = afterWarmup; firstWeight = afterWarmup; sameWeight = true; } } if (leastCount == 1) { return (Invoker)invokers.get(leastIndexes[0]); } else { if (!sameWeight && totalWeight > 0) { offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight); for(int i = 0; i < leastCount; ++i) { leastIndex = leastIndexes[i]; offsetWeight -= weights[leastIndex]; if (offsetWeight < 0) { return (Invoker)invokers.get(leastIndex); } } } return (Invoker)invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]); } } }
ConsistentHashLoadBalance:一致性Hash算法
一致性Hash算法,doSelect方法进行选择。一致性Hash负载均衡涉及到两个主要的配置参数为hash.arguments与hash.nodes
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.List; import java.util.TreeMap; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.dubbo.common.URL; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.support.RpcUtils; public class ConsistentHashLoadBalance extends AbstractLoadBalance { public static final String NAME = "consistenthash"; public static final String HASH_NODES = "hash.nodes"; public static final String HASH_ARGUMENTS = "hash.arguments"; private final ConcurrentMap<String, ConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = new ConcurrentHashMap(); public ConsistentHashLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String methodName = RpcUtils.getMethodName(invocation); String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + methodName; int identityHashCode = System.identityHashCode(invokers); ConsistentHashLoadBalance.ConsistentHashSelector<T> selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key); if (selector == null || selector.identityHashCode != identityHashCode) { this.selectors.put(key, new ConsistentHashLoadBalance.ConsistentHashSelector(invokers, methodName, identityHashCode)); selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key); } return selector.select(invocation); } private static final class ConsistentHashSelector<T> { private final TreeMap<Long, Invoker<T>> virtualInvokers = new TreeMap(); private final int replicaNumber; private final int identityHashCode; private final int[] argumentIndex; ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) { this.identityHashCode = identityHashCode; URL url = ((Invoker)invokers.get(0)).getUrl(); this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160); String[] index = CommonConstants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0")); this.argumentIndex = new int[index.length]; for(int i = 0; i < index.length; ++i) { this.argumentIndex[i] = Integer.parseInt(index[i]); } Iterator var14 = invokers.iterator(); while(var14.hasNext()) { Invoker<T> invoker = (Invoker)var14.next(); String address = invoker.getUrl().getAddress(); for(int i = 0; i < this.replicaNumber / 4; ++i) { byte[] digest = this.md5(address + i); for(int h = 0; h < 4; ++h) { long m = this.hash(digest, h); this.virtualInvokers.put(m, invoker); } } } } public Invoker<T> select(Invocation invocation) { String key = this.toKey(invocation.getArguments()); byte[] digest = this.md5(key); return this.selectForKey(this.hash(digest, 0)); } private String toKey(Object[] args) { StringBuilder buf = new StringBuilder(); int[] var3 = this.argumentIndex; int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { int i = var3[var5]; if (i >= 0 && i < args.length) { buf.append(args[i]); } } return buf.toString(); } private Invoker<T> selectForKey(long hash) { Entry<Long, Invoker<T>> entry = this.virtualInvokers.ceilingEntry(hash); if (entry == null) { entry = this.virtualInvokers.firstEntry(); } return (Invoker)entry.getValue(); } private long hash(byte[] digest, int number) { return ((long)(digest[3 + number * 4] & 255) << 24 | (long)(digest[2 + number * 4] & 255) << 16 | (long)(digest[1 + number * 4] & 255) << 8 | (long)(digest[number * 4] & 255)) & 4294967295L; } private byte[] md5(String value) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var4) { throw new IllegalStateException(var4.getMessage(), var4); } md5.reset(); byte[] bytes = value.getBytes(StandardCharsets.UTF_8); md5.update(bytes); return md5.digest(); } } }
如何改变dubbo的负载均衡策略?👇
如果是springboot项目,直接注解在@Reference中引用,然后注明loadblance="xx".其中xx为每个实现类中的name的值
/** * loadbalance:轮询策略 * retries:重试次数 */ @Resource @Reference(loadbalance = "roundrobin",retries = 2) private Bank1Service bank1Service;
xml 配置的方式:
<dubbo:service ref="bank1Service" loadbalance="roundrobin"
interface="com.mlq.integration.zk.api.service.Bank1Service"/>
小结:👇
本篇博客讲述了dubbo的负载均衡机制,其中可以看到除了一致性Hash算法,其它都是根据权重进行计算的,在实际的分布式应用中,理解dubbo如何与zookeeper进行通信选择,如何实现负载均衡,如何维护服务的高可用性,理解负载均衡对于微服务的重要意义,将对于我们学习分布式的开发起着推波助澜的作用。
引文:https://www.cnblogs.com/wyq178/p/9822731.html