Ribbon之IRule 负载均衡策略详解

1.IRule家族

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LneKI92V-1623824544221)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/092b6cbe-bdee-4762-8286-86c37ced48c5/Untitled.png)]



2.RoundRobinRule 轮询策略

2.1 核心方法解读

private AtomicInteger nextServerCyclicCounter;

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
						// 在线服务列表 这个就是BaseLoadBalancer中的upServerList
            List<Server> reachableServers = lb.getReachableServers();
						// 全部服务实例列表
            List<Server> allServers = lb.getAllServers();
						
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
						// 基于全部服务列表去做轮询
            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
								// 让出cpu的使用权,这个东西很少看到框架有用
                Thread.yield();
                continue;
            }
						// 判断服务是否存活
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }
				// 如果轮询超过10次都没有找到一个在线的服务,那么就会退出
        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

上面大致步骤为:

  1. 获取服务列表
  2. 基于nextServerCyclicCounter原子类去递增然后获取索引
  3. 根据索引获取的服务判断是否在线
  4. 不在线就重新获取,最多重复10次
  5. 最后返回服务实例信息(可能为null)

2.2轮询索引位算法

private int incrementAndGetModulo(int modulo) {
      for (;;) {
					// 原子类递增
          int current = nextServerCyclicCounter.get();
					// 取模使之永远在服务实例列表大小之内
          int next = (current + 1) % modulo;
					// 重新设置当前索引位
          if (nextServerCyclicCounter.compareAndSet(current, next))
              return next;
      }
  }


3.WeightedResponseTimeRule 基于权重的随机算法

这个类的权重是根据平均响应时间来计算的

public void maintainWeights() {
            ILoadBalancer lb = getLoadBalancer();
            if (lb == null) {
                return;
            }
            // 同一个任务,只有一个能够执行
            if (!serverWeightAssignmentInProgress.compareAndSet(false,  true))  {
                return; 
            }
            
            try {
                logger.info("Weight adjusting job started");
                AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
								// 获取负载均衡的数据统计
                LoadBalancerStats stats = nlb.getLoadBalancerStats();
                if (stats == null) {
                    // no statistics, nothing to do
                    return;
                }
                double totalResponseTime = 0;
                // find maximal 95% response time
								// 统计每个服务实例的平均响应时间的总和
                for (Server server : nlb.getAllServers()) {
                    // this will automatically load the stats if not in cache
                    ServerStats ss = stats.getSingleServerStat(server);
                    totalResponseTime += ss.getResponseTimeAvg();
                }
                // weight for each server is (sum of responseTime of all servers - responseTime)
                // so that the longer the response time, the less the weight and the less likely to be chosen
                Double weightSoFar = 0.0;
                
                // create new list and hot swap the reference
								// 
                List<Double> finalWeights = new ArrayList<Double>();
                for (Server server : nlb.getAllServers()) {
                    ServerStats ss = stats.getSingleServerStat(server);
										// 当前服务响应时间越长,则权重越小
                    double weight = totalResponseTime - ss.getResponseTimeAvg();
		                // 区间就是 weightSoFar,weightSoFar+weight
										// 当ss.getResponseTimeAvg();用户平均相应时间
                    weightSoFar += weight;
                    finalWeights.add(weightSoFar);   
                }
                setWeights(finalWeights);
            } catch (Exception e) {
                logger.error("Error calculating server weights", e);
            } finally {
								// 结束权重计算进程
                serverWeightAssignmentInProgress.set(false);
            }

        }

这个计算过程打个比方:

服务A有5个实例

A1平均响应时间100ms

A2平均响应时间150ms

A3平均响应时间50ms

A4平均响应时间200ms

A5平均响应时间100ms

则totalResponseTime  = 100+150+50+200+100 = 600ms

A1的随机值范围为:600-100 = 500    区间:[0,500)

A2的随机值范围为:600-150  + 500(A1权重)= 950   区间: [500,950)

A3的随机值范围为:600-50 + 950 = 1500   区间:[950,1500)

A4的随机值范围为:600-200 + 1500 = 1900   区间:[1500,1900)

A5的随机值范围为:600 - 100 + 1900 = 2400 区间:[1900,2400)

上面就是基于LoadBalancerStats来做的统计过程,这个统计每30秒执行一次

