负载均衡
在发起RPC远程调用的时候,也就是我们前面经常提的 InvokerInvocationHandler 。在这里面还会进行负载均衡。负载出Invoke节点进行调用。
负载均衡算法
- **Random LoadBalance:**随机,按权重设置随机概率。在一个节点上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者的权重。
- RoundRobin LoadBalance: 轮询,按公约后的权重设置轮询比例。存在慢的提供者累积请求的问题, 比如:第二台机器很慢,但没“挂”,当请求调到第二台时就卡在那里久而久之,所有请求都卡在调到第二台上。
- LeastActive LoadBalance: 最少活跃调用数,如果活跃数相同则随机调用,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
- **ConsistentHash LoadBalance:**一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者“挂”时,原本发往该提供者的请求,基于虚拟节点,会平摊到其他提 供者,不会引起剧烈变动。默认只对第一个参数“Hash”,如果要修改, 则配置 <dubbo:parameter key="hash.arguments" value="0,1"/>o默认使用160份虚拟节点,如果要修改,则配置〈dubbo:parameterkey="hash.nodes" value="320" />
protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) {
// 这里就是加载 第一张图中的 负载均衡的SPI接口的扩展类
if (CollectionUtils.isNotEmpty(invokers)) {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE));
} else {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
}
}
AbstractClusterInvoker.java
/**
* Select a invoker using loadbalance policy.</br>
* a) Firstly, select an invoker using loadbalance. If this invoker is in previously selected list, or,
* if this invoker is unavailable, then continue step b (reselect), otherwise return the first selected invoker</br>
* <p>
* b) Reselection, the validation rule for reselection: selected > available. This rule guarantees that
* the selected invoker has the minimum chance to be one in the previously selected list, and also
* guarantees this invoker is available.
*
* @param loadbalance 负载均衡算法类型
* @param invocation 调用信息封装
* @param invokers 参与负载均衡
* @param selected 排除的,不参与负载均衡
* @return the invoker which will final to do invoke.
* @throws RpcException exception
*/
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
// 为空直接返回
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 从调用信息中拿到调用方法名称
String methodName = invocation == null ? StringUtils.EMPTY : invocation.getMethodName();
// 是否开启粘滞连接
// 这个我们之前在Dubbo高级应用提到过,就是通过 <dubbo:protocol name="dubbo" sticky="true" /> sticky属性配置
// 所谓 sticky连接就是
// 粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者“挂了”,再连接另一台。粘滞连接将自动开启延迟连接,以减少长连接数。
boolean sticky = invokers.get(0).getUrl()
.getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);
// stickyInvoker缓存了之前使用过的粘滞连接的Invoker
// 如果缓存不是空, 而 该缓存又没包含在最新invokers列表中, 则说明该粘滞连接的Invoker 处理宕机状态
// 则清空缓存
if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
stickyInvoker = null;
}
// 当前开启了URL粘滞连接 并且 当前已经存在之前调用过的粘滞连接Invoker
// 不参与负载均衡的invoker为空 或者 当前不参与负载均衡的invoker列表中不包含 粘滞连接的缓存
if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
// 开启了检测是否可以, 那就去检测一下invoker是否可以
// 如果可用,将不再做负载。直接返回粘滞连接的invoke
if (availablecheck && stickyInvoker.isAvailable()) {
return stickyInvoker;
}
}
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
// 如果开启了粘滞连接, 则缓存
if (sticky) {
stickyInvoker = invoker;
}
return invoker;
}
private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
// 为空直接返回
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 如果只有一个就没有做负载的必要了,直接返回这一个
if (invokers.size() == 1) {
return invokers.get(0);
}
Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
// 选择出的这个invoker包含在有问题invoker集合selected中,或这个invoker直接是不可用的
if ((selected != null && selected.contains(invoker))
|| (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
try {
// 重新负载均衡选择
Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
if (rInvoker != null) {
// 选择出了
invoker = rInvoker;
} else {
// 重新选择的结果为null
int index = invokers.indexOf(invoker);
try {
// 拿到第一次负载出invoker的索取, 直接返回
// 事情到了这一步也没办法了, 因为实在选不出invoker了。 管你这个invoker可不可用,直接返回给你就行了
invoker = invokers.get((index + 1) % invokers.size());
} catch (Exception e) {
logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
}
}
} catch (Throwable t) {
logger.error("cluster reselect fail reason is :" + t.getMessage() + " if can not solve, you can set cluster.availablecheck=false in url", t);
}
}
return invoker;
}
/**
* 重新选择invoker
* @param loadbalance 负载策略
* @param invocation 包含调用的消息
* @param invokers 对该列表中的invoker负载均衡
* @param selected 被排除的,不参与负载均衡的invoker
* @param availablecheck true:表示检测invoker是否可以 false:不检测
* @return the reselect result to do invoke
* @throws RpcException exception
*/
private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException {
// 刚才选择出来的有问题,把数量减掉一个
List<Invoker<T>> reselectInvokers = new ArrayList<>(
invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());
// 循环invoker节点
for (Invoker<T> invoker : invokers) {
// 如果开启节点检测 而 检测的结果是不可用,则进行下一次
if (availablecheck && !invoker.isAvailable()) {
continue;
}
// 代码到这里表示当次循环的invkers可用
// 如果它不是排除的invkers
if (selected == null || !selected.contains(invoker)) {
// 则添加到重新选择出可用invker列表中
reselectInvokers.add(invoker);
}
}
// 重新选择出了可用的, 对这个列表进行负载均衡
if (!reselectInvokers.isEmpty()) {
// 返回负载出的invoker
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
// 到这里,说明最终从可用的invoker中没有选择出可用的
// 那就试一下从排除的invoker列表中再选一次
// 刚才selected中的invoker不可用, 说不定现在就可用了。 所以这里还是试一试
if (selected != null) {
for (Invoker<T> invoker : selected) {
// 重选出的invoker列表中不包含该invoker。并且 现在这个invoker是可用的
if ((invoker.isAvailable()) && !reselectInvokers.contains(invoker)) {
// 添加到重选的invoker列表
reselectInvokers.add(invoker);
}
}
}
// 如果重选的这个列表有元素, 则进行负载并返回
if (!reselectInvokers.isEmpty()) {
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
// 最终
// 上面的业务都走过了还是没有选出invoker, 则返回null
return null;
}
AbstractLoadBalance.java
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) {
return invokers.get(0);
}
// 真正对invokers进行负载均衡算法
return doSelect(invokers, url, invocation);
}
随机算法
使用方式
<dubbo:reference id="gcService" loadbalance="random" check="false" interface="org.apache.dubbo.gc.GcService"/>
使用场景
用于服务级别,其适用于,提供者主机性能差别较大的场景。若所有提供者的性能差别不大,则会使该算法变为了纯粹的随机算法。而纯粹的随机算法则可能会出现“负载堆积”问题,即可能存在生成的某些随机数机率较高从而导致某些提供者的负载较大。
RandomLoadBalance.java
/**
* 负载均衡随机算法
*/
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// Number of invokers
int length = invokers.size();
// 用于标识所有invoker的权重是否相同
boolean sameWeight = true;
// 按照invoker的数量生成一个权重的数组
int[] weights = new int[length];
// 先对第一个invoker进行权重, 作为标本
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// 权重值总和
int totalWeight = firstWeight;
// 对整个invoker列表进行权重,除第一个外
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// 将权重完的值写到对应invoker权重数组中
weights[i] = weight;
// 累加
totalWeight += weight;
// 看当前Invoker是否和第一个权重相同
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 如果每个invoker的权重都不一样
if (totalWeight > 0 && !sameWeight) {
// 线程安全的随机数生成器
// 从权重总和中生成一个随机数
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 遍历参与权重的invoker列表
for (int i = 0; i < length; i++) {
// 生成的随机数 - 当前循环的invoker(主机) = 随机数
// 如果随机数 小于 0 那本次的负载均衡选择出的就是这个invoker(主机)
// 如果不是小于0 那就继续下一次循环, 继续减去下一个invoker(主机)的权重, 直到最后得到的数是 小于 0为止
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 这里就是所有的invoker中的权重是一致的, 这个时候就进行真正的随机
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
AbstractLoadBalance.java
/**
* 为当前invoker进行权重
* @param invoker 为当前invoker进行权重
* @param invocation 包含调用信息
* @return weight
*/
protected int getWeight(Invoker<?> invoker, Invocation invocation) {
// 从URL中获取当前Invoker设置的权重值
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
// 大于0, 则进行权重计算
if (weight > 0) {
// 获取到服务节点的启动时间, 在服务发现的时候 会写入 dubbo框架的版本 dubbo的协议号 当前的时间戳 线程ID
long timestamp = invoker.getUrl().getParameter(REMOTE_TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// 当前时间戳 减去 服务启动的时间 = 这个服务节点已经启动多久了
// 取到已经预热时间
int uptime = (int) (System.currentTimeMillis() - timestamp);
// 获取设置的预热时间, 如果不设置默认是10分钟
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
if (uptime > 0 && uptime < warmup) {
// 计算预热权重
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
return weight >= 0 ? weight : 0;
}
/**
* Calculate the weight according to the uptime proportion of warmup time
* the new weight will be within 1(inclusive) to weight(inclusive)
*
* @param uptime 已经预热的时候
* @param warmup 用户配置的预热时间
* @param weight 用户配置的权重值
* @return weight which takes warmup into account
*/
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// 这么做的目的是, 服务刚启动的时候需要有个预热的过程,如果一启动就给予100%的流量,则可能会让服务崩溃
// (启动至今时间/给予的预热总时间) X 权重
// 例:
// 假设我们设置A服务的权重是5,让它预热10分钟,则第一分钟的时候,它的权重变为(1/10) X5 = 0.5, 0.5/5 = 0.1,也就是只承担10%的流量;10分钟后
// 权重就变为(10/10) X5 = 5,也就是权重变为设置的100%,承担了所有的流量。
int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}
最小活跃度算法
使用方式
<dubbo:reference id="gcService" loadbalance="leastactive" check="false" interface="org.apache.dubbo.gc.GcService"/>
使用场景
用于服务级别,其适合于提供者主机性能差别不大的场景。根据各个提供者主机当前处理实际情况进行负载均衡。某一台机器处理的请求快,处理完了就又会给这个机器分请求处理。 似于:能力越大 责任越大。
LeastActiveLoadBalance.java
/**
* 最小活跃度
* LeastActiveLoadBalance
* <p>
* Filter the number of invokers with the least number of active calls and count the weights and quantities of these invokers.
* If there is only one invoker, use the invoker directly;
* if there are multiple invokers and the weights are not the same, then random according to the total weight;
* if there are multiple invokers and the same weight, then randomly called.
*/
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 当前主机列表
int length = invokers.size();
// 主机初始活跃度
int leastActive = -1;
// 具有最小活跃度invoker的数量
int leastCount = 0;
// 该数组中存放的是具有最小活跃度的invoker的索引
int[] leastIndexes = new int[length];
// 该数组存放每个invoker的权重
int[] weights = new int[length];
// 具有最小活跃度的invoker的权重之和
int totalWeight = 0;
// The weight of the first least active invoke
int firstWeight = 0;
// 用于标识 具有最小活跃度的所有invoker的权重是否相同
boolean sameWeight = true;
// 遍历所有invoker,过滤出所有具有最小活跃度的invoker
for (int i = 0; i < length; i++) {
// 拿到当前循环的invoker
Invoker<T> invoker = invokers.get(i);
// 拿到当前invoker方法中的活跃度
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取权重
int afterWarmup = getWeight(invoker, invocation);
// 记录下当前invoker的权重到数组
weights[i] = afterWarmup;
// 处理找到更小活跃度后的重置
if (leastActive == -1 || active < leastActive) {
// 更新最小活跃度
leastActive = active;
// 重置计数器
leastCount = 1;
// Put the first least active invoker first in leastIndexes
leastIndexes[0] = i;
// 重置权重和
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的累计 计数器增一
leastIndexes[leastCount++] = i;
// Accumulate the total weight of the least active invoker
totalWeight += afterWarmup;
// If every invoker has the same weight?
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// 若具有最小活跃度的invoker就一个,则直接返回这个即可
if (leastCount == 1) {
// If we got exactly one invoker having the least active value, return this invoker directly.
return invokers.get(leastIndexes[0]);
}
if (!sameWeight && totalWeight > 0) {
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on
// totalWeight.
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// Return a invoker based on the random value.
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.
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
轮询算法
使用方式
<!-- 该算法是针对方法使用 -->
<dubbo:reference id="gcService" check="false" interface="org.apache.dubbo.gc.GcService">
<dubbo:method name="getGCInfo" loadbalance="leastactive"/>
</dubbo:reference>
使用场景
用于方法级别,其适合于某服务中包含多个方法,其中某个方法属于高消耗方法(消耗系统资源或耗时)。 若对于该方法的请求突然再现高并发,此时若采用服务级别的负载均衡,则可能会出现问题: 某时刻某个或某些提供者刚处理完毕一批请求,此时它们的负载较小。而此时恰好对这个高消耗的方法的高并发请求到达。这时可能会一次性将大量的这个高消耗请求分配给这些负载较小的提供者,可能会导致这些提供者直接崩溃。
算法详述
现假设有 A B C D E 五台主机,其初始的主机权重分别是 3 5 7 4 9。 轮询权重初始都是0。总共执行6次调用的轮询选出的主机如下图:
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
// 回收期,默认60秒
// 若某个invoker的轮询权重当前值“一直”没有变化,
// 则说明每次轮询时都没有其对应的这个invoker,
// 说明这个invoker挂了,需要将其从缓存map中清除。
// 这个“一直”就是这里的 回收期
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.set(0); // 将轮询权重当前值归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,
// 外层map的key是全限定性方法名,value是内层map
// 内层map的key为一个invoker的url,其就代表了一个invoker,所以我们就可以将其直接视为invoker
// 内层map的value为轮询权重实例
// 这个内层map与其对应的外层map的key间的关系是,内层map可以提供外层map的key(方法)的服务
private ConcurrentMap<String, ConcurrentMap<String, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap<String, ConcurrentMap<String, WeightedRoundRobin>>();
// 更新锁,其值为false,表示锁是打开的
private AtomicBoolean updateLock = new AtomicBoolean();
/**
* get invoker addr list cached for specified invocation
* <p>
* <b>for unit test only</b>
*
* @param invokers
* @param invocation
* @return
*/
protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
Map<String, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map != null) {
return map.keySet();
}
return null;
}
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取当前RPC调用的方法的全限定性方法名
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 从缓存Map中获取当前调用方法所对应的所有invoker及其对应的轮询权重,即内层map
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
// 若这个内层map为null,说明要么没有这个方法,要么真的是这个map为null
// 此时创建一个内层map放入到缓存Map中
if (map == null) {
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
int totalWeight = 0;
// 记录本轮循环中轮询权重当前值的最大值
long maxCurrent = Long.MIN_VALUE;
// 当前时间
long now = System.currentTimeMillis();
// 记录最终被选择出的invoker及其对应的轮询权重实例
Invoker<T> selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
// 遍历所有invoker
for (Invoker<T> invoker : invokers) {
// 获取当前遍历invoker的url,即内层map的key,格式 dubbo://ip:port/业务接口名
String identifyString = invoker.getUrl().toIdentityString();
// 从缓存中获取当前invoker对应的轮询权重实例
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
// 获取当前invoker的主机权重
int weight = getWeight(invoker, invocation);
// 若当前invoker对应的轮询权重实例为null,则创建一个新的,初始化后再写入到缓存map
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
weightedRoundRobin.setWeight(weight);
map.putIfAbsent(identifyString, weightedRoundRobin);
}
// 什么时候主机权重会发生变化?warmup过程中的主机权重
// 会随着时间的推移,越来越接近weight设置值
if (weight != weightedRoundRobin.getWeight()) {
//weight changed
// 更新当前轮询权重实例的主机权重。注意,此时也会将轮询权重当前值归0
weightedRoundRobin.setWeight(weight);
}
// 为当前invoker的轮询权重当前值增重
long cur = weightedRoundRobin.increaseCurrent();
// 记录本次增重时间戳
weightedRoundRobin.setLastUpdate(now);
// 若当前轮询权重当前值比当前记录的最大current还大,则记录下这个值及invoker
if (cur > maxCurrent) {
// 更新最大当前值
maxCurrent = cur;
selectedInvoker = invoker;
selectedWRR = weightedRoundRobin;
}
// 计算主机权重total
totalWeight += weight;
} // end-for
// 若updateLock为false(即更新锁是打开状态),
// 且当前所有可用的invoker数量与缓存中invoker的数量不相同,则进行如下处理
// 问题:这个“不相同”的情况有几种可能?
// 1)invokers.size() > map.size()
// 2)invokers.size() < map.size()
// 分析:
// 1> 若有新增的invoker,则经过前面的for,已经会为这些新增invoker创建相应的轮询实例,
// 将其其放入到了缓存map中。所以对于这种有新增invoker的情况,代码走到这里,其invokers的
// 数量也是与map的size()相同的。故,第1)种情况不可能出现
// 2> 第2)种情况描述的是什么场景?有invoker出现了宕机的情况
if (!updateLock.get() && invokers.size() != map.size()) {
// 上锁
if (updateLock.compareAndSet(false, true)) {
try {
// copy -> modify -> update reference ==> 目的是为了“迭代稳定性”
// 新建一个map
ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
// 使用老map初始化新map
newMap.putAll(map);
// 迭代新map
Iterator<Entry<String, WeightedRoundRobin>> it = newMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, WeightedRoundRobin> item = it.next();
// item.getValue().getLastUpdate() 获取当前迭代元素的最后增重时间戳
// now - item.getValue().getLastUpdate() 当前迭代元素已经有多久没有更新时间戳了
// if()条件用于判断停更时长是否超过了 回收期
// 若超过了,则说明当前迭代invoker已经挂了,需要从map中删除
if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
it.remove();
}
}
// 将更新过的新map替换掉缓存Map中的老map
methodWeightMap.put(key, newMap);
} finally {
// 解锁
updateLock.set(false);
}
}
}
// 若用于记录轮询权重最大的invoker的变量selectedInvoker不为null,则返回这个invoker
// 但是在返回之前,先让其当前值变为最小
if (selectedInvoker != null) {
// 轮询权重当前值减重变为最小
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// should not happen here
return invokers.get(0);
}
一致性Hash算法
使用方式
<!-- 该算法针对 方法-参数使用 -->
<dubbo:reference id="gcService" check="false" interface="org.apache.dubbo.gc.GcService">
<dubbo:method name="getGCInfo">
<!-- 对该两个方法的参数进行hash -->
<dubbo:parameter key="hash.arguments" value="1,2" />
<!-- 虚拟主机的数量,用于进行hahs计算 -->
<dubbo:parameter key="hash.nodes" value="160" />
</dubbo:method>
</dubbo:reference>
使用场景
用于方法参数级别,其应用场景是,对于某些方法,其参数选择范围较小,即参数值相对较固定。使用该算 法,可以使对相同方法的相同参数请求,被负载均衡到相同的提供者。这样就可以充分地利用提供者的缓存,从而达到高性能。
ConsistentHashLoadBalance.java
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<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
@SuppressWarnings("unchecked")
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取调用的方法名称
String methodName = RpcUtils.getMethodName(invocation);
// 全限定方法名
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 计算出这个列表的hash值, 用于构建selector选择器
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 当前选择器是空的, 或者当前列表算出的hash值和缓存的hash不匹配, 则将当前的hash加入到缓存
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 使用selector根据调用信息进行选择
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
private final TreeMap<Long, Invoker<T>> virtualInvokers;
private final int replicaNumber;
private final int identityHashCode;
private final int[] argumentIndex;
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
// 创建一个map用于存放虚拟invoker
// 其key为虚拟invoker(其本质就是一个hash值)
// 其Value为invoker
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
// 整个invoker列表的hash
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
// 获取hash.nodes属性值,即为每个物理invoker创建的虚拟invoker的数量,默认160
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
// 获取hash.arguments属性值,并使用逗号(,)进行分隔. 该值是用户配置(这个配置值只能是整数数字)
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]);
}
// 开始遍历invoker列表
for (Invoker<T> invoker : invokers) {
// 获取物理invoker的地址 ip:port
String address = invoker.getUrl().getAddress();
for (int i = 0; i < replicaNumber / 4; i++) {
// 使用md5算法生成一个128位的摘要
// 生成一个hash值需要使用一个32位的二进制数,
// 所以一个digest可以生成4个hash,将这个digest分为4段:
// 0-31, 32-63, 64-95, 96-127
byte[] digest = md5(address + i);
// 使用digest的每一段生成一个hash
for (int h = 0; h < 4; h++) {
// 使用一个32位的二进制数生成一个hash,
// 这个hash就代表了一个虚拟invoker
long m = hash(digest, h);
// 将这个hash(虚拟invoker)作为key,
// 物理invoker作为value,写入到map
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
// 将指定的实参值进行字符串拼接
String key = toKey(invocation.getArguments());
// 根据这个key计算出摘要
byte[] digest = md5(key);
// 使用这个digest的前32位(0-31)计算出一个hash,
// 然后使用这个hash做选择
return selectForKey(hash(digest, 0));
}
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
// 遍历argumentIndex的每一个元素,这些元素是RPC调用方法的实参的索引
for (int i : argumentIndex) {
// 若这个遍历的索引值在有效范围内,则将这个实参拼接
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
private Invoker<T> selectForKey(long hash) {
// 选择出一个虚拟invoker的entry
// 选择比当前指定hash值大的最小的虚拟invoker(hash值),若不存在,则返回null
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
// 若这个entry为null,则选择第一个虚拟invoker的entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
// 返回选择的虚拟invoker对应的物理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();
}
}
}