기사 디렉토리
머리말
마이크로 서비스 수준에서 로드 밸런싱은 주로 서버 압력 문제를 해결합니다.로드 밸런싱 알고리즘을 통해 요청을 다른 마이크로 서비스에 할당할 수 있으므로 과도한 요청 압력으로 인해 특정 마이크로 서비스로 인해 서비스가 붕괴되는 문제를 해결할 수 있습니다. 이 글은 dubbo의 여러 로드 밸런싱 알고리즘을 분석한 것으로, dubbo의 로드 밸런싱 알고리즘 구현을 이해하면 로드 밸런싱 알고리즘의 핵심 아이디어에 익숙해질 수 있습니다.
Dubbo는 로드 밸런싱을 위한 추상 인터페이스 LoadBalance를 제공합니다.
- 일관된 해시 알고리즘: ConsistentHashLoadBalance.
- 최소 활동 알고리즘: LeastActiveLoadBalance.
- dubbo의 기본 로드 밸런싱 알고리즘인 가중 임의: RandomLoadBalance.
- 라운드 로빈 알고리즘: RoundRobinLoadBalance.
- 최단 응답 시간 알고리즘: ShortestResponseLoadBalance.
dubbo에서 자체 로드 밸런싱 알고리즘을 사용하려면 이 인터페이스를 구현하고 메서드를 다시 작성한 다음 dubbo spi에 따라 구현을 구성하기만 하면 됩니다. 로드 밸런싱 알고리즘을 사용할 것입니다.
- LoadBalance: 로드 밸런싱을 위한 추상 인터페이스입니다.
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
- AbstractLoadBalance: 로드 밸런싱 추상 인터페이스 LoadBalance의 추상 구현입니다.
public abstract class AbstractLoadBalance implements LoadBalance {
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
int ww = (int) (Math.round(Math.pow((uptime / (double) warmup), 2) * weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}
@Override
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);
}
return doSelect(invokers, url, invocation);
}
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
if (invoker instanceof ClusterInvoker) {
url = ((ClusterInvoker<?>) invoker).getRegistryUrl();
}
// Multiple registry scenario, load balance among multiple registries.
if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
weight = url.getParameter(WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
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;
}
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);
}
}
일관된 해시 알고리즘: ConsistentHashLoadBalance
분산 시스템에서 일관된 해시 알고리즘은 지정된 시스템에 대한 요청의 일부를 고정하고 균형 분배 효과를 얻을 수 있습니다. 일관된 해시 알고리즘에는 해시 링이라는 개념이 있습니다.dubbo에서는 TreeMap을 사용하여 해시 링을 구현합니다.TreeMap의 기능은 키가 작은 것부터 큰 것까지 저장하는 것입니다.
소스 코드 분석:
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;
// 计算整个节点集合list的hashcode,目的在于判断集合中的元素是否发生了变化。
int invokersHashCode = getCorrespondingHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 如果根据key取出来的selector是空的,或者hashcode已经发生了变化,就说明节点信息已经发生了变化,那么就需要重新构造hash环。
if (selector == null || selector.identityHashCode != invokersHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 选择节点
return selector.select(invocation);
}
// 构造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();
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
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]);
}
// 循环所有的节点信息,构造hash环。
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
// replicaNumber默认为160,这里会根据每一个节点的ip生成40个虚拟节点。
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = Bytes.getMD5(address + i);
// 为每一个虚拟节点生成四个存储位置
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
// key就是当前的节点的ip计算出来的hash值,value就是当前循环的节点。
virtualInvokers.put(m, invoker);
}
}
totalRequestCount = new AtomicLong(0);
serverCount = invokers.size();
erRequestCountMap.clear();
}
// 最终调用这个方法进行选择节点
private Invoker<T> selectForKey(long hash) {
// 参数hash就是根据hash参数计算出来的下标,这里根据下标获取大于等于该hash值的最小节点信息
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
// 如果节点信息不存在,那么说明当前hash值就是hash环上最大的那么节点信息,
if (entry == null) {
// 那么接下来的那个下标就是hash环上的第一个元素了。
entry = virtualInvokers.firstEntry();
}
// 取出节点信息的地址
String serverAddress = entry.getValue().getUrl().getAddress();
// 这里设定了一个请求线程数阈值
double overloadThread = ((double) totalRequestCount.get() / (double) serverCount) * OVERLOAD_RATIO_THREAD;
// 如果当前选定的这个节点已经接收过请求并且接收的请求数超过了设定的这个线程数阈值,说明这个节点不可用
while (serverRequestCountMap.containsKey(serverAddress)
&& serverRequestCountMap.get(serverAddress).get() >= overloadThread) {
// 如果这个节点不可用,就需要获取大于给定的key的最小节点信息
entry = getNextInvokerNode(virtualInvokers, entry);
// 重新获取节点信息的地址
serverAddress = entry.getValue().getUrl().getAddress();
}
// 当前选定的这个节点没有接收过请求,那么就说明可用,将当前节点信息假如到serverRequestCountMap中
if (!serverRequestCountMap.containsKey(serverAddress)) {
serverRequestCountMap.put(serverAddress, new AtomicLong(1));
} else {
// 接收的请求数未超过设定的这个线程数阈值,那么就说明可用,将当前节点信息已经接收的请求数加1
serverRequestCountMap.get(serverAddress).incrementAndGet();
}
totalRequestCount.incrementAndGet();
return entry.getValue();
}
요약하다:
- 먼저 전체 서비스 목록의 해시코드 값을 계산하고, 서비스 목록의 해시코드 값에 따라 해시링 재구축이 필요한지 판단하고, 해시링 구성 시 각 노드의 IP에 따라 40개의 가상노드를 생성한다. , 각 가상 노드는 4개의 위치에 저장되며, 가상 노드의 도입은 실제 노드에서 더 적은 데이터로 인해 발생하는 데이터 왜곡 문제를 해결하기 위한 것입니다.
- 요청에 지정된 매개 변수 값에 따라 해시 값을 계산하고 해시 링에서 노드 정보를 얻습니다.
- 획득한 노드 정보에 따라 현재 노드가 이전에 요청을 수신했는지 여부를 계산합니다.
- 요청이 수신되었고 수신된 요청 수가 설정된 요청 수 임계값을 초과하면 현재 선택된 노드는 사용할 수 없는 것으로 간주되고 다음 노드 정보가 계속 선택됩니다.
- 현재 선택한 노드가 요청을 받지 못했다면 사용 가능하다는 의미이고 현재 노드 정보가 serverRequestCountMap에 저장되어 있거나 받은 요청 수가 설정된 스레드 수 임계값을 초과하지 않으면 해당 노드가 있음을 의미합니다. 현재 노드 정보 수신 요청 수를 1 증가시킵니다.
- 마지막으로 선택한 노드를 반환합니다.
최소 활동: 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;
// 标志是否所有的服务的具有相同的权重,默认为true
boolean sameWeight = true;
// 循环所有的服务
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取当前服务的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取当前服务的权重,默认为100
int afterWarmup = getWeight(invoker, invocation);
// 将当前服务的权重按照序号加入到weights数组中。
weights[i] = afterWarmup;
// 如果当前服务是第一个服务或者当前服务的活跃数比之前记录的最小活跃数还小
if (leastActive == -1 || active < leastActive) {
// 当前服务的活跃数为最小活跃数
leastActive = active;
// 具有相同最小活跃数的服务个数,也就是当前服务器的个数
leastCount = 1;
// 将当前服务按照序号加入到具有相同最小活跃数的服务集合中
leastIndexes[0] = i;
// 当前服务的权重就是权重之和
totalWeight = afterWarmup;
// 当前服务的权重就是第一个最小活跃数的服务的权重
firstWeight = afterWarmup;
// 这种情况下,所有的服务的权重都是一样的。
sameWeight = true;
// 如果当前服务的活跃数和之前记录的最小活跃数是一样的
} else if (active == leastActive) {
// 当前服务加入到具有最小活跃数的服务集合中,具有相同最小活跃数的服务个数加1
leastIndexes[leastCount++] = i;
// 总权重加上当前服务的权重
totalWeight += afterWarmup;
// 如果所有的服务的权重都是一样的,并且当前服务的权重和第一个最小活跃数的服务的权重不相等
if (sameWeight && afterWarmup != firstWeight) {
// 说明所有的服务的权重不一样的
sameWeight = false;
}
}
}
// 如果上边的所有的流程完了之后,只有一个最小活跃数的服务
if (leastCount == 1) {
// 那么就直接将这个服务返回即可,这个服务就是目前最小活跃数的服务
return invokers.get(leastIndexes[0]);
}
// 如果所有的服务的权重不一样,并且总权重之和大于0
if (!sameWeight && totalWeight > 0) {
// 随机从总权重中获取一个数
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环具有相同最小活跃数的服务集合
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
// 从权重集合中获取当前服务的权重,并用随机权重减去这个服务的权重
offsetWeight -= weights[leastIndex];
// 如果随机权重减去这个服务的权重之后小于0,那么就表明这个服务就是符合条件的服务。
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
// 如果所有服务的权重都是一样的,并且总权重为0,那么随机选取一个服务即可
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
요약하다:
- 모든 서비스 중에서 최소 활성 수가 같은 서비스를 찾아 이들 서비스의 가중치 집합과 서비스 수를 기록하고 모든 서비스의 가중치가 동일한지 확인합니다.
- 모든 서비스의 가중치가 같으면 최소 활성 수가 동일한 발견된 서비스 중에서 무작위로 서비스를 반환할 수 있습니다.
- 모든 서비스의 가중치가 다르고 최소 활성 수가 동일한 서비스가 하나만 있는 경우 이 서비스를 직접 반환합니다.
- 모든 서비스 가중치가 동일하지 않고 최소 활성 수가 동일한 서비스가 여러 개 있는 경우 총 가중치에 따라 임의의 숫자를 구한 후 동일한 최소 활성 수를 가진 각 검색된 서비스의 가중치에서 빼며, 획득한 값이 0보다 작으면 현재 서비스는 적격 서비스이므로 그냥 반환합니다.
가중 무작위: RandomLoadBalance(dubbo의 기본 로드 밸런싱 알고리즘)
가중 랜덤 알고리즘은 dubbo의 기본 로드 밸런싱 알고리즘으로, 가중치를 사용하여 서비스 목록에서 무작위로 서비스를 가져옵니다.
소스 코드 분석
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取远程服务的个数
int length = invokers.size();
// 这是一个标识,标识所有的远程服务的权重是否都是一样的,后续再遍历所有的远程服务的时候,只要有一个远程服务的权重和其他的不一致,该标志就会被改为false,默认为true。
boolean sameWeight = true;
// 用来存放各个远程服务的权重
int[] weights = new int[length];
// 总权重,各个远程服务的权重之和。
int totalWeight = 0;
// 循环所有的远程服务
for (int i = 0; i < length; i++) {
// 获取当前远程服务的权重
int weight = getWeight(invokers.get(i), invocation);
// 计算所有的远程服务权重之和
totalWeight += weight;
// 保存当前远程服务的权重
weights[i] = totalWeight;
// 旧版本的是注释的这种写法,容易理解点:就是当前远程服务的权重和上一个远程服务的权重进行比较,如果不一样,就将sameWeight改为false,新版的这种写法不容易理解。
/**
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
**/
if (sameWeight && totalWeight != weight * (i + 1)) {
sameWeight = false;
}
}
// 如果总权重 > 0 并且所有的远程服务的权重都不一样
if (totalWeight > 0 && !sameWeight) {
// 随机从总权重中获取一个数字。
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 再次循环所有的远程服务集合
for (int i = 0; i < length; i++) {
// 如果随机数小于当前远程服务的权重,那么随机数刚好落在当前远程服务所占有的权重区间。
if (offset < weights[i]) {
return invokers.get(i);
}
}
}
// 如果总权重 <= 0 或者所有的远程服务的权重都一样,那么随机选取一个远程服务
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
요약하다:
- 모든 서비스 노드의 수를 계산하고 이 수를 반복하여 각 노드의 가중치를 구하고 모든 노드의 가중치가 동일한지 확인합니다.
- 모든 노드의 가중치가 동일하면 임의로 노드를 반환합니다.
- 각 노드의 가중치가 다른 경우 노드 가중치의 합을 계산하고 루프 노드의 순서대로 각 노드의 가중치를 캐시합니다.
- 가중치 합 내에서 임의의 데이터를 임의로 생성하고 가중치와 캐시를 순환하며 가중치 합보다 조금 작은 난수가 있는 첨자가 현재 선택된 노드입니다.
라운드 로빈 알고리즘: RoundRobinLoadBalance
폴링 알고리즘의 아이디어는 매번 서비스를 선택하는 것이며 이 서비스는 마지막 선택과 일치하지 않아야 합니다.
소스 코드 분석:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// key就是接口全限定名.方法名
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 处理RoundRobinLoadBalance自身的缓存,key为接口全限定名.方法名,value为mao<每一个服务的标识identifyString,封装的WeightedRoundRobin>
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.computeIfAbsent(key, k -> new ConcurrentHashMap<>());
// 总权重
int totalWeight = 0;
// 设置的最大值,默认为long的最小值,为负数
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
// 选中的服务
Invoker<T> selectedInvoker = null;
// 选中服务对应的WeightedRoundRobin信息
WeightedRoundRobin selectedWRR = null;
// 开始循环所有服务
for (Invoker<T> invoker : invokers) {
// 获取当前服务的标识
String identifyString = invoker.getUrl().toIdentityString();
// 获取当前服务的权重,默认为100
int weight = getWeight(invoker, invocation);
// 处理缓存,如果存在当前服务的WeightedRoundRobin信息,就返回当前服务的WeightedRoundRobin,如果没有,就构造一个WeightedRoundRobin,并将当前服务的权重设置进去。
WeightedRoundRobin weightedRoundRobin = map.computeIfAbsent(identifyString, k -> {
WeightedRoundRobin wrr = new WeightedRoundRobin();
wrr.setWeight(weight);
return wrr;
});
// 判断下,是不是权重发生了变化,如果发生了变化,需要重新设置
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
// 实际上这里获取的就是当前这个服务的对应的权重,但是这个值在后续还会处理,这个值会直接影响服务能不能被选中。
long cur = weightedRoundRobin.increaseCurrent();
// 设置对应的服务信息最后更新时间为当前时间
weightedRoundRobin.setLastUpdate(now);
// 如果cur大于maxCurrent,当前服务被选中,相当于在找最大权重的那个服务
if (cur > maxCurrent) {
// 设置cur为maxCurrent
maxCurrent = cur;
// 记录选中的服务
selectedInvoker = invoker;
// 记录选中的服务的其他信息
selectedWRR = weightedRoundRobin;
}
// 记录总权重
totalWeight += weight;
}
// 处理RoundRobinLoadBalance中自身的缓存,如果缓存中的服务信息和服务列表中的信息不一致,那么就需要从缓存中将超过1分钟还没有被更新的服务移除掉。
if (invokers.size() != map.size()) {
map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
}
// 如果选中的服务不为空
if (selectedInvoker != null) {
// 这里会将当前选中的服务信息的cur设置成一个负数,也就是设置成所有权重的之和的负数,那么这个服务的cur就是最小的一个了,目的是为了保证下一次不会再次选中该服务
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// 如果上述流程之后,还未选中服务,那么直接就返回第一个服务。
return invokers.get(0);
}
요약하다:
- RoundRobinLoadBalance는 서비스 목록 선택 정보를 자체적으로 캐싱하며, 실제 서비스 목록 정보가 변경되면 자체 캐시 정보를 업데이트합니다. 즉, 1분 이상 캐시 정보에서 서비스 정보가 업데이트되지 않은 서비스를 삭제합니다.
- 각 호출에 대해 서비스 목록으로 이동하여 가중치가 가장 높은 서비스를 찾으면 기록되며, 없으면 첫 번째 서비스로 바로 돌아갑니다.
- 서비스 목록이 순환되면 서비스 시간이 먼저 업데이트되고 cur 값이 설정됩니다. 이 값은 매우 중요하며 현재 서비스를 선택할 수 있는지 여부를 직접 결정합니다. 선택한 서비스는 나중에 cur 값을 다음으로 설정합니다. 다음 투표가 다시 선택되지 않도록 보장합니다.
최단 응답 시간 알고리즘: ShortestResponseLoadBalance
소스 코드 분석:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取远程服务的个数
int length = invokers.size();
// 最大的响应时间,默认为long的最大值
long shortestResponse = Long.MAX_VALUE;
// 具有相同最短响应时间的服务的个数
int shortestCount = 0;
// 具有相同最短响应时间的服务的集合
int[] shortestIndexes = new int[length];
// 每一个服务的权重集合
int[] weights = new int[length];
// 所有服务总权重和
int totalWeight = 0;
// 第一个最短响应时间的服务的权重,类似于选中了一个标准,之后用这个标准和每一个服务的权重进行比较,用来判断是否所有的服务的具有相同的权重。
int firstWeight = 0;
// 标志是否所有的服务的具有相同的权重,默认为true
boolean sameWeight = true;
// 循环所有的服务
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取当前服务的rpc信息
RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 获取当前服务成功响应所花费的平均响应时间
long succeededAverageElapsed = rpcStatus.getSucceededAverageElapsed();
// 获取当前服务的活跃数,加1的目的就是算上本次请求。
int active = rpcStatus.getActive() + 1;
// 计算处理完这些请求需要花费的总体响应时间
long estimateResponse = succeededAverageElapsed * active;
// 获取当前服务的权重
int afterWarmup = getWeight(invoker, invocation);
// 按照序号记录权重
weights[i] = afterWarmup;
// 如果当前服务处理完这些请求需要花费的总体响应时间小于设置最大的响应时间,这里实际上就是在找最小响应时间的服务。
if (estimateResponse < shortestResponse) {
// 设置最大的响应时间为当前服务的总体响应时间
shortestResponse = estimateResponse;
// 最短响应时间的数量为1,也就是本服务一个
shortestCount = 1;
// 记录最短响应时间的这个服务
shortestIndexes[0] = i;
// 总权重就是当前这个服务的权重
totalWeight = afterWarmup;
// 当前服务的权重就是第一个最短响应时间的服务的权重
firstWeight = afterWarmup;
// 这种情况下,所有的服务的权重都是一样的。
sameWeight = true;
// 如果当前服务的最短响应时间和之前记录的最短响应时间是一样的
} else if (estimateResponse == shortestResponse) {
// 当前服务加入到具有最短响应时间的服务集合中,具有相同最短响应时间的服务个数加1
shortestIndexes[shortestCount++] = i;
// 总权重加上当前服务的权重
totalWeight += afterWarmup;
// 如果所有的服务的权重都是一样的,并且当前服务不是列表中第一个服务,并且当前服务的权重和第一个最短响应时间的服务的权重不相等
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
// 说明所有的服务的权重不一样的
sameWeight = false;
}
}
}
// 如果上边的所有的流程完了之后,只有一个最短响应时间的服务
if (shortestCount == 1) {
// 那么就直接将这个服务返回即可,这个服务就是目前最短响应时间的服务
return invokers.get(shortestIndexes[0]);
}
// 如果所有的服务的权重不一样,并且总权重之和大于0
if (!sameWeight && totalWeight > 0) {
// 随机从总权重中获取一个数
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环具有相同最短响应时间的服务集合
for (int i = 0; i < shortestCount; i++) {
int shortestIndex = shortestIndexes[i];
// 从权重集合中获取当前服务的权重,并用随机权重减去这个服务的权重
offsetWeight -= weights[shortestIndex];
// 如果随机权重减去这个服务的权重之后小于0,那么就表明这个服务就是符合条件的服务。
if (offsetWeight < 0) {
return invokers.get(shortestIndex);
}
}
}
// 如果所有服务的权重都是一样的,并且总权重为0,那么随机选取一个服务即可
return invokers.get(shortestIndexes[ThreadLocalRandom.current().nextInt(shortestCount)]);
}
요약하다:
- 모든 서비스 중에서 응답 시간이 가장 짧은 서비스를 찾아 이들 서비스의 가중치 집합과 서비스 수를 기록하고 모든 서비스의 가중치가 동일한지 확인합니다.
- 모든 서비스의 가중치가 같으면 응답 시간이 가장 짧은 검색된 서비스 중에서 무작위로 서비스를 반환할 수 있습니다.
- 모든 서비스의 가중치가 다르고 응답 시간이 가장 짧은 서비스가 하나만 있는 경우 이 서비스를 직접 반환합니다.
- 모든 서비스 가중치가 동일하지 않고 응답 시간이 가장 짧은 서비스가 여러 개 있는 경우 전체 가중치에 따라 임의의 숫자를 구한 다음 검색된 각 응답 시간이 가장 짧은 서비스의 가중치에서 뺍니다. 획득한 값이 0보다 작으면 현재 서비스는 적격 서비스이므로 그냥 반환합니다.