- LoadBalance:负载均衡 SPI 接口;
- AbstractLoadBalance:负载均衡
模板基类
;提供了 “获取一个 Invoker(filtered) 的权重” 的方式:
- 获取当前Invoker设置的权重weight和预热时间warmup,并且计算启动至今时间uptime
- 如果uptime
eg. 假设设置的权重是 100, 预热时间 10min,
- 第一分钟的时候:权重变为 (1/10)*100=10, 也就是承担 10/100 = 10% 的流量;
- 第二分钟的时候:权重变为 (2/10)*100=20, 也就是承担 20/100 = 20% 的流量;
- 第十分钟的时候:权重变为 (10/10)*100=100, 也就是承担 100/100 = 100% 的流量;
- 超过十分钟之后(即 uptime>warmup,表示预热期过了,则直接返回 weight=100,不再计算)
- RandomLoadBalance(
默认
):带权重的随机负载均衡器;
- 通过计算每一个 Invoker 的权重来计算总权重 totalWeight,并判断是否所有的 Invoker 都有相同的权重;
- 如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;
- 如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取,算法如下:
假设有 4 个 Invoker,权重分别是1,2,3,4,则总权重是 1+2+3+4=10,说明每个 Invoker 被选中的概率为1/10,2/10,3/10,4/10。先随机生成一个 [0,10) 的值 index,比如 5,从前向后让 index 递减权重,直到差值<0,那么最后那个使差值<0的 Invoker 就是当前选择的 Invoker - RandomLoadBalance 采用该种算法
- 减第一个 Invoker,5-1=4>0,继续减
- 减第二个 Invoker,4-2=2>0,继续减
- 减第三个 Invoker,2-3<0,则获取第三个 Invoker
- RoundRobinLoadBalance:带权重的轮询负载均衡器;
平滑权重轮询算法:
- 每次做负载均衡时,遍历所有的服务端(Invoker)列表。对每个 Invoker,
a) current = current + weight
b) 计算总权重 totalWeight = totalWeight + weight
- 遍历完所有的 Invoker 后,current 最大的节点就是本次要选择的节点。最后,该节点的 current = current - totalWeight
- LeastActiveLoadBalance:带权重的最小活跃数负载均衡器;
- 需要与 ActiveLimitFilter 配合使用,后者用于记录当前客户端对当前 Invoker 的活跃数及其当前调用方法的活跃数。(注意:actives如果设置为0,则不会加载;设置为<0,只会记录活跃数,不会进行并发数限流;设置为>0,则会进行每个客户端的并发限制逻辑)
- 总体步骤(都是针对当前客户端对指定 Invoker 的并发执行数)
- 初始化最小活跃数的 Invoker 列表:leastIndexs[]
遍历所有的 Invoker,1.1. 获取每一个 Invoker 的当前被调用方法的活跃数 active 及其权重;
1.2. 如果遍历到的 Invoker 是第一个遍历的 Invoker 或者有更小的活跃数的 Invoker,所有的计数清空,重新进行初始化;
1.3. 如果遍历到的 Invoker 的活跃数 active 与之前记录的 leastActive 相同,则将当前的 Invoker 记录到 leastIndexs[] 中
判断所有的 Invoker 是否都有相同的权重。
- 如果 leastIndexs[] 中只有一个值,则直接获取对应索引的 Invoker;否则按照 RandomLoadBalance 的逻辑进行选择:如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取。
- ConsistentHashLoadBalance:一致性 Hash 负载均衡器(与权重无关):
- 组装
serviceKey.methodName
=> {group/}interface{:version}.methodName,获取或创建(第一次或者 invokers 发生了变化
)该 serviceKey 对应的的 selector。
为每一个Invoker创建160个虚拟节点,存储到TreeMap
中:
- key 的计算(需要将每个虚拟节点的 key 打散到整个环上:0 ~ 232)
- value:当前遍历的 Invoker
- 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker
a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对 digest[0~3] 进行 hash
c) 从 TreeMap 中获取第一个 >= 该hash 的 Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
d) 返回该 Entry 的 value 值,即 Invoker
一、LoadBalance
@SPI(RandomLoadBalance.NAME) // 默认是 random=RandomLoadBalance
public interface LoadBalance {
/** 从 List 中选择一个 Invoker */
@Adaptive("loadbalance")
Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException;
}
二、AbstractLoadBalance
public abstract class AbstractLoadBalance implements LoadBalance {
@Override
public Invoker select(List> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.isEmpty()) {
return null;
}
// 1. 如果只有一个 Invoker,直接返回
if (invokers.size() == 1) {
return invokers.get(0);
}
// 2. 调用子类进行选择
return doSelect(invokers, url, invocation);
}
/**
* 子类重写的方法:真正选择 Invoker(filtered) 的方法
*/
protected abstract Invoker doSelect(List> invokers, URL url, Invocation invocation);
/**
* 获取一个 Invoker(filtered) 的权重
* 1、获取当前Invoker设置的权重weight和预热时间warmup,并且计算启动至今时间uptime
* 2、如果uptime invoker, Invocation invocation) {
// 1. 获取当前Invoker设置的权重:weight=100(该值配置在provider端)
// 全局:
// 单个服务:
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
if (weight > 0) {
// 2. 获取启动的时间点,该值服务启动时会存储在注册的URL上(timestamp):dubbo://10.213.11.98:20880/com.alibaba.dubbo.demo.DemoService?...×tamp=1565775720703&warmup=10000&weight=10
long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// 3. 计算启动至今时间
int uptime = (int) (System.currentTimeMillis() - timestamp);
// 4. 获取当前Invoker设置的预热时间,默认 warmup=10*60*1000=10min
int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP); //
// 5. 如果没有过完预热时间,则计算预热权重
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
return weight;
}
/**
* 计算预热权重
* 预热公式:uptime/warmup*weight => 启动至今时间/设置的预热总时间*权重
*/
private int calculateWarmupWeight(int uptime, int warmup, int weight) {
/**
* eg. 设置的权重是 100, 预热时间 10min,
* 第一分钟的时候:权重变为 (1/10)*100=10, 也就是承担 10/100 = 10% 的流量;
* 第二分钟的时候:权重变为 (2/10)*100=20, 也就是承担 20/100 = 20% 的流量;
* 第十分钟的时候:权重变为 (10/10)*100=100, 也就是承担 100/100 = 100% 的流量;
* 超过十分钟之后(即 uptime>warmup,表示预热期过了,则直接返回 weight=100,不再计算)
*/
int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}
}
预热和权重的设置
- 配置在 provider 端,注册在 provider 的 URL 上:dubbo://10.213.11.98:20880/com.alibaba.dubbo.demo.DemoService?...&
timestamp
=1565775720703&warmup
=10000&weight
=10- consumer 服务发现时,会拉取该 URL,获取其参数,其中 timestamp 会用来计算启动至今时间 uptime。
全局:
单个服务:
三、RandomLoadBalance
- 随机,按权重设置随机概率。
- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
三种使用姿势
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
private final Random random = new Random();
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Number of invokers
int totalWeight = 0; // The sum of weights
boolean sameWeight = true; // Every invoker has the same weight?
for (int i = 0; i < length; i++) {
// 计算每一个Invoker的权重
int weight = getWeight(invokers.get(i), invocation);
// 计算总权重
totalWeight += weight; // Sum
// 计算所有Invoker的权重是否相同
// 判断方法:每次遍历一个Invoker,都与其前一个Invoker的权重作比较,如果不相等,则设置sameWeight=false,一旦sameWeight=false后,后续的遍历就不必再进行判断了
if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
// 如果总权重>0&&不是所有的Invoker都有相同的权重,则根据权重进行随机获取
// eg. 4个Invoker,权重分别是1,2,3,4,则总权重是1+2+3+4=10,说明每个Invoker被选中的概率为1/10,2/10,3/10,4/10。此时有两种算法可以实现带概率的选择:
// 1. 想象有这样的一个数组 [1,2,2,3,3,3,4,4,4,4], 先随机生成一个[0,10)的值,比如5,该值作为数组的index,此时获取到的是3,即使用第三个Invoker
// 2. 先随机生成一个[0,10)的值,比如5,从前向后让索引递减权重,直到差值<0,那么最后那个使差值<0的Invoker就是当前选择的Invoker,5-1-2-3<0,那么最终获取的就是第三个Invoker(Dubbo 使用了该算法)
if (totalWeight > 0 && !sameWeight) {
int offset = random.nextInt(totalWeight);
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有的Invokers都有相同的权重 or 总权重=0,则直接随机获取
return invokers.get(random.nextInt(length));
}
}
- 总体步骤:
- 通过计算每一个 Invoker 的权重来计算总权重 totalWeight,并判断是否所有的 Invoker 都有相同的权重;
- 如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;
- 如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取,算法如下:
- 根据权重进行随机获取,有两种算法:假设有 4 个 Invoker,权重分别是1,2,3,4,则总权重是 1+2+3+4=10,说明每个 Invoker 被选中的概率为1/10,2/10,3/10,4/10。此时有两种算法可以实现带概率的选择:
- 构造这样的一个数组 a = [1,2,2,3,3,3,4,4,4,4], 先随机生成一个 [0,10) 的值,比如 5,该值作为数组的 index,此时获取到的是 a[5] = 3,即使用第三个 Invoker;(该算法占用空间大)
- 先随机生成一个 [0,10) 的值 index,比如 5,从前向后让 index 递减权重,直到差值<0,那么最后那个使差值<0的 Invoker 就是当前选择的 Invoker - RandomLoadBalance 采用该种算法
- 减第一个 Invoker,5-1=4>0,继续减
- 减第二个 Invoker,4-2=2>0,继续减
- 减第三个 Invoker,2-3<0,则获取第三个 Invoker
- 判断所有 Invoker 的权重是否相同,有两种算法:
- 每次遍历一个 Invoker,都与其前一个 Invoker 的权重作比较,如果不相等,则设置 sameWeight=false,一旦 sameWeight=false 后,后续的遍历就不必再进行判断了 - RandomLoadBalance 采用这种;
- 将所有的 Invoker 与第一个 Invoker 的权重作比较,如果都相等,则sameWeight=true,否则 sameWeight=false - LeastActiveLoadBalance 采用这种。
四、RoundRobinLoadBalance
- 轮询,按公约后的权重设置轮询比率。
- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
private static int RECYCLE_PERIOD = 60000;
protected static class WeightedRoundRobin {
private int weight; // Invoker的权重
private AtomicLong current = new AtomicLong(0); // 同时被多个线程选中的权重之后,假设同时被4个线程选中,weight=100,那么current=400
private long lastUpdate; // 用于缓存超时的判断
...
// 增加一个权重值
public long increaseCurrent() {
return current.addAndGet(weight);
}
public void sel(int total) {
current.addAndGet(-1 * total);
}
...
}
/**
* 外层 key: serviceKey.methodName
* 内层 key: url = {protocol://username:password@ip:port/path?xx=yy&uu=ii}
*/
private ConcurrentMap> methodWeightMap = new ConcurrentHashMap>();
private AtomicBoolean updateLock = new AtomicBoolean();
...
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
// 创建缓存 Map> methodWeightMap
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
ConcurrentMap map = methodWeightMap.get(key);
if (map == null) {
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap());
map = methodWeightMap.get(key);
}
int totalWeight = 0;
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
Invoker selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
for (Invoker invoker : invokers) {
// url = {protocol://username:password@ip:port/path?xx=yy&uu=ii}
String identifyString = invoker.getUrl().toIdentityString();
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
// 获取当前 Invoker 的权重
int weight = getWeight(invoker, invocation);
if (weight < 0) {
weight = 0;
}
// 创建 WeightedRoundRobin
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
weightedRoundRobin.setWeight(weight);
map.putIfAbsent(identifyString, weightedRoundRobin);
weightedRoundRobin = map.get(identifyString);
}
// 权重发生了变化,eg. 在预热期间,预热权重随时间发生变化
if (weight != weightedRoundRobin.getWeight()) {
//weight changed
weightedRoundRobin.setWeight(weight);
}
// 1. 将weight加到current上:current=current+weight
long cur = weightedRoundRobin.increaseCurrent();
// 设置最后更新的时间
weightedRoundRobin.setLastUpdate(now);
// 2. 最后选出current最大的Invoker作为最终要调用的Invoker
if (cur > maxCurrent) {
maxCurrent = cur;
selectedInvoker = invoker;
selectedWRR = weightedRoundRobin;
}
// 计算总权重
totalWeight += weight;
}
// 加锁做缓存清除操作:
// invokers.size() != map.size() 说明 invokers 发生了变化(新增或下线)
// 新增:下次循环会增加到map
// 下线:只能靠如下的缓存清除策略从map中进行删除
if (!updateLock.get() && invokers.size() != map.size()) {
if (updateLock.compareAndSet(false, true)) {
try {
// CopyOnWrite
// copy -> modify -> update reference
ConcurrentMap newMap = new ConcurrentHashMap();
newMap.putAll(map);
Iterator> it = newMap.entrySet().iterator();
while (it.hasNext()) {
Entry item = it.next();
// 如果该缓存已经有60s没有使用了,则清除
if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
it.remove();
}
}
methodWeightMap.put(key, newMap);
} finally {
updateLock.set(false);
}
}
}
// 返回current最大的Invoker作为最终要调用的Invoker
if (selectedInvoker != null) {
// 3. 当前的Invoker的current减去总权重:current=current-totalWeight
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
}
}
平滑权重轮询算法:
- 每次做负载均衡时,遍历所有的服务端(Invoker)列表。对每个 Invoker,
a) current = current + weight
b) 计算总权重 totalWeight = totalWeight + weight
- 遍历完所有的 Invoker 后,current 最大的节点就是本次要选择的节点。最后,该节点的 current = current - totalWeight
举例说明:
eg. 假设有3个Invoker:A,B,C, 权重是1,2,3,调用如下,我们发现最后调用次数 A:B:C=1:2:3 与权重相符。而且A,B,C的调用也是穿插的(平滑权重轮询的好处,而普通权重轮询是会出现 [C,C,C,B,B,A] 这样短时间内不断调用同一个节点的问题-会导致该节点压力骤增)
五、LeastActiveLoadBalance
- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(通过
ActiveLimitFilter
计算每个接口方法的活跃数)- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
三种使用姿势
注意
LeastActiveLoadBalance 需要与 ActiveLimitFilter 配合使用,后者用于记录当前客户端对当前 Invoker 的活跃数及其当前调用方法的活跃数。(注意:actives如果设置为0,则不会加载;设置为<0,只会记录活跃数,不会进行并发数限流;设置为>0,则会进行每个客户端的并发限制逻辑)
/**
* LeastActiveLoadBalance
*
* 需要与ActiveLimitFilter配合使用(ActiveLimitFilter用于记录当前的Invoker的当前方法的活跃数active)
* @see com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
*/
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
private final Random random = new Random();
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Number of invokers
int leastActive = -1; // 所有Invoker的最小活跃数
int leastCount = 0; // 有最小活跃数leastActive的Invoker个数(leastCount=2,表示有两个Invoker其leastActive相同)
int[] leastIndexs = new int[length]; // 存储leastActive的Invoker在List> invokers列表中的索引值
int totalWeight = 0; // 总权重
int firstWeight = 0; // 第一个被遍历的Invoker的权重,用于比较来计算是否所有的Invoker都有相同的权重
boolean sameWeight = true; // 是否所有的Invoker都有相同的权重
/**
* 1、初始化最小活跃数的Invoker列表:leastIndexs[]
* 遍历所有的Invoker,
* a) 获取每一个方法的活跃数active及其权重;
* b) 如果遍历到的Invoker是第一个遍历的Invoker或者有更小的活跃数的Invoker,所有的计数清空,重新进行初始化;
* c) 如果遍历到的Invoker的活跃数active与之前记录的leastActive相同,则将当前的Invoker记录到 leastIndexs[] 中
* 判断所有的Invoker是否都有相同的权重。
* 2、如果leastIndexs[]中只有一个值,则直接获取对应索引的Invoker;否则按照 RandomLoadBalance 的逻辑进行选择
*/
for (int i = 0; i < length; i++) {
Invoker invoker = invokers.get(i);
// 获取当前Invoker当前方法的活跃数,该活跃数由 ActiveLimitFilter 进行记录
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // Active number
int afterWarmup = getWeight(invoker, invocation); // Weight
if (leastActive == -1 || active < leastActive) { // 如果遍历的是第一个Invoker或者有更小的活跃数,所有的计数清空,重新进行初始化
leastActive = active; // 记录最小活跃数
leastCount = 1; // Reset leastCount, count again based on current leastCount
leastIndexs[0] = i; // Reset
totalWeight = afterWarmup; // Reset
firstWeight = afterWarmup; // 记录第一个被遍历的Invoker的权重
sameWeight = true; // Reset, every invoker has the same weight value?
} else if (active == leastActive) { // 如果遍历到的Invoker的活跃数active与之前记录的leastActive相同
leastIndexs[leastCount++] = i; // 则将当前的Invoker记录到 leastIndexs[] 中
totalWeight += afterWarmup; // Add this invoker's weight to totalWeight.
// 判断所有的Invoker是否都有相同的权重?
if (sameWeight && i > 0 && afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
if (leastCount == 1) {
return invokers.get(leastIndexs[0]);
}
// 后续的逻辑与 RandomLoadBalance 相同
if (!sameWeight && totalWeight > 0) {
int offsetWeight = random.nextInt(totalWeight) + 1;
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0) {
return invokers.get(leastIndex);
}
}
}
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
}
}
- 总体步骤(都是针对当前客户端对指定 Invoker 的并发执行数)
- 初始化最小活跃数的 Invoker 列表:leastIndexs[]
遍历所有的 Invoker,
1.1. 获取每一个 Invoker 的当前被调用方法的活跃数 active 及其权重;
1.2. 如果遍历到的 Invoker 是第一个遍历的 Invoker 或者有更小的活跃数的 Invoker,所有的计数清空,重新进行初始化;
1.3. 如果遍历到的 Invoker 的活跃数 active 与之前记录的 leastActive 相同,则将当前的 Invoker 记录到 leastIndexs[] 中
判断所有的 Invoker 是否都有相同的权重。
- 如果 leastIndexs[] 中只有一个值,则直接获取对应索引的 Invoker;否则按照 RandomLoadBalance 的逻辑进行选择:如果总权重=0或者所有的 Invoker 都有相同的权重,则直接随机获取;如果总权重>0并且不是所有的 Invoker 都有相同的权重,则根据权重进行随机获取。
最后看下 ActiveLimitFilter
的相关逻辑:(关于 ActiveLimitFilter
后续进行分析)
/**
* 1. 仅用于 consumer 端
* 2. 需要配置 actives 参数
* <0,只记录活跃数(并发度)
* >0, 记录活跃数(并发度)+ 限流(限制每个客户端的并发执行数)
*/
@Activate(group = Constants.CONSUMER, value = Constants.ACTIVES_KEY)
public class ActiveLimitFilter implements Filter {
@Override
public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
// 获取最大并发数 actives=10
int max = invoker.getUrl().getMethodParameter(methodName, Constants.ACTIVES_KEY, 0);
// 获取当前调用方法的RpcStatus
RpcStatus count = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 只有当配置的 actives>0,才会做并发度限流,否则只是简单的计数
if (max > 0) {
// 限流操作
...
}
long begin = System.currentTimeMillis();
// 当前活跃数 + 1
RpcStatus.beginCount(url, methodName);
try {
// 真正调用
Result result = invoker.invoke(invocation);
// 正常结束:当前活跃数-1
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, true);
return result;
} catch (RuntimeException t) {
// 发生异常:当前活跃数-1,抛出异常
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, false);
throw t;
}
}
}
六、ConsistentHashLoadBalance
关于一致性 hash 的介绍及其优点,见 https://www.cnblogs.com/java-zhao/p/5158034.html
- 一致性 Hash,相同参数的请求总是发到同一提供者。
- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。(https://www.cnblogs.com/java-zhao/p/5158034.html)
- 缺省只对第一个参数值 hash,如果要修改,请配置
(表示对前两个参数值进行 hash)
- 缺省用
160
份虚拟节点,如果要修改,请配置
/**
* ConsistentHashLoadBalance
* 1. 组装 serviceKey.methodName => {group/}interface{:version}.methodName,获取或创建(第一次或者invokers发生了变化)该 serviceKey 对应的的 selector
* 为每一个Invoker创建160个虚拟节点,存储到 TreeMap 中
* key的计算(需要将key打散): 对于每一个Invoker,
* a) 40次外层循环:md5(ip:port+i)(i=[0~39]),此时生成一个长度为16的字节数组 byte[] digest
* b) 4层内层循环:分别对 digest 按照每四个字节(h=0=>digest[0~3],h=1=>digest[4~7],...)进行hash(公式:hash(md5(ip:port+i),h)(h=[0~3])),计算出4个不同的Long,该值作为TreeMap的key
* value:当前遍历的 Invoker
*
* 2. 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker
* a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
* b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对digest[0~3]进行hash
* c) 从 TreeMap 中获取第一个 >= 该hash 的Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
* d) 返回该 Entry 的 value 值,即 Invoker
*/
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
/**
* key: serviceKey.methodName => {group/}interface{:version}.methodName
*/
private final ConcurrentMap> selectors = new ConcurrentHashMap>();
@SuppressWarnings("unchecked")
@Override
protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {
// 1. 获取方法名
String methodName = RpcUtils.getMethodName(invocation);
// 2. 组装key:serviceKey.methodName => {group/}interface{:version}.methodName
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 3. 计算 invokers 的 hash 值
int identityHashCode = System.identityHashCode(invokers);
// 4. 根据 key 获取相应的 selector 实例
ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(key);
// 5. 如果是第一次创建selector || invokers已经发生了变化(宕机或者添加了新的机器 - 此时identityHashCode发生了变化),
// 则新建selector,并存储到缓存中
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(key, new ConsistentHashSelector(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector) selectors.get(key);
}
// 6. 根据请求参数invocation使用selector获取Invoker(一致性hash:相同的请求参数值会请求到同一台机器Invoker上去)
return selector.select(invocation);
}
/**
* 该 selector 是针对 key:serviceKey.methodName => {group/}interface{:version}.methodName 的存储器
*/
private static final class ConsistentHashSelector {
/**
* 虚拟节点
*/
private final TreeMap> virtualInvokers;
/**
* 虚拟节点数,默认是 160
*
*
*
*/
private final int replicaNumber;
/**
* 默认只对第一个参数进行hash,配置 hash.arguments=0,1,表示对前两个参数进行hash
*
*
*
*/
private final int[] argumentIndex;
/**
* 如上key的全部Invoker列表的hash值
* 如果发生了变化,说明新添加了如上key的机器Invoker或者有如上key的机器Invoker下线,此时需要重建selector
*/
private final int identityHashCode;
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.arguments=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]);
}
for (Invoker invoker : invokers) {
// 1. 获取真实节点: ip:port
String address = invoker.getUrl().getAddress();
// 2. 生成虚拟节点
for (int i = 0; i < replicaNumber / 4; i++) {
// 2.1. 对ip:port+递增数字做md5 -> 4个虚拟节点的总标识digest(16字节长度)
byte[] digest = md5(address + i);
for (int h = 0; h < 4; h++) {
// 2.2. 再对digest进行hash(每四位)得到最终的每个虚拟节点的标识m,m作为TreeMap的key,Invoker作为value,存储起来
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker select(Invocation invocation) {
// 1. 获取argumentIndex中指定的参数值,连接起来作为key
String key = toKey(invocation.getArguments());
// 2. 对参数值连接key做md5
byte[] digest = md5(key);
// 3. 对digest[0~3]进行hash,然后进行选择
return selectForKey(hash(digest, 0));
}
// 获取 argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为 key
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();
}
// TreeMap 是有序的树形结构。
// 1. 首先获取至少大于或者等于当前key的Entry - 即完成顺时针查找的目的
// 2. 如果没有找到,则直接获取第一个Entry
private Invoker selectForKey(long hash) {
Map.Entry> entry = virtualInvokers.tailMap(hash, true).firstEntry();
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
// number=0,对 digest[0~3] 进行 hash
// number=1,对 digest[4~7] 进行 hash
// number=2,对 digest[8~11] 进行 hash
// number=3,对 digest[12~15] 进行 hash
private long hash(byte[] digest, int number) {
...
}
// 返回 16 个字节长度的字节数组
private byte[] md5(String value) {
...
}
}
}
- 总体步骤
- 组装
serviceKey.methodName
=> {group/}interface{:version}.methodName,获取或创建(第一次或者 invokers 发生了变化
)该 serviceKey 对应的的 selector。
为每一个Invoker创建160个虚拟节点,存储到 TreeMap 中:
- key 的计算(需要将 key 打散): 对于每一个 Invoker,
- a) 40次外层循环:md5(ip:port+i)(i=[0~39]),此时生成一个长度为16的字节数组 byte[] diges
- b) 4次内层循环:分别对 digest 按照每四个字节(h=0=>digest[0 ~ 3],h=1=>digest[4 ~ 7],...)进行hash(公式:hash(md5(ip:port+i),h)(h=[0~3])),计算出4个不同的Long,该值作为TreeMap的key
- value:当前遍历的 Invoker
- 根据请求参数值 invocation.getArguments() 使用 selector 获取 Invoker
a) 获取argumentIndex[](默认只是用第一个参数值,可配置)中指定的参数值,连接起来作为key
b) 对该key进行md5,得到长度为16的字节数组 byte[] digest,对 digest[0~3] 进行 hash
c) 从 TreeMap 中获取第一个 >= 该hash 的 Entry,如果没有获取到,则直接获取 TreeMap 的第一个 Entry 元素
d) 返回该 Entry 的 value 值,即 Invoker