LoadBalance LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载“均摊”到不同的机器上。避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况。通过负载均衡,可以让每台服务器获取到适合自己处理能力的负载。在为高负载服务器分流的同时,还可以避免资源浪费,一举两得。负载均衡可分为软件负载均衡
和硬件负载均衡
Dubbo 需要对服务消费者的调用请求进行分配,避免少数服务提供者负载过大。服务提供者负载过大,会导致部分请求超时。因此将负载均衡到每个服务提供者上
【dubbo的负载均衡】dubbo常用的负载均衡算法
- 基于权重随机算法的 RandomLoadBalance
- 基于最少活跃调用数算法的 LeastActiveLoadBalance
- 基于 hash 一致性的 ConsistentHashLoadBalance
- 基于加权轮询算法的 RoundRobinLoadBalance
Directory
获取所有的Invoker列表,然后经过Router根据路由规则过滤Invoker,最后幸存下的Invoker需要经过负载均衡后,选出最终要调用的InvokerMETA-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.LoadBalance
random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
AbstractLoadBalance
负载均衡的的抽象了类,封装了基本实现
public abstract class AbstractLoadBalance implements LoadBalance {
// 计算服务权重
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// 计算权重 这个比较厉害
// 随着服务运行时间uptime增大,权重计算值ww会慢慢接近配置weight
int ww = (int) ( uptime / ((float) warmup / weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}
@Override
public Invoker select(List> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 如果invokers列表中仅有一个Invoker,直接返回即可,无需进行负载
if (invokers.size() == 1) {
return invokers.get(0);
}
// 调用doSelect方法进行负载均衡,该方法为抽象方法,有子类实现
return doSelect(invokers, url, invocation);
}
protected abstract Invoker doSelect(List> invokers, URL url, Invocation invocation);
// 计算权重
int getWeight(Invoker> invoker, Invocation invocation) {
int weight = 0;
URL url = invoker.getUrl();
// Multiple registry scenario, load balance among multiple registries.
if (url.getServiceInterface().equals("org.apache.dubbo.registry.RegistryService")) {
weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
// 从url中获取权重weight
weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
if (weight > 0) {
// 获取服务提供者启动时间戳
long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// 计算服务提供者运行时长
long uptime = System.currentTimeMillis() - timestamp;
if (uptime < 0) {
return 1;
}
// 获取服务预热时间,默认为10分钟
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
// 如果服务运行时间小于预热时间,则重新计算服务权重,即降权
if (uptime > 0 && uptime < warmup) {
// 重新计算服务权重
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}
}
RandomLoadBalance
算法思想
假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可
public static final String NAME = "random";
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
// Number of invokers
int length = invokers.size();
// Every invoker has the same weight?
boolean sameWeight = true;
// the weight of every invokers
int[] weights = new int[length];
// the first invoker's weight
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// The sum of weights
int totalWeight = firstWeight;
// 计算总权重
// 检查每个服务的权重是否相同
for (int i = 1;
i < length;
i++) {
int weight = getWeight(invokers.get(i), invocation);
weights[i] = weight;
// 累加权重
totalWeight += weight;
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 服务提供者的权重不相等的情况
if (totalWeight > 0 && !sameWeight) {
// 随机获取一个[0,totalWeight)区间的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。
// 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。
// 第一次循环,offset - 5 = 2 > 0,即 offset > 5,
// 表明其不会落在服务器 A 对应的区间上。
// 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8,
// 表明其会落在服务器 B 对应的区间上
for (int i = 0;
i < length;
i++) {
// 让随机值 offset 减去权重值
offset -= weights[i];
if (offset < 0) {
// 返回相应的 Invoker
return invokers.get(i);
}
}
}
// 所有的服务提供者权重相等,直接随机返回一个
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
LeastActiveLoadBalance
最小活跃数负载均衡,活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者最小活跃数负载均衡算法: 初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求LeastActiveLoadBalance 是基于加权最小活跃数算法实现的
public static final String NAME = "leastactive";
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
// Number of invokers
int length = invokers.size();
// 最小的活跃数
int leastActive = -1;
// 具有相同“最小活跃数”的服务者提供者(以下用 Invoker 代称)数量
int leastCount = 0;
// leastIndexs 用于记录具有相同“最小活跃数”的 Invoker 在 invokers 列表中的下标信息
int[] leastIndexes = new int[length];
// the weight of every invokers
int[] weights = new int[length];
// The sum of the warmup weights of all the least active invokers
int totalWeight = 0;
// 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,
// 以检测是否“所有具有相同最小活跃数的 Invoker 的权重”均相等
int firstWeight = 0;
// Every least active invoker has the same weight value?
boolean sameWeight = true;
// Filter out all the least active invokers
for (int i = 0;
i < length;
i++) {
Invoker invoker = invokers.get(i);
// Get the active number of the invoker
// 获取 Invoker 对应的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// Get the weight of the invoker's configuration. The default value is 100.
int afterWarmup = getWeight(invoker, invocation);
// save for later use
weights[i] = afterWarmup;
// If it is the first invoker or the active number of the invoker is less than the current least active number
// 发现更小的活跃数,重新开始
if (leastActive == -1 || active < leastActive) {
// Reset the active number of the current invoker to the least active number
// 使用当前活跃数active更新最小活跃数据leastActive
leastActive = active;
// Reset the number of least active invokers
leastCount = 1;
// Put the first least active invoker first in leastIndexes
// 记录当前下标值到leastIndexs中
leastIndexes[0] = i;
// Reset totalWeight
totalWeight = afterWarmup;
// Record the weight the first least active invoker
firstWeight = afterWarmup;
// Each invoke has the same weight (only one invoker here)
sameWeight = true;
// If current invoker's active value equals with leaseActive, then accumulating.
} else if (active == leastActive) { // 当前Invoker的活跃数active与最小活跃数leastActive相同
// Record the index of the least active invoker in leastIndexes order
// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标
leastIndexes[leastCount++] = i;
// Accumulate the total weight of the least active invoker
// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标
totalWeight += afterWarmup;
// 累加权重
// If every invoker has the same weight?
// 检测当前 Invoker 的权重与 firstWeight 是否相等,
// 不相等则将 sameWeight 置为 false
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// Choose an invoker from all the least active invokers
// 当只有一个 Invoker 具有最小活跃数,此时直接返回该 Invoker 即可
if (leastCount == 1) {
// If we got exactly one invoker having the least active value, return this invoker directly.
return invokers.get(leastIndexes[0]);
}
// 有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同
if (!sameWeight && totalWeight > 0) {
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on
// totalWeight.
// 随机生成一个 [0, totalWeight) 之间的数字
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// Return a invoker based on the random value.
// 循环让随机数减去具有最小活跃数的 Invoker 的权重值,
// 当 offset 小于等于0时,返回相应的 Invoker
for (int i = 0;
i < leastCount;
i++) {
int leastIndex = leastIndexes[i];
// 获取权重值,并让随机数减去权重值 - ??
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
// If all invokers have the same weight value or totalWeight=0, return evenly.
// 如果权重相同或权重为0时,随机返回一个 Invoker
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
ConsistentHashLoadBalance
一致性 hash 算法,哈希环public class ConsistentHashLoadBalance extends AbstractLoadBalance {
public static final String NAME = "consistenthash";
/**
* Hash nodes name
*/
public static final String HASH_NODES = "hash.nodes";
/**
* Hash arguments name
*/
public static final String HASH_ARGUMENTS = "hash.arguments";
// 一致性哈希的选择器
private final ConcurrentMap, ConsistentHashSelector>> selectors = new ConcurrentHashMap, ConsistentHashSelector>>();
@SuppressWarnings("unchecked")
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
String methodName = RpcUtils.getMethodName(invocation);
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 获取 invokers 原始的 hashcode
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(key);
// 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。
// 此时 selector.identityHashCode != identityHashCode 条件成立
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(key, new ConsistentHashSelector(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector) selectors.get(key);
}
// 调用 ConsistentHashSelector 的 select 方法选择 Invoker
return selector.select(invocation);
}private static final class ConsistentHashSelector {
// 使用 TreeMap 存储 Invoker 虚拟节点
private final TreeMap> virtualInvokers;
private final int replicaNumber;
private final int identityHashCode;
private final int[] argumentIndex;
ConsistentHashSelector(List> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap>();
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
// 获取虚拟节点数,默认为160
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
// 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算
String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
argumentIndex = new int[index.length];
for (int i = 0;
i < index.length;
i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
for (Invoker invoker : invokers) {
String address = invoker.getUrl().getAddress();
for (int i = 0;
i < replicaNumber / 4;
i++) {
// 对 address + i 进行 md5 运算,得到一个长度为16的字节数组
byte[] digest = md5(address + i);
// 对 digest 部分字节进行4次 hash 运算,得到四个不同的 long 型正整数
for (int h = 0;
h < 4;
h++) {
// h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算
// h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算
// h = 2, h = 3 时过程同上
long m = hash(digest, h);
// 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中,
// virtualInvokers 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构
virtualInvokers.put(m, invoker);
}
}
}
}public Invoker select(Invocation invocation) {
// 将参数转为 key
String key = toKey(invocation.getArguments());
// 对参数 key 进行 md5 运算
byte[] digest = md5(key);
// 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,
// 寻找合适的 Invoker
return selectForKey(hash(digest, 0));
}
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}private Invoker selectForKey(long hash) {
// 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker
Map.Entry> entry = virtualInvokers.ceilingEntry(hash);
// 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,
// 需要将 TreeMap 的头节点赋值给 entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
// 返回 Invoker
return entry.getValue();
}private long hash(byte[] digest, int number) {
return (((long) (digest[3 + number * 4] & 0xFF) << 24)
| ((long) (digest[2 + number * 4] & 0xFF) << 16)
| ((long) (digest[1 + number * 4] & 0xFF) << 8)
| (digest[number * 4] & 0xFF))
& 0xFFFFFFFFL;
}private byte[] md5(String value) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.reset();
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
md5.update(bytes);
return md5.digest();
}}}
RoundRobinLoadBalance
加权轮询负载均衡轮询是指将请求轮流分配给每台服务器。举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。但现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
private static final int RECYCLE_PERIOD = 60000;
protected static class WeightedRoundRobin {
// 服务提供者权重
private int weight;
// 当前权重
private AtomicLong current = new AtomicLong(0);
// 最后一次更新时间
private long lastUpdate;
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
// 初始情况下,current = 0
current.set(0);
}
public long increaseCurrent() {
return current.addAndGet(weight);
}
public void sel(int total) {
current.addAndGet(-1 * total);
}
public long getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(long lastUpdate) {
this.lastUpdate = lastUpdate;
}
}
// 嵌套 Map 结构,存储的数据结构示例如下:
// {
//"UserService.query": {
//"url1": WeightedRoundRobin@123,
//"url2": WeightedRoundRobin@456,
//},
//"UserService.update": {
//"url1": WeightedRoundRobin@123,
//"url2": WeightedRoundRobin@456,
//}
// }
// 最外层为服务类名 + 方法名,第二层为 url 到 WeightedRoundRobin 的映射关系。
// 这里我们可以将 url 看成是服务提供者的 id
private ConcurrentMap, ConcurrentMap, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap, ConcurrentMap, WeightedRoundRobin>>();
private AtomicBoolean updateLock = new AtomicBoolean();
protected Collection> getInvokerAddrList(List> invokers, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
Map, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map != null) {
return map.keySet();
}
return null;
}@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
// key = 全限定类名 + "." + 方法名,比如 com.xxx.DemoService.sayHello
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
ConcurrentMap, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map == null) {
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
int totalWeight = 0;
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
Invoker selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
// 下面这个循环主要做了这样几件事情:
//1. 遍历 Invoker 列表,检测当前 Invoker 是否有
//相应的 WeightedRoundRobin,没有则创建
//2. 检测 Invoker 权重是否发生了变化,若变化了,
//则更新 WeightedRoundRobin 的 weight 字段
//3. 让 current 字段加上自身权重,等价于 current += weight
//4. 设置 lastUpdate 字段,即 lastUpdate = now
//5. 寻找具有最大 current 的 Invoker,以及 Invoker 对应的 WeightedRoundRobin,
//暂存起来,留作后用
//6. 计算权重总和
for (Invoker invoker : invokers) {
String identifyString = invoker.getUrl().toIdentityString();
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
int weight = getWeight(invoker, invocation);
// 检测当前 Invoker 是否有对应的 WeightedRoundRobin,没有则创建
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
// 设置 Invoker 权重
weightedRoundRobin.setWeight(weight);
// 存储 url 唯一标识 identifyString 到 weightedRoundRobin 的映射关系
map.putIfAbsent(identifyString, weightedRoundRobin);
}
// Invoker 权重不等于 WeightedRoundRobin 中保存的权重,说明权重变化了,此时进行更新
if (weight != weightedRoundRobin.getWeight()) {
//weight changed
weightedRoundRobin.setWeight(weight);
}
// 让 current 加上自身权重,等价于 current += weight
long cur = weightedRoundRobin.increaseCurrent();
// 设置 lastUpdate,表示近期更新过
weightedRoundRobin.setLastUpdate(now);
// 找出最大的 current
if (cur > maxCurrent) {
maxCurrent = cur;
// 将具有最大 current 权重的 Invoker 赋值给 selectedInvoker
selectedInvoker = invoker;
// 将 Invoker 对应的 weightedRoundRobin 赋值给 selectedWRR,留作后用
selectedWRR = weightedRoundRobin;
}
// 计算权重总和
totalWeight += weight;
}
// 对 进行检查,过滤掉长时间未被更新的节点。
// 该节点可能挂了,invokers 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。
// 若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。
if (!updateLock.get() && invokers.size() != map.size()) {
if (updateLock.compareAndSet(false, true)) {
try {
// copy -> modify -> update reference
ConcurrentMap, WeightedRoundRobin> newMap = new ConcurrentHashMap<>(map);
// 遍历修改,即移除过期记录
newMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
// 更新引用
methodWeightMap.put(key, newMap);
} finally {
updateLock.set(false);
}
}
}
if (selectedInvoker != null) {
// 让 current 减去权重总和,等价于 current -= totalWeight
selectedWRR.sel(totalWeight);
// 返回具有最大 current 的 Invoker
return selectedInvoker;
}
// should not happen here
return invokers.get(0);
}}
推荐阅读
- Dubbo使用Hessian2序列化时针对Byte类型出现java.lang.ClassCastException
- Dubbo|【Dubbo | Zookeeper】一篇文章入门Dubbo+Zookeeper
- SpringBoot整合RPC框架Dubbo
- Spring|Spring Cloud Alibaba(四)(Spring Cloud 使用 Sentinel 实现限流)
- Spring|Spring Cloud Alibaba(三)(使用 Nacos config 实现统一配置管理)
- Spring|Spring Cloud Alibaba(二)(注解实现 Dubbo 服务调用失败时的本地伪装)
- java框架|org.apache.curator.CuratorConnectionLossException: KeeperErrorCode = ConnectLoss
- Dubbo之RpcContext原理
- Java|dubbo @EnableAsync @Configuration
- dubbo中的ExtensionLoader详解