Ribbon负载均衡学习

你未必出类拔萃,但一定与众不同

客户端负载均衡-Ribbon

Spring Cloud Netflix Ribbon是一种客户端负载均衡的组件

在Spring Cloud中,提供服务的调用是Ribbon和OpenFeign。OpenFeign实际上也是基于Ribbon实现的。

微服务之间的调用往往被称为客户端的负载均衡,这是因为在Eureka的机制中,任何微服务都是Eureka的客户端。

文章目录

  • 客户端负载均衡-Ribbon
    • 负载均衡概述
    • 初识Ribbon
      • Ribbon概述
      • 实现负载均衡
      • Ribbon负载均衡器和策略
        • 负载均衡器
          • ILoadBalancer接口
          • BaseLoadBalancer的chooseServer
          • ZoneAwareLoadBalancer默认的负载均衡器
        • 负载均衡策略
          • 默认的负载均衡器
          • RoundRobinRule
          • RetryRule
          • WeightedResponseTimeRule
          • ZoneAvoidanceRule

负载均衡概述

负载均衡是大型网络系统必须实现的功能之一,主要原因有四点

  • 降低单机压力

    实现了负载均衡的系统往往能够根据合理的算法将用户请求和数据分摊到各个机器上,减少单机压力

  • 高可用和高性能

    某个节点出现问题,通过心跳机制进行判断,出现忙碌情况的,也可以根据负载均衡的算法进行调度,遇到性能瓶颈也可以通过添加机器来保证性能

  • 可伸缩性

    当企业业务规模快速扩大,可以通过增加节点,提高系统的服务能力

  • 请求过滤

    提供过滤器的使用,过滤器可以根据判断来监测请求的合法或者对请求的流量进行限制,达到保护系统和提高响应能力的目的。

初识Ribbon

使用Ribbon 主要就是RestTemplate的使用

Ribbon概述

接口定义 Spring Bean Name 默认实现类 说明
IClientConfig ribbonClientConfig DefaultClientConfigImpl 客户端配置
IRule ribbonRule ZoneAvoidanceRule 负载均衡策略
IPing ribbonPing DummyPing 通过ping判断服务是否可用
ServerList ribbonServerList ConfigurationBasedServerList 服务实例清单
ServerListFilter ribbonServerListFilter ZonePreferenceServerListFilter 根据某些条件过滤后得到服务实例清单
ILoadBalancer ribbonLoadBalancer ZoneAwareLoadBalancer 负载均衡器。按照某种策略选取服务实例
ServerListUpdater ribbonServerListUpdater PollingServerListUpdater 根据一定策略更新服务实例清单

以上这些在Spring Boot中都是通过配置类RibbonClientConfiguration 来自定义装配的

实现负载均衡

一般通过在RestTemplate上加入一个@LoadBalanced,通过这个注解启动负载均衡,就像下面一样。

/**
 * 配置负载均衡实现
 * @return
 */
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
    return new RestTemplate();
}

Ribbon负载均衡学习_第1张图片

这里的serviceId指的是微服务的ID,根据一定的策略返回一个微服务的实例

public interface ServiceInstanceChooser {
    ServiceInstance choose(String serviceId);
}

LoadBalancerClient接口继承了ServiceInstanceChooser接口因此就有了choose方法

LoadBalancerClient还定义了三个方法

  • execute(String serviceId, LoadBalancerRequest request)

    通过serviceId找到具体的微服务,然后执行请求request

  • execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request)

    通过serviceId和serviceInstance服务实例找到具体的微服务,然后执行请求request

  • reconstructURI(ServiceInstance instance, URI original)

    根据当前的URI重构可用的URL

public interface LoadBalancerClient extends ServiceInstanceChooser {
     T execute(String serviceId, LoadBalancerRequest request) throws IOException;

     T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException;

    URI reconstructURI(ServiceInstance instance, URI original);
}

RibbonLoadBalancerClient中的execute(String serviceId, LoadBalancerRequest request)方法

@Override
public  T execute(String serviceId, LoadBalancerRequest request)
      throws IOException {
   return execute(serviceId, request, null);
}

public  T execute(String serviceId, LoadBalancerRequest request, Object hint)
      throws IOException {
    
   ILoadBalancer loadBalancer = getLoadBalancer(serviceId);  //负载均衡器
   Server server = getServer(loadBalancer, hint);           //获取具体的服务实例
   if (server == null) {									//判断
      throw new IllegalStateException("No instances available for " + serviceId);
   }
   RibbonServer ribbonServer = new RibbonServer(serviceId, server,     //包装为Ribbon服务的实例
         isSecure(server, serviceId),
         serverIntrospector(serviceId).getMetadata(server));

   return execute(serviceId, ribbonServer, request);     //调度另外一个execute方法执行请求
}

主要流程

  • 通过serviceId获取微服务的实例
  • 使用微服务的实例为参数调度另外一个execute方法

而在execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request)方法中则还会创建一个分析记录器对请求进行分析,最后调用请求返回的结果。

@Override
public  T execute(String serviceId, ServiceInstance serviceInstance,
      LoadBalancerRequest request) throws IOException {
   Server server = null;
   if (serviceInstance instanceof RibbonServer) {
      server = ((RibbonServer) serviceInstance).getServer();
   }
   if (server == null) {
      throw new IllegalStateException("No instances available for " + serviceId);
   }
	//创建一个分析记录器对请求进行分析
   RibbonLoadBalancerContext context = this.clientFactory
         .getLoadBalancerContext(serviceId);
   RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

   try {
   //将请求发送到具体的微服务
      T returnVal = request.apply(serviceInstance);
      //结果则同步到分析记录器中
      statsRecorder.recordStats(returnVal);
      //返回请求结果
      return returnVal;
   }
   // catch IOException and rethrow so RestTemplate behaves correctly
   catch (IOException ex) {
      statsRecorder.recordStats(ex);
      throw ex;
   }
   catch (Exception ex) {
      statsRecorder.recordStats(ex);
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}

Ribbon负载均衡器和策略

负载均衡主要包括两个部分

  • 负载均衡器
  • 负载均衡策略

负载均衡器

ILoadBalancer接口
public interface ILoadBalancer {

	//新增服务实例列表
   public void addServers(List newServers);
   
	//选择具体服务实例
   public Server chooseServer(Object key);
   
	//记录服务下线
   public void markServerDown(Server server);
   
	//获取具体的服务
   @Deprecated
   public List getServerList(boolean availableOnly);
	//获取可以访问且正常运行的服务实例
   public List getReachableServers();
	//获取服务的服务实例
   public List getAllServers();
}
BaseLoadBalancer的chooseServer
/*
 * Get the alive server dedicated to key
 * 
 * @return the dedicated server
 */
public Server chooseServer(Object key) {
	//计数器
    if (counter == null) {
        counter = createCounter();
    }
    //线程安全+1操作
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
        //路由策略获取服务
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}

每当执行一次chooseServer方法,计数器就加1,而默认的IRule接口的实现类用的是RoundRobinRule,采用轮询策略

ZoneAwareLoadBalancer默认的负载均衡器
@Override
public Server chooseServer(Object key) {
    if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
        logger.debug("Zone aware logic disabled or there is only one zone");
        return super.chooseServer(key);
    }
    Server server = null;
    try {
        LoadBalancerStats lbStats = getLoadBalancerStats();
        Map zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
        logger.debug("Zone snapshots: {}", zoneSnapshot);
        if (triggeringLoad == null) {
            triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
        }

        if (triggeringBlackoutPercentage == null) {
            triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
        }
        Set availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
        logger.debug("Available zones: {}", availableZones);
        if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
            String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
            logger.debug("Zone chosen: {}", zone);
            if (zone != null) {
                BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                server = zoneLoadBalancer.chooseServer(key);
            }
        }
    } catch (Exception e) {
        logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
    }
    if (server != null) {
        return server;
    } else {
        logger.debug("Zone avoidance logic is not invoked.");
        return super.chooseServer(key);
    }
}

这段代码是负载均衡器中的ZoneAwareLoadBalancer中的chooseServer,主要步骤

  • 判断是否开启了Zone的功能,没有Zone,或者Zone的数量只有1个,采用chooseServer来选择具体的服务

  • 按照负载均衡阀值来排除Zone,排除最高负载20%的Zone

    if (triggeringLoad == null) {
    triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
    “ZoneAwareNIWSDiscoveryLoadBalancer.” + this.getName() + “.triggeringLoadPerServerThreshold”, 0.2d);
    }

  • 按照故障率来排除Zone,排除大于99.99%的Zone

    if (triggeringBlackoutPercentage == null) {
    triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
    “ZoneAwareNIWSDiscoveryLoadBalancer.” + this.getName() + “.avoidZoneWithBlackoutPercetage”, 0.99999d);
    }

  • 以上都存在可用的Zone,采用随机算法获取Zone,选中以后再通过zoneLoadBalancer的chooseServer方法选择服务

  • 如果选择失败就通过BaseLoadBalancerchooseServer方法选择服务

负载均衡器通过一定的方法过滤服务实例,而保证微服务的性能。默认就是使用ZoneAwareLoadBalancer负载均衡器

而通过RibbonClientConfiguration客户端配置可以看到默认的负载均衡器ZoneAwareLoadBalancer和默认的负载均衡策略ZoneAvoidanceRule

//负载均衡策略

@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
   if (this.propertiesFactory.isSet(IRule.class, name)) {
      return this.propertiesFactory.get(IRule.class, config, name);
   }
   ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
   rule.initWithNiwsConfig(config);
   return rule;
}

//负载均衡器
	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList serverList, ServerListFilter serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

负载均衡策略

在对基本的BaseLoadBalancer负载均衡器学习的时候,有一个重要接口就是IRule,通过他提供的算法进行具体的负载均衡策略

public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}
默认的负载均衡器
策略类 说明 备注
BestAvailableRule 探测当前的服务是否可用,如果可用,就选择当前服务被分配最少请求的那个 逐个考察实例
AvailabilityFilteringRule 过滤掉那些被标记为tripped的服务实例,无法连接的服务实例,以及超过最大请求阀值的服务实例 Ribbon会调用AvailabilityPredicate处理相应的过滤业务
ResponseTimeWeightedRule 响应时间权重策略 已经废弃
WeightedResponseTimeRule 根据响应时间分配一个权重值weight,对于响应时间短的服务可以有更大的概率分配到请求,而响应时间长的服务实例则分配概率小一点 使用平均/百分比响应时间的规则为每个服务实例分配动态权重,Ribbon会在后台开启线程进行时间权重的计算
RetryRule 重试服务策略 在一个特定时间,如果当前服务不可用,则通过轮询来选定
RoundRobinRule 轮询选择服务 通过下标,轮询服务实例列表,从而选择一个服务
RandomRule 随机选择服务 通过随机数结合服务列表长度,随机选择服务
ZoneAvoidanceRule 复合判断实例坐在区域的性能和故障,选择合适的服务实例 判断实例是否可用,并且过滤那些负载较高的实例,选取对应的服务实例

ZoneAvoidanceRule是默认的选择策略

介绍四种常用的选择策略

RoundRobinRule

程序生成一个线程安全的整数,然后通过加一取模来确定一个下标获取服务,如果服务为空或者不可用,则重复循环这个过程,但是只循环十次,十次过后无法获取就返回空。

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) {
    //获取可用服务实例清单
        List reachableServers = lb.getReachableServers();
        //获取全部服务实例清单
        List 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. */
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        // Next.
        server = null;
    }

    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "
                + lb);
    }
    return server;
}
RetryRule

通过轮询进行查找服务,超时时间为500ms,选择一个可用的服务实例或者超时返回null

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)) {

      InterruptTask task = new InterruptTask(deadline
            - System.currentTimeMillis());
		//在线程终止前循环尝试获取可用服务实例
      while (!Thread.interrupted()) {
         answer = subRule.choose(key);

         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;
   }
}
WeightedResponseTimeRule

通过后台线程进行分析各个服务的响应时间

  • 通过服务的统计分析对象得到各个服务的平均统计时间,然后计算各个服务实例的平均响应时间总和
  • 计算权重,使用 至今为止的权重 + 总平均响应时间 - 服务平均响应时间

至今为止的权重指的是一个个服务调用平均响应时间的累计

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
        List currentWeights = accumulatedWeights;
        if (Thread.interrupted()) {
            return null;
        }
        List 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()) {
            server =  super.choose(getLoadBalancer(), key);
            if(server == null) {
                return server;
            }
        } else {
            // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
            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);
        }

        if (server == null) {
            /* Transient. */
            Thread.yield();
            continue;
        }

        if (server.isAlive()) {
            return (server);
        }

        // Next.
        server = null;
    }
    return server;
}
ZoneAvoidanceRule

先过滤后执行的过程

private static final Random random = new Random();

private CompositePredicate compositePredicate;

public ZoneAvoidanceRule() {
    super();
    ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
    AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
    compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}

创建了两个过滤断言,一个是Zone断言ZoneAvoidancePredicate,一个是可用性断言AvailabilityPredicate,然后组合起来

ZoneAvoidancePredicate目的是找到性能较差的Zone 将其排除在外

AvailabilityPredicate是确定服务是否被熔断,或者负载过大,没有这种情况就返回该情况

compositePredicate就是先使用ZoneAvoidancePredicate进行过滤,在使用AvailabilityPredicate进行过滤。

以上就是负载均衡策略。

你可能感兴趣的:(笔记,分布式,java)