一、dubbo负载均衡
1、负载均衡设置
1)服务端服务级别
<dubbo:service interface="..." loadbalance="roundrobin" />
2)客户端服务级别
<dubbo:reference interface="..." loadbalance="roundrobin" />
3)服务端方法级别
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
4)客户端方法级别
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
2、RandomLoadBalance
/**
* 假定有3台dubbo provider:
*
*
* 10.0.0.1:20884, weight=二
* 10.0.0.1:20886, weight=3
* 10.0.0.1:20888, weight=4
* 随机算法的实现:
* totalWeight=9;
*
* 假设offset=1(即random.nextInt(9)=1)
* 1-2=-1<0?是,所以选中 10.0.0.1:20884, weight=2
* 假设offset=4(即random.nextInt(9)=4)
* 4-2=2<0?否,这时候offset=2,2-3<0?是,所以选中 10.0.0.1:20886, weight=3
* 假设offset=7(即random.nextInt(9)=7)
* 7-2=5<0?否,这时候offset=5, 5-3=2<0?否,这时候offset=2,
* 2-4<0?是,所以选中 10.0.0.1:20888, weight=4
*/
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); //计算invoker总数量
int totalWeight = 0; // 计算总权重值
boolean sameWeight = true; // 判断是否每个invoker的权重都相同,如果相同,就随机选择了
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
//计算总权重 totalWeight,
totalWeight += weight; // Sum
// 检测当前服务提供者的权重与上一个服务提供者的权重是否相同,
// 不相同的话,则将 sameWeight 置为 false。
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
// 权重不相等,随机后,判断在哪个 Invoker 的权重区间中
if (totalWeight > 0 && !sameWeight) {
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
// 随机获取一个 [0, totalWeight) 区间内的数字
int offset = random.nextInt(totalWeight);
// Return a invoker based on the random value.
// 循环让 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 -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
// 返回相应的 Invoker
return invokers.get(i);
}
}
}
// If all invokers have the same weight value or totalWeight=0, return evenly.
// 权重相等,平均随机
return invokers.get(random.nextInt(length));
}
3、RoundRobinLoadBalance
RoundRobinLoadBalance是加权轮训负载均衡,根据加权轮训
/**
* 假设我们有三台服务器 servers = [A, B, C],对应的权重为 weights = [2, 5, 1]。
* 接下来对上面的逻辑进行简单的模拟。
*
* mod = 0:满足条件,此时直接返回服务器 A
*
* mod = 1:需要进行一次递减操作才能满足条件,此时返回服务器 B
*
* mod = 2:需要进行两次递减操作才能满足条件,此时返回服务器 C
*
* mod = 3:需要进行三次递减操作才能满足条件,经过递减后,服务器权重为 [1, 4, 0],此时返回服务器 A
*
* mod = 4:需要进行四次递减操作才能满足条件,经过递减后,服务器权重为 [0, 4, 0],此时返回服务器 B
*
* mod = 5:需要进行五次递减操作才能满足条件,经过递减后,服务器权重为 [0, 3, 0],此时返回服务器 B
*
* mod = 6:需要进行六次递减操作才能满足条件,经过递减后,服务器权重为 [0, 2, 0],此时返回服务器 B
*
* mod = 7:需要进行七次递减操作才能满足条件,经过递减后,服务器权重为 [0, 1, 0],此时返回服务器 B
*
* 经过8次调用后,我们得到的负载均衡结果为 [A, B, C, A, B, B, B, B],
* 次数比 A:B:C = 2:5:1,等于权重比。当 sequence = 8 时,mod = 0,此时重头再来。
* 从上面的模拟过程可以看出,当 mod >= 3 后,服务器 C 就不会被选中了,因为它的权重被减为0了。
* 当 mod >= 4 后,服务器 A 的权重被减为0,此后 A 就不会再被选中。
*/
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// key = 全限定类名 + "." + 方法名,比如 com.xxx.DemoService.sayHello
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int length = invokers.size(); // Number of invokers
int maxWeight = 0; // The maximum weight 最大权重
int minWeight = Integer.MAX_VALUE; // The minimum weight 最小权重
final LinkedHashMap<Invoker<T>, IntegerWrapper> invokerToWeightMap = new LinkedHashMap<Invoker<T>, IntegerWrapper>();
int weightSum = 0; // 权重总和
// 查找最大和最小权重,计算权重总和
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// 获取最大和最小权重
maxWeight = Math.max(maxWeight, weight); // Choose the maximum weight
minWeight = Math.min(minWeight, weight); // Choose the minimum weight
if (weight > 0) {
// 将 weight 封装到 IntegerWrapper 中
invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight));
// 累加权重
weightSum += weight;
}
}
// 查找 key 对应的对应 AtomicPositiveInteger 实例,为空则创建。
// AtomicPositiveInteger 用于记录服务的调用编号即可
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
// 获得当前调用编号,并递增 + 1,sequence的值是从0开始,而且 getAndIncrement是先得到0, 再增加
int currentSequence = sequence.getAndIncrement();
// 如果最小权重小于最大权重,表明服务提供者之间的权重是不相等的
if (maxWeight > 0 && minWeight < maxWeight) {
// 使用调用编号对权重总和进行取余操作
int mod = currentSequence % weightSum;
for (int i = 0; i < maxWeight; i++) {
// 遍历 invokerToWeightMap
for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
//获取invoker
final Invoker<T> k = each.getKey();
// 获取权重包装类 IntegerWrapper
final IntegerWrapper v = each.getValue();
// 如果 mod = 0,且权重大于0,此时返回相应的 Invoker
if (mod == 0 && v.getValue() > 0) {
return k;
}
// mod != 0,且权重大于0,此时对权重和 mod 分别进行自减操作
if (v.getValue() > 0) {
v.decrement();
mod--;
}
}
}
}
// Round robin
// 服务提供者之间的权重相等,此时通过轮询选择 Invoker
return invokers.get(currentSequence % length);
}
4、LeastActiveLoadBalance
LeastActiveLoadBalance 最小连接数负载均衡
/**
* 最小活跃数算法实现:
* 假定有3台dubbo provider:
*
* 10.0.0.1:20884, weight=2,active=2
* 10.0.0.1:20886, weight=3,active=4
* 10.0.0.1:20888, weight=4,active=3
* active=2最小,且只有一个2,所以选择10.0.0.1:20884
*
* 假定有3台dubbo provider:
*
* 10.0.0.1:20884, weight=2,active=2
* 10.0.0.1:20886, weight=3,active=2
* 10.0.0.1:20888, weight=4,active=3
* active=2最小,且有2个,所以从[10.0.0.1:20884,10.0.0.1:20886 ]中选择;
* 接下来的算法与随机算法类似:
*
* 假设offset=1(即random.nextInt(5)=1)
* 1-2=-1<0?是,所以选中 10.0.0.1:20884, weight=2
* 假设offset=4(即random.nextInt(5)=4)
* 4-2=2<0?否,这时候offset=2, 2-3<0?是,所以选中 10.0.0.1:20886, weight=3
*/
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 计算可用invoker总数量
// 最小的活跃数
int leastActive = -1; // 所有可用服务中,最小的活跃数
// 相同最小活跃数的服务提供者个数
int leastCount = 0;
// 记录有相同最小活跃数的 Invoker 在 invokers 列表中的下标信息
int[] leastIndexs = new int[length];
//计算总权重
int totalWeight = 0;
// 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,
// 以检测是否所有具有相同最小活跃数的 Invoker 的权重均相等
int firstWeight = 0; // Initial value, used for comparision
boolean sameWeight = true; // Every invoker has the same weight value?
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取 Invoker 对应的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取权重
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
// 发现更小的活跃数,重新开始
if (leastActive == -1 || active < leastActive) {
// 使用当前活跃数 active 更新最小活跃数 leastActive
leastActive = active;
// 更新 leastCount 为 1
leastCount = 1;
// 记录当前下标值到 leastIndexs 中
leastIndexs[0] = i; // Reset
totalWeight = weight; // Reset
firstWeight = weight; // Record the weight the first invoker
sameWeight = true; // Reset, every invoker has the same weight value?
// 当前 Invoker 的活跃数 active 与最小活跃数 leastActive 相同
} else if (active == leastActive) { // If current invoker's active value equals with leaseActive, then accumulating.
// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标
leastIndexs[leastCount++] = i; // Record index number of this invoker
// 累加权重
totalWeight += weight; // Add this invoker's weight to totalWeight.
// If every invoker has the same weight?
// 检测当前 Invoker 的权重与 firstWeight 是否相等,
// 不相等则将 sameWeight 置为 false
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false;
}
}
}
// assert(leastCount > 0) 当只有一个 Invoker 具有最小活跃数,此时直接返回该 Invoker 即可
if (leastCount == 1) {
// If we got exactly one invoker having the least active value, return this invoker directly.
return invokers.get(leastIndexs[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 = random.nextInt(totalWeight);
// Return a invoker based on the random value.
// 循环让随机数减去具有最小活跃数的 Invoker 的权重值,
// 当 offset 小于等于0时,返回相应的 Invoker
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
// 有bug,假设有一组 Invoker 的权重为 5、2、1,假设 offsetWeight = 7,当 for 循环进行第二次遍历后 offsetWeight = 7 - 5 - 2 = 0,提前返回了。
// 在2.6.5被修复了: int offsetWeight = random.nextInt(totalWeight) + 1;
// 获取权重值,并让随机数减去权重值 - ⭐️
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(leastIndexs[random.nextInt(leastCount)]);
}
5、ConsistentHashLoadBalance
一致性hash负载均衡,虚拟出来160个虚拟节点,然后进行hash值落地
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 基于 invokers 集合,根据对象内存地址来计算定义哈希值
int identityHashCode = System.identityHashCode(invokers);
// 获得 ConsistentHashSelector 对象
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,
// 可能新增也可能减少了,此时 selector.identityHashCode != identityHashCode 条件成立
if (selector == null || selector.identityHashCode != identityHashCode) {
// 创建新的 ConsistentHashSelector
selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
/**
* 使用 TreeMap 存储 Invoker 虚拟节点 虚拟节点与 Invoker 的映射关系
*/
private final TreeMap<Long, Invoker<T>> virtualInvokers;
/**
* 每个Invoker 对应的虚拟节点数
*/
private final int replicaNumber;
private final int identityHashCode;
private final int[] argumentIndex;//存放需要做hash的参数在方法中的是属于第几个参数
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
//每个 Invoker 对应的虚拟节点数,默认为 160
// <dubbo:parameter key="hash.nodes" value="320" />
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算
// <dubbo:parameter key="hash.arguments" value="0,1" />
String[] index = Constants.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]);
}
// 初始化 virtualInvokers
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
// 每四个虚拟结点为一组
for (int i = 0; i < replicaNumber / 4; i++) {
// Md5是一个16字节长度的数组,将16字节的数组每四个字节一组,
// 分别对应一个虚拟结点
// 对 address + i 进行 md5 运算,得到一个长度为16的字节数组
byte[] digest = md5(address + i);
for (int h = 0; h < 4; h++) {
// 对于每四个字节,组成一个long值,做为这个虚拟节点的在环中的惟一key
// 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);
}
}
}
}
//首先是对参数进行 md5 以及 hash 运算,得到一个 hash 值。然后再拿这个值到 TreeMap 中查找目标 Invoker 即可
public Invoker<T> 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<T> selectForKey(long hash) {
// 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
// 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,
// 需要将 TreeMap 的头节点赋值给 entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
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;
try {
bytes = value.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.update(bytes);
return md5.digest();
}
}