Ribbon的负载均衡策略及原理

2018年06月27日 15:21:15 吴帝永 阅读数:9709

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wudiyong22/article/details/80829808

 

Load Balance负载均衡是用于解决一台机器(一个进程)无法解决所有请求而产生的一种算法。像nginx可以使用负载均衡分配流量,ribbon为客户端提供负载均衡,dubbo服务调用里的负载均衡等等,很多地方都使用到了负载均衡。

使用负载均衡带来的好处很明显:

  1. 当集群里的1台或者多台服务器down的时候,剩余的没有down的服务器可以保证服务的继续使用
  2. 使用了更多的机器保证了机器的良性使用,不会由于某一高峰时刻导致系统cpu急剧上升

负载均衡有好几种实现策略,常见的有:

  1. 随机 (Random)
  2. 轮询 (RoundRobin)
  3. 一致性哈希 (ConsistentHash)
  4. 哈希 (Hash)
  5. 加权(Weighted)

ILoadBalance 负载均衡器

ribbon是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做ILoadBalance的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。ILoadBalance的继承关系如下:

Ribbon的负载均衡策略及原理

 

负载均衡器是从EurekaClient(EurekaClient的实现类为DiscoveryClient)获取服务信息,根据IRule去路由,并且根据IPing判断服务的可用性。

负载均衡器多久一次去获取一次从Eureka Client获取注册信息呢?在BaseLoadBalancer类下,BaseLoadBalancer的构造函数,该构造函数开启了一个PingTask任务setupPingTask();,代码如下:

 
  1. public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,

  2. IPing ping, IPingStrategy pingStrategy) {

  3. if (logger.isDebugEnabled()) {

  4. logger.debug("LoadBalancer: initialized");

  5. }

  6. this.name = name;

  7. this.ping = ping;

  8. this.pingStrategy = pingStrategy;

  9. setRule(rule);

  10. setupPingTask();

  11. lbStats = stats;

  12. init();

  13. }

setupPingTask()的具体代码逻辑,它开启了ShutdownEnabledTimer执行PingTask任务,在默认情况下pingIntervalSeconds为10,即每10秒钟,向EurekaClient发送一次”ping”。

 
  1. void setupPingTask() {

  2. if (canSkipPing()) {

  3. return;

  4. }

  5. if (lbTimer != null) {

  6. lbTimer.cancel();

  7. }

  8. lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,

  9. true);

  10. lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);

  11. forceQuickPing();

  12. }

PingTask源码,即new一个Pinger对象,并执行runPinger()方法。

查看Pinger的runPinger()方法,最终根据 pingerStrategy.pingServers(ping, allServers)来获取服务的可用性,如果该返回结果,如之前相同,则不去向EurekaClient获取注册列表,如果不同则通知ServerStatusChangeListener或者changeListeners发生了改变,进行更新或者重新拉取。

完整过程是:

LoadBalancerClient(RibbonLoadBalancerClient是实现类)在初始化的时候(execute方法),会通过ILoadBalance(BaseLoadBalancer是实现类)向Eureka注册中心获取服务注册列表,并且每10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则从注册中心更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以根据具体的IRule来进行负载均衡。

 

IRule 路由

IRule接口代表负载均衡策略:

 
  1. public interface IRule{

  2. public Server choose(Object key);

  3. public void setLoadBalancer(ILoadBalancer lb);

  4. public ILoadBalancer getLoadBalancer();

  5. }

IRule接口的实现类有以下几种:

Ribbon的负载均衡策略及原理

Ribbon的负载均衡策略及原理

其中RandomRule表示随机策略、RoundRobinRule表示轮询策略、WeightedResponseTimeRule表示加权策略、BestAvailableRule表示请求数最少策略等等。

随机策略很简单,就是从服务器中随机选择一个服务器,RandomRule的实现代码如下:

 
  1. public Server choose(ILoadBalancer lb, Object key) {

  2. if (lb == null) {

  3. return null;

  4. }

  5. Server server = null;

  6.  
  7. while (server == null) {

  8. if (Thread.interrupted()) {

  9. return null;

  10. }

  11. List<Server> upList = lb.getReachableServers();

  12. List<Server> allList = lb.getAllServers();

  13. int serverCount = allList.size();

  14. if (serverCount == 0) {

  15. return null;

  16. }

  17. int index = rand.nextInt(serverCount); // 使用jdk内部的Random类随机获取索引值index

  18. server = upList.get(index); // 得到服务器实例

  19.  
  20. if (server == null) {

  21. Thread.yield();

  22. continue;

  23. }

  24.  
  25. if (server.isAlive()) {

  26. return (server);

  27. }

  28.  
  29. server = null;

  30. Thread.yield();

  31. }

  32. return server;

  33. }