下面来看一下基于权重的选择服务过程

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            // get hold of the current reference in case it is changed from the other thread
	          // 这个就是上面计算得到的一个范围区间列表,就是[0,2400)的区间列表
						List<Double> currentWeights = accumulatedWeights;
						// 判断当前线程是否已经被中断过
            if (Thread.interrupted()) {
                return null;
            }
						// 获取所有的服务列表
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();

            if (serverCount == 0) {
                return null;
            }

            int serverIndex = 0;

            // last one in the list is the sum of all weights
						// 获取权重范围区间的最大值,这个就是随机值的范围
            double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); 
            // No server has been hit yet and total weight is not initialized
            // fallback to use round robin
            if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
								// 如果响应时间累计少于10毫秒,或者服务数量跟区间数量不统一
								// 那么还是走轮询算法
                server =  super.choose(getLoadBalancer(), key);
                if(server == null) {
                    return server;
                }
            } else {
                // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
                // 获取随机值,这里的random是所有线程公用一个的,所以结果是服从正态分布的
								// 但是也存在问题就是关于下一个种子的获取过程存在循环竞争问题
								double randomWeight = random.nextDouble() * maxTotalWeight;
                // pick the server index based on the randomIndex
                int n = 0;
                for (Double d : currentWeights) {
										// 判断随机值在哪个区间,就取那个区间的索引
                    if (d >= randomWeight) {
                        serverIndex = n;
                        break;
                    } else {
                        n++;
                    }
                }
							
                server = allList.get(serverIndex);
            }
						如果server为空,那就让出线程
            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }
						// 判断服务是否在线
            if (server.isAlive()) {
                return (server);
            }

            // Next.
            server = null;
        }
				// 这里一定会获取到服务实例,如果一个服务的多个服务实例下线,那么会延长负载均衡的计算时间
				// 对性能是会造成一定影响的。但是安全
        return server;
    }

从上面我们可以看到,其实就是拿着区间去计算

  1. 获取区间最大值
  2. 基于区间最大值生成随机数
  3. 判断随机数落在哪个区间
  4. 获取当前区间的服务实例
  5. 判断服务实例是否在线
  6. 返回服务实例信息


4.ResponseTimeWeightedRule 这个类是不建议使用的了

跟WeightedResponseTimeRule不能说一模一样,但是也差不了多少,只是在一些细节方面进行了改动。没啥好讲的

5.ClientConfigEnabledRoundRobinRule这个类就是为后面的几个子类做铺垫的

这个类主要就是将RoundRobinRule设置为默认默认规则,设定了默认的choose方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSUV4BSV-1623824544225)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bd71b3dd-71e3-43fd-a4d8-d71d2cbc71c4/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewdX0TgH-1623824544227)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb4148a5-cb56-43cb-921a-689f9adc9930/Untitled.png)]



6.BestAvailableRule 最小连接数选择策略

public Server choose(Object key) {
        if (loadBalancerStats == null) {
            return super.choose(key);
        }
				// 获取服务列表
        List<Server> serverList = getLoadBalancer().getAllServers();

				// 最小并发连接数
        int minimalConcurrentConnections = Integer.MAX_VALUE;
        long currentTime = System.currentTimeMillis();
        Server chosen = null;
        for (Server server: serverList) {
						// 获取每个服务的并发状态
            ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
            if (!serverStats.isCircuitBreakerTripped(currentTime)) {
								// 获取当前时间这个服务的并发连接数
                int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                // 如果当前并发连接数小于最小并发连接数,那么说明这个服务压力更小
								// 就选择这个服务来做调用
								if (concurrentConnections < minimalConcurrentConnections) {
										// 重置最小并发连接数
                    minimalConcurrentConnections = concurrentConnections;
                    // 选择连接数最小的服务
										chosen = server;
                }
            }
        }
				// 如果无法获取服务,则使用服务的轮询来做
        if (chosen == null) {
            return super.choose(key);
        } else {
            return chosen;
        }
    }

这个类的目的很明确,找到整个服务列表中连接数最小的,但是好像没有去除下线的服务。不太可靠的样子。



7.PredicateBasedRule 与 ZoneAvoidanceRule 基于Zone的过滤策略

ZoneAvoidanceRulePredicateBasedRule 子类。PredicateBasedRule 主要就是用来做过滤的。

主要使用的就是Predicate.apply方法来判断输入的数据是复合条件

ZoneAvoidanceRule已经讲过了,这里就不再赘述了。

其实就是在轮询之前,将服务列表过滤一遍,找到符合条件的服务进行返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qddCPAOj-1623824544231)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0b75e9fd-0bbf-4e98-8cc6-85c42f71f4be/Untitled.png)]



8.AvailabilityFilteringRule 服务实例可用性过滤策略

这个类也是在过滤后做轮询,主要是使用AvailabilityPredicate进行过滤

在AvailabilityPredicate中,我们可以看到apply方法中是过滤掉被断路器断路的服务实例,还有超过服务实例活跃连接数上限的服务实例。

基本上就是断路器限流+默认连接数限流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YU8Jvo7g-1623824544237)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9fe22e91-7f5f-45bf-9b48-87e9c78a300a/Untitled.png)]

Predicate 这个guawa类可以好好研究一下。跳起来有点懵。



9. RandomRule 纯随机的服务实例过滤策略

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
						// 判断线程有没有被打断过(debug)
            if (Thread.interrupted()) {
                return null;
            }
						// 获取在线列表
            List<Server> upList = lb.getReachableServers();
            // 获取服务实例列表
						List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }
						// 基于全部服务实例去随机
            int index = chooseRandomInt(serverCount);
						// 基于在线列表去获取,这是个锤子操作啊。数组下标越界,轻轻松松的
            server = upList.get(index);

            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
								服务为空 ,让出CPU区间
                Thread.yield();
                continue;
            }
					
            if (server.isAlive()) {
                return (server);
            }
							
            // Shouldn't actually happen.. but must be transient or a bug.
						// 服务实例为下线状态时,需要重置,这个基本不可能发生,因为是从上线列表拿的服务实例
						// 即使服务真的下线了,这里其实也是感知不到的,服务实例状态从在线列表中获取,
						// 基本一定为在线状态
            server = null;
						// 服务实例为空,再让一让
            Thread.yield(); // 再让一让CPU使用权
        }

        return server;

    }

总结:

  1. 拿到总服务实例列表
  2. 拿到在线服务实例列表
  3. 基于总服务实例数量进行随机
  4. 基于随机值去在线服务实例列表获取对应索引位的服务。
  5. 判断服务是否在线
  6. 返回服务


10.RetryRule 基于轮询的重试策略

public Server choose(ILoadBalancer lb, Object key) {
		long requestTime = System.currentTimeMillis();
		// 最大重试时间
		long deadline = requestTime + maxRetryMillis;
		
		Server answer = null;
		// 获取服务
		answer = subRule.choose(key);
		// 当上面没有获取到时
		if (((answer == null) || (!answer.isAlive()))
				&& (System.currentTimeMillis() < deadline)) {
			// 开启一个延迟500ms的定时任务标记线程被打断
			InterruptTask task = new InterruptTask(deadline
					- System.currentTimeMillis());

			while (!Thread.interrupted()) {
				// 循环获取
				answer = subRule.choose(key);
				// 最多循环500ms就退出获取流程
				if (((answer == null) || (!answer.isAlive()))
						&& (System.currentTimeMillis() < deadline)) {
					/* pause and retry hoping it's transient */
					Thread.yield();
				} else {
					break;
				}
			}

			task.cancel();
		}

		if ((answer == null) || (!answer.isAlive())) {
			return null;
		} else {
			return answer;
		}
	}

这个类就是在获取在线服务实例失败时,会进行重试,这个重试跟轮询的时候最多10次的不同,是有最长时间限制。在限定500ms后退出循环。

这里有个很有意思的设计:

定时任务+线程interrupt+线程yield

这里有个最多500ms重试时长是如何实现的呢?

  1. ribbon先会通过规则选择一个server
  2. 如果server不在线则会创建一个InterruptTask,这个定时任务主要是用于在延迟最多500ms后,对当前线程进行interrupt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JZPIKKWH-1623824544239)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ea4a47ec-e988-4bdb-ba11-d8e29e3bcc6b/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9tkphtKV-1623824544249)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/75af1fc2-3022-4519-ae83-aedb89d7ea0b/Untitled.png)]

  1. 当InterruptTask被执行后,则这个循环就会退出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S583dwT1-1623824544255)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/70ffd8a8-fffd-40d0-8f9d-8b269b403862/Untitled.png)]

这样就实现了单线程上的定时退出统计。

如果让我自己实现的话,我应该想不到这种方法,我会在while处直接处理

//这种实现方式
while(System.currentTimeMillis<deadline){
	answer = subRule.choose(key);

	if (((answer == null) || (!answer.isAlive()))
			&& (System.currentTimeMillis() < deadline)) {
		/* pause and retry hoping it's transient */
		Thread.yield();
	} else {
		break;
	}
}

11.小结

ribbon的负载均衡策略基本都解析完了,后面看看在负载均衡策略中使用的数据统计有没有什么亮点,如果有那就解析一下看看,没有就算了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欢谷悠扬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值