spring-cloud学习笔记之负载均衡:spring-cloud-Ribbon 负载均衡器分析

spring-cloud学习笔记之负载均衡:spring-cloud-Ribbon 负载均衡器分析

  • spring-cloud学习笔记之负载均衡:spring-cloud-Ribbon 负载均衡器分析
    • 1、负载均衡器
      • 如何实现客户端负载均衡?

spring-cloud学习笔记之负载均衡:spring-cloud-Ribbon 负载均衡器分析

1、负载均衡器

​ spring-cloud中定义了LoadBalancerClient作为负载均衡器的通用接口。并针对Ribbon实现了RibbonLoadBalancerClient,但是在具体实现客户端负载均衡时,是通过Ribbon的ILoadBalancer接口实现的。

如何实现客户端负载均衡?

​ AbstractLoadBalancer是ILoadBalancer接口的一个抽象实现,在此抽喜爱那个类中定义了一个关于服务实例的非组枚举类ServerGroup,它包含三种不同类型。

  • ALL,所有服务实例。
  • STATUS_UP,正常服务的实例。
  • STATUS_NOT_UP,停止服务的实力。

​ 除此还实现了一个chooseServer()函数,这个函数通过调用接口中的chooseServer(Object key)实现,中参数key为null,表示在选择具体服务实例忽略key的条件判断。

​ 最后,还定义了两个抽象函数

  • getServerList(ServerGroup serverGroup):定义了根据非组类型来获取不同的服务实例的列表。
  • getLoadBalancerStats():定义了获取LoadBalancerStats对象的方法,LoadBalancerStats对象被用来存储负载均衡中各个服务树立当前的属性和统计信息,可以利用这些信息置顶负载均衡策略。

​ BaseLoadBalancer类是Ribbon负载均衡器的基础实现类,在该类中定义了黑多关于负载均衡器相关的基础内容。

  • 定义并维护了两个存储服务实例server对象类表。一个用于存储所有服务实例的清单,一个用于存储正常服务的实例清单。
  • 定义了用于存储负载均衡器个服务实例属性和统计信息的LoadBalancerStats对象。
  • 定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认为null,需要在构造时注入它的具体实现。
  • 定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了带泪中定义的静态内部类SerialPingStrategy实现按。查看其源码可以看出该策略采用线性便利平服务实例的方式实现检查。该策略在放IPing实现速度不理想或者server列表过大时,可能会影响系统性能,这个时候西药实现IPingStrategy接口并重写pingServers(IPing ping,Server[] servers)函数去扩展ping执行策略。
  • 定义了负载均衡的处理规则IRule对象,从BaseLoadBalancer中chooseServer(Object key)的实现源码可知,负载均衡器实际将服务实例选择任务委托给IRule实例中的choose函数实现,RoundRobinRule是实现了最基本且常用的线性负载均衡规则,可以作为初始化为IR了的实现对象。
  • 启动ping任务,在BaseLoadBalancer的默认构造函数中,会直接启动一个用于定时检查Server是否健康的任务,默认执行间隔是10秒。
  • 实现了ILoadBalancer接口定义的负载均衡器应具备以下系列的基本操作:
    • addServers(List newServers):向负载均衡器中增加新的服务实例列表。该实现将原本已经维护的所有服务实例清单allServerList和新传入的服务实例清单newServersList韩寒苏对NewList进行处理,在BaseLoadBalancer实现爱你的时候会使用新的类表覆盖旧的列表,通过setServersList函数的重写实现。
    • chooseServer(Object key):挑选一个具体的服务实例。
    • markServerDown(Server server):标记某个服务实例暂停服务
    • List getReachableServers():获取可用服务实例列表,由于BaseLoadBalancer中但读维护了一个正常服务的实例清单,所以直接返回即可。
    • List getAllServers(),获取所有的服务实例类表。由于BaseLoadBalancer单独维护了一个所有服务的实例清单,所以也直接返回他即可。

​ DynamicServerListLoadBalancer继承了BaseLoadBalancer类,是对BaseLoadBalancer基础负载均衡器的扩展,其中实现了服务实例清单在运行期的动态更新能力。同时脊背对服务实例请按的过滤功能,也就是说我们可以通过过滤器来选择性的获取一批服务实例清单。

  • ServerList 代表了一个具体的服务实例的扩展类。其中具有两个抽象方法:

    • public List getInitialListOfServers();用于获取初始化的服务实例清单

    • public List getUpdatedListOfServers(); 用于获取更新的服务实例清单

    • 在这个负载均衡器中需要实现服务实例的动态更新,那么则需要Ribbon具备访问Eureka来获取服务实例的能力。通过搜索整合Ribbon与Eureka的包可以找到配置类EurekaRibbonClientConfiguration可以找到创建serverList的实例源代码:创建了一个DomainExtractingServerList实例,从它的源码可以找到他的内部还定义了一个ServerList ,同时这个类中对getInitialListOfServers和getUpdatedListOfServers的具体实现,其实委托给了内部定义的ServerList 对象,而该对象通过创建DomainExtractingServerList时,由构造函数传入DiscoveryEnabledNIWSServerList实现的。而DiscoveryEnabledNIWSServerList通过类方法中的一个私有函数obtainServersViaDiscovery()通过服务发现机制来实现发现服务实例的获取的。obtainServersViaDiscovery的实现逻辑如下:主要是依靠eurekaClient从服务注册中心获取具体的服务实例InstanceInfo列表,然后对这些服务实例进行遍历将状态为UP的实力转换成DiscoveryEnabledServer对象,最后将这些实例组织成类表返回。源码:

      private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
              List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
      
              if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
                  logger.warn("EurekaClient has not been initialized yet, returning an empty list");
                  return new ArrayList<DiscoveryEnabledServer>();
              }
      
              EurekaClient eurekaClient = eurekaClientProvider.get();
              if (vipAddresses!=null){
                  for (String vipAddress : vipAddresses.split(",")) {
                      // if targetRegion is null, it will be interpreted as the same region of client
                      List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                      for (InstanceInfo ii : listOfInstanceInfo) {
                          if (ii.getStatus().equals(InstanceStatus.UP)) {
      
                              if(shouldUseOverridePort){
                                  if(logger.isDebugEnabled()){
                                      logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                                  }
      
                                  // copy is necessary since the InstanceInfo builder just uses the original reference,
                                  // and we don't want to corrupt the global eureka copy of the object which may be
                                  // used by other clients in our system
                                  InstanceInfo copy = new InstanceInfo(ii);
      
                                  if(isSecure){
                                      ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                                  }else{
                                      ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                                  }
                              }
      
                              DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
                              serverList.add(des);
                          }
                      }
                      if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                          break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
                      }
                  }
              }
              return serverList;
          }
      

      在DiscoveryEnabledNIWSServerList中通过EurekaClient从服务注册中心获取到最新的服务实例清单后,返回list到DomainExtractingServerList类中,将继续通过setZones函数进行处理,setZones()函数将list类表中的元素,转换成内部定义的DiscoveryEnabledServer的子类对象DomainExtractingServer,在该对象爱你个的构造函数将为服务实例对象设置一些必要的属性信息。

      private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
      		List<DiscoveryEnabledServer> result = new ArrayList<>();
      		boolean isSecure = this.ribbon.isSecure(true);
      		boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
      		for (DiscoveryEnabledServer server : servers) {
      			result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr,
      					this.approximateZoneFromHostname));
      		}
      		return result;
      	}
      
  • ServerListUpdater:Ribbon和Eureka整合后,这个对象实现的是ServerList的更新–服务更新器。

    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
            @Override
            public void doUpdate() {
                updateListOfServers();
            }
        };
    
    

    源码可以看出其内部定义了UpdateAction接口,UpdateAction()以匿名内部类的方式创建了一个具体实现,其中doUpdate实现的内容就是对ServerList的具体更新操作,除此之外,ServerListUpdater中还定义了一系列控制它和获取他的信息的操作。

    public interface ServerListUpdater {
        public interface UpdateAction {
            void doUpdate();
        }
        //qidongfuwu gengxin
        void start(UpdateAction updateAction);
        void stop();
        String getLastUpdate();
        long getDurationSinceLastUpdateMs();
        int getNumberMissedCycles();
        int getCoreThreads();
    }
    

    ServerListUpdater的实现类主要有:EurekaNotificationServerListUpdater和PollingServerListUpdater两个实现类

    作用:

    • PollingServerListUpdater 负载均衡器中默认实现,通过定时任务的方式进行服务类表的更新。
    • EurekaNotificationServerListUpdater 这个更新器也可以服务于DynamicServerListLoadBalancer负载均衡器,但他的触发机制与PollingServerListUpdater 不同,他是利用Rureka的时间监听器来驱动服务类表的更新操作。

    默认实现:PollingServerListUpdater ,它首先创建一个Runnable的线程实现,调用updateAction。doUpdate(),最后在为这个Runnable线程启动类一个定时任务来执行。其中用于的启动定时任务的两个重要参数initialDelayMs、refreshIntervalMs默认定义非别为1000和30*1000毫秒。就是说更新服务实例在初始化延迟1秒后执行,周期为30秒重复执行,还有一些其他参数如最后更新实际那,是否存活等信息,同时实现了serverListUpdater中定义的一些其他操作内容。

  • ServerListFilter:在DynamicServerListLoadBalancer中实际实现的委托给了updateListOfServer函数。在获取服务可用实例的类表后引入了新的filter—ServerListFilter。这个接口中只有一个 List getFilteredListOfServers(List servers);抽象方法,主要用于实现对服务实例类表的过滤,通过传入的服务实例清单,根据一些规则返回过滤后的服务实例清单,[外链图片转存失败(img-ynupHTIk-1566362733553)(C:\Users\79182\AppData\Roaming\Typora\typora-user-images\1563511153283.png)]其中除了ZonePreferenceServerListFilter的实现是spring-cloud-Ribbon中对Netflix Ribbon的扩展实现外,其他均是Netflix Ribbon的原生实现类。

    • AbstractServerListFilter:这是一个抽象锅炉汽在这里定义了过滤时需要的一个重要根据对象LoadBanlancerStats。这个对象存储了关于负载均衡器的一些属性和统计信息。
    • ZonePreferenceServerListFilter:这个锅炉去基于区域感知的方式实现服务实例的过滤,也就是说他会优先使用同处于一个区域的实例。
    • DefaultNIWSServerListFilter:完全继承自ZoneAffinityServerListFilter,默认的NIWS过滤器
    • ServerListSubsetFilter:也继承自ZoneAffinityServerListFilter,它是用于拥有打过莫服务器集群的系统,它可以产生一个区域感知结果的自己列表。同时他还能够通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择行从服务实例类表中提出哪些相对不够健康的实例,实现分为三步:
      • 1、获取区域感知的锅炉结果,作为候选的服务实例清单。
      • 2、从当前想飞这维护的服务实例自己中提出相对不够健康的实例。
      • 3、完成剔除后清单少了至少10%默认的服务实例。最后通过随机的方式从候选清单中选出一批实例加入到清单中,以保持服务实例自己与原来的数量一致。默认的实例自己数量为20
    • ZonePreferenceServerListFilter:spring cloud整合是新增的过滤器,使用spring-丑陋的整合eureka和ribbon是默认使用的过滤器,他实现了通过配置eureka实例原数组的躲在区域来锅炉除同区域的服务实例。
  • ZoneAwareLoadBalancer负载均衡器是对DynamicServerListLoadBalancer的扩展,在DynamicServerListLoadBalancer中并没有腹泻选择具体服务实例的chooseServer函数,所以依旧采用BaseLoadBelancer中实现的算法。使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例,这个算法实现了简单并没有区域的概念,所以他会把所有实例视为一个Zone下的节点来看待,这样就会周期性的产生跨区域访问的情况,就会产生更高的延迟,会有一定的性能问题。而在ZoneAwareLoadBalancer中,没有复写setServersList说明实现服务实例清单的更新主逻辑没有修改,但是我们可以发现他复写了这个函数setServerListForZones根据区域Zone分组的实例列表,每一个区域对应着一个ZoneStats,用于存储每个Zone的一些状态和统计信息。源码:

     @Override
        protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
            super.setServerListForZones(zoneServersMap);
            if (balancers == null) {
                balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
            }
            for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
            	String zone = entry.getKey().toLowerCase();
                getLoadBalancer(zone).setServersList(entry.getValue());
            }
            // check if there is any zone that no longer has a server
            // and set the list to empty so that the zone related metrics does not
            // contain stale data
            for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
                if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
                    existingLBEntry.getValue().setServersList(Collections.emptyList());
                }
            }
        }    
    

    源码可以看出:在实现中创建了一个ConcurrentHashMap类型的balancers,用来存储每个Zone区域对应的负载均衡器,二聚体的负载均衡器的创建则通过在下面的第一个循环中getLoadBalancer来实现,同时创建的时候会创建它的规则,创建完成后调用setServersList函数为其设置对应的Zone区域的实例清单,第二个循环则是对Zone区域中的。实例清单进行检查,检查是否有Zone区域下已经没有实例了,是的话就将balancers中对应的Zone区域的实例列表清空,作用主要是为了后续选择节点时,防止过时的zone区域统计信息干扰具体实例的选择算法。

    选择实例:

    @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<String, ZoneSnapshot> 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<String> 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);
            }
        }
    

    只有当负载均衡器中维护的实例所属的zone区域的个数大于1的时候才会执行这里的选择策略,否则还是使用父类的实现。当zone区域的个数大于1的时候,实现的步骤为:

    • 调用ZoneAvoidanceRule的静态方法createSnapshot,为当前负载均衡器中所有的zone区域非别创建快照,保存在 Map zoneSnapshot中用于后续的算法。
    • 调用ZoneAvoidanceRule的静态方法getAvailableZones湖区可用的zone区域集合,在此函数中会通过zone区域快照中的统计数据实现可用区域的挑选。
      • 1、提出符合以下规则的区域:所属实例数为0的区域;区域内实例平均负载小于0,或者实例故障率大于等于阈值默认是0.99999
      • 根据区域的实例平均负载计算出最差的区域,这里最差指的是实例平均负载最高的区域
      • 如果上面过程没有符合提出条件的区域,同时实例最大平均负载小于阈值(默认20%),就直接返回所有的zone区域为可用区域,否则从最坏区域集合中随机选择一个,将他移除。
    • 当获得的可用区域集合不为空,并且小于区域总数,随机选择一个区域
    • 确定某个区域后,获取对应区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口的choose函数来选择具体的服务实例,这里IRule接口的实现会使用ZoneAvoidanceRule来挑选出具体的服务实例。

你可能感兴趣的