RoundRobinRule轮询策略表示每次都取下一个服务器,比如一共有5台服务器,第1次取第1台,第2次取第2台,第3次取第3台,以此类推:

 
  1. public Server choose(ILoadBalancer lb, Object key) {

  2. if (lb == null) {

  3. log.warn("no load balancer");

  4. return null;

  5. }

  6.  
  7. Server server = null;

  8. int count = 0;

  9. while (server == null && count++ < 10) {

  10. List<Server> reachableServers = lb.getReachableServers();

  11. List<Server> allServers = lb.getAllServers();

  12. int upCount = reachableServers.size();

  13. int serverCount = allServers.size();

  14.  
  15. if ((upCount == 0) || (serverCount == 0)) {

  16. log.warn("No up servers available from load balancer: " + lb);

  17. return null;

  18. }

  19.  
  20. int nextServerIndex = incrementAndGetModulo(serverCount);

  21. server = allServers.get(nextServerIndex);

  22.  
  23. if (server == null) {

  24. /* Transient. */

  25. Thread.yield();

  26. continue;

  27. }

  28.  
  29. if (server.isAlive() && (server.isReadyToServe())) {

  30. return (server);

  31. }

  32.  
  33. // Next.

  34. server = null;

  35. }

  36.  
  37. if (count >= 10) {

  38. log.warn("No available alive servers after 10 tries from load balancer: "

  39. + lb);

  40. }

  41. return server;

  42. }

  43.  
  44. /**

  45. * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.

  46. *

  47. * @param modulo The modulo to bound the value of the counter.

  48. * @return The next value.

  49. */

  50. private int incrementAndGetModulo(int modulo) {

  51. for (;;) {

  52. int current = nextServerCyclicCounter.get();

  53. int next = (current + 1) % modulo;

  54. if (nextServerCyclicCounter.compareAndSet(current, next))

  55. return next;

  56. }

  57. }

WeightedResponseTimeRule继承了RoundRobinRule,开始的时候还没有权重列表,采用父类的轮询方式,有一个默认每30秒更新一次权重列表的定时任务,该定时任务会根据实例的响应时间来更新权重列表,choose方法做的事情就是,用一个(0,1)的随机double数乘以最大的权重得到randomWeight,然后遍历权重列表,找出第一个比randomWeight大的实例下标,然后返回该实例,代码略。

BestAvailableRule策略用来选取最少并发量请求的服务器:

 
  1. public Server choose(Object key) {

  2. if (loadBalancerStats == null) {

  3. return super.choose(key);

  4. }

  5. List<Server> serverList = getLoadBalancer().getAllServers(); // 获取所有的服务器列表

  6. int minimalConcurrentConnections = Integer.MAX_VALUE;

  7. long currentTime = System.currentTimeMillis();

  8. Server chosen = null;

  9. for (Server server: serverList) { // 遍历每个服务器

  10. ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 获取各个服务器的状态

  11. if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 没有触发断路器的话继续执行

  12. int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 获取当前服务器的请求个数

  13. if (concurrentConnections < minimalConcurrentConnections) { // 比较各个服务器之间的请求数,然后选取请求数最少的服务器并放到chosen变量中

  14. minimalConcurrentConnections = concurrentConnections;

  15. chosen = server;

  16. }

  17. }

  18. }

  19. if (chosen == null) { // 如果没有选上,调用父类ClientConfigEnabledRoundRobinRule的choose方法,也就是使用RoundRobinRule轮询的方式进行负载均衡

  20. return super.choose(key);

  21. } else {

  22. return chosen;

  23. }

  24. }

使用Ribbon提供的负载均衡策略很简单,只需以下几部:

1、创建具有负载均衡功能的RestTemplate实例

 
  1. @Bean

  2. @LoadBalanced

  3. RestTemplate restTemplate() {

  4. return new RestTemplate();

  5. }

使用RestTemplate进行rest操作的时候,会自动使用负载均衡策略,它内部会在RestTemplate中加入LoadBalancerInterceptor这个拦截器,这个拦截器的作用就是使用负载均衡。

默认情况下会采用轮询策略,如果希望采用其它策略,则指定IRule实现,如:

 
  1. @Bean

  2. public IRule ribbonRule() {

  3. return new BestAvailableRule();

  4. }

这种方式对Feign也有效。

我们也可以参考ribbon,自己写一个负载均衡实现类。

可以通过下面方法获取负载均衡策略最终选择了哪个服务实例:

 
  1. @Autowired

  2. LoadBalancerClient loadBalancerClient;

  3.  
  4. //测试负载均衡最终选中哪个实例

  5. public String getChoosedService() {

  6. ServiceInstance serviceInstance = loadBalancerClient.choose("USERINFO-SERVICE");

  7. StringBuilder sb = new StringBuilder();

  8. sb.append("host: ").append(serviceInstance.getHost()).append(", ");

  9. sb.append("port: ").append(serviceInstance.getPort()).append(", ");

  10. sb.append("uri: ").append(serviceInstance.getUri());

  11. return sb.toString();

  12. }

 

分类:

技术点:

相关文章: