【你好Ribbon】十五:Ribbon具有区域识别能力的负载均衡器-ZoneAwareLoadBalancer

前言

上一节我们介绍了Ribbon的一个功能很强大的负载均衡器集成了Ribbon的五大组件的能力,其实有了它基本就能满足我们平时的需求了。但是Netflix开发人员继续发扬精益求精的代码精神 又默默为我们奉献了一个更高级的负载均衡器ZoneAwareLoadBalancer。从名字可以看出是一个具备区域意识的负载均衡器。例如当前的服务自华南、华北、华东、西北四个区域那么ZoneAwareLoadBalancer会过滤掉负载较高或者几乎不可用的区域。

回顾 ZoneAvoidanceRule.getAvailableZones

因为ZoneAwareLoadBalancer 本身代码比较简单,主要的逻辑是基于ZoneAvoidanceRule 所以这里有必要对它进行回顾。这里可以直达之前的文章。
【你好Ribbon】十一:Ribbon负载均衡服务选取规则组件IRule-客户端可配置规则ClientConfigEnabledRoundRobinRule

简单对ZoneAvoidanceRule的静态方法getAvailableZones()做一个回顾(也可点击上面连接看更详细介绍):

public static Set<String> getAvailableZones(
            Map<String, ZoneSnapshot> snapshot, double triggeringLoad,
            double triggeringBlackoutPercentage) {
  if (snapshot.isEmpty()) {
       return null;
   }
   Set<String> availableZones = new HashSet<String>(snapshot.keySet());
   /**如果只有一个zone 直接返回不需要判断*/
   if (availableZones.size() == 1) {
       return availableZones;
   }
   /**保存最坏的zone*/
   Set<String> worstZones = new HashSet<String>();
   /**所有的zone中平均负载最高值*/
   double maxLoadPerServer = 0;
   /**是否有一部分可用的负载 false时意为全部可用 true的时候有限可用*/
   boolean limitedZoneAvailability = false;
   for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
       String zone = zoneEntry.getKey();
       ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
       int instanceCount = zoneSnapshot.getInstanceCount();
       /**如果zone内没有可用实例 从可用的zone中移除 将部分可用赋值为true*/
       if (instanceCount == 0) {
           availableZones.remove(zone);
           limitedZoneAvailability = true;
       } else {
           double loadPerServer = zoneSnapshot.getLoadPerServer();
           /**如果熔断率大于传过来的阈值 或者实例的平均负载小于0 也从可用的zone中移除
            * 前半部分条件:熔断实例数/总实例数 >= 阈值(阈值为0.99999d) 也就差不多是所有的server都被
            *             熔断了  该zone才不可用
            * 后半部分条件:loadPerServer < 0 这里是什么意思?什么情况下loadPerServer才会小于0 那就要
            *            去看看 LoadBalancerStats#getZoneSnapshot()
            *            if (circuitBreakerTrippedCount == instanceCount)
            *               loadPerServer = -1
            *           也就是所有的实例都熔断了 那么loadPerServer久没有任何意义了 所以就赋值为-1
            * 那前半部分的条件和后半部分岂不是一样的?前半部分是可以配置的 通过下面
            * ZoneAwareNIWSDiscoveryLoadBalancer.clientName.avoidZoneWithBlackoutPercetage 默认是0.99999d
            * */
           if (((double) zoneSnapshot.getCircuitTrippedCount())
                   / instanceCount >= triggeringBlackoutPercentage
                   || loadPerServer < 0) {
               availableZones.remove(zone);
               limitedZoneAvailability = true;
           } else {
               /**当前的平均负载和上一次最大的负载一样大 那就将当前的区域加入 最坏负载集合
                * 这样worstZones 就会有多个值  一般情况下worstZones这个集合会出现一个值
                * 但是如果有两个区域负载相同时 就会有2个值 以此类推
                * */
               if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
                   // they are the same considering double calculation
                   // round error
                   worstZones.add(zone);
               } else if (loadPerServer > maxLoadPerServer) {
                   /**如果当前负载大于上一次的最大负载 就保存当前负载的区域 并且交换最大负载的值*/
                   maxLoadPerServer = loadPerServer;
                   worstZones.clear();
                   worstZones.add(zone);
               }
           }
       }
   }

   /**如果全部区域都可用 并且最大负载没达到阈值 返回全部的可用区域
    * triggeringLoad 负载阈值 默认值为0.2 通过
    *   ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold来设置
    * 请求量/实例数 = 平均负载阈值  默认值为0.2有点不合理 加入一个区域10台服务器 有两个活跃的请求
    * 这个zone的平均负载就是0.2。超过两个请求进来就会负载过重。
    *
    * 这么设置的后果:假如有两个zone区域 总会有一个被移除掉 会导致负载均衡策略完全失效。
    * (对于这个结论我们可以在AvailableZonesTest中模拟)
    *
    * 所以生产环境如果有多个zone区域的话 建议不要使用默认值
    * ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold = 50
    * */
   if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
       // zone override is not needed here
       return availableZones;
   }
   /**
    * 这一步是吧最坏的负载区域移除掉 worstZones这个集合上面说了 一般会是一个值

 - List item

    * 当出现最大负载有多个区域时 才会有多个值
    *
    * 程序走到这里 availableZones肯定是不止一个zone的。因为在最开始 就进行了判断
    * availableZones 如果仅有一个值 直接返回了。
    * */
   String zoneToAvoid = randomChooseZone(snapshot, worstZones);
   if (zoneToAvoid != null) {
       availableZones.remove(zoneToAvoid);
   }
   return availableZones;

}

以上是选择可用区域的逻辑比较重要。代码比注释较多 需要用心看。
下面简要的回顾一下选择可用区域的逻辑:

两个配置:

  • ZoneAwareNIWSDiscoveryLoadBalancer.<clientName>.triggeringLoadPerServerThreshold 平均负载阈值。默认0.2这个值我们上面也说了不合理
  • ZoneAwareNIWSDiscoveryLoadBalancer.<clientName>.avoidZoneWithBlackoutPercetage 区域熔断率 默认0.99999d 相当于是要全部熔断才会触发该值。

触发移除zone的条件:

  • zone没有服务器
  • 同一区域的 熔断数 / 实例总数 > 0.9999d 可配置
  • 所有的服务器都熔断了
  • 如果所有区域的最大平均负载大于 配置的平均负载值0.2 随机从worstZones移除一个区域

chooseServer

 @Override
    public Server chooseServer(Object key) {
        //ENABLED是一个开关 如果没有开启  或者区域数量小于等于1 就不使用该负载均衡器使用DynamicServerListLoadBalancer
        //这也是SpringCloud选择它的原因
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            return super.chooseServer(key);
        }
        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            //从LoadBalancerStats获取区域的快照信息
            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());
            //这里一个判断要注意 如果可用区域没有任何问题 也就是可用区域里面的服务都很正常。
            //使用父类的chooseServer 也就是BaseLoadBalancer的chooseServer方法
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
            //随机选择一个区域
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                if (zone != null) {
                    //获取当前区域对应的负载均衡 
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
        }
        if (server != null) {
            return server;
        } else {
            return super.chooseServer(key);
        }
    }
  • ZoneAwareNIWSDiscoveryLoadBalancer.enabled 该属性来控制是否启用ZoneAwareLoadBalancer
  • ZoneAvoidanceRule.createSnapshot 通过LoadBalancerStats来获取区域对应的快照信息
  • ZoneAvoidanceRule.getAvailableZones 获取可用的区域
  • ZoneAvoidanceRule.randomChooseZone 随机获取一个可用的区域
  • BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone) 获取对应区域的负载均衡器 这里ZoneAwareLoadBalancer是为每一个zone分配一个ILoadBalancer

为每一个Zone分配一个ILoadBalancer


//该方法是父类更新服务列表的时候调用的
  protected void setServerListForZones(
            Map<String, List<Server>> zoneServersMap) {
        LOGGER.debug("Setting server list for zones: {}", zoneServersMap);
        getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
    }

//该方法是子类对setServerListForZones的重写
 @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());
      }
      for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
          if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
              existingLBEntry.getValue().setServersList(Collections.emptyList());
          }
      }
  }  

setServerListForZones 这个方法是重写父类的方法,除了向LoadBalancerStats中设置区域服务列表映射之外 还为每一个Zone分配了一个ILoadBalancer

看一下ZoneAwareLoadBalancer如何为每一个Zone指定一个ILoadBalancer的。进入上面的getLoadBalancer(zone)方法:

 BaseLoadBalancer getLoadBalancer(String zone) {
    zone = zone.toLowerCase();
    //从缓存中获取对应的负载均衡器
    BaseLoadBalancer loadBalancer = balancers.get(zone);
    if (loadBalancer == null) {
        //赋值规则 如果当前规则不存在使用 AvailabilityFilteringRule
    	IRule rule = cloneRule(this.getRule());
    	//实例化一个 BaseLoadBalancer
        loadBalancer = new BaseLoadBalancer(this.getName() + "_" + zone, rule, this.getLoadBalancerStats());
        BaseLoadBalancer prev = balancers.putIfAbsent(zone, loadBalancer);
        if (prev != null) {
        	loadBalancer = prev;
        }
    } 
    return loadBalancer;        
}

上面代码是一个创建BaseLoadBalancer并加入缓存的一个过程。IRule使用的是当前设置的,如果没有设置使用AvailabilityFilteringRule。那为什么要创建一个BaseLoadBalancer作为zone的默认的负载均衡器,其实这里只是使用BaseLoadBalancerchooseServer的能力,而这个能力DynamicServerListLoadBalancer并没有对BaseLoadBalancer进行扩展。

在这里插入图片描述

示例

自定义ServerList

static class ServerListForHardCode implements ServerList<Server>{
    static List<Server> allServer = new ArrayList<>();
    static {
      allServer.add(createServer("HN" , 1));
      allServer.add(createServer("HN" , 2));
      allServer.add(createServer("HB" , 1));
      allServer.add(createServer("HB" , 2));
    }
    @Override
    public List<Server> getInitialListOfServers() {
      return allServer;
    }
    @Override
    public List<Server> getUpdatedListOfServers() {
      return allServer;
    }
    private static Server createServer(String zone, int index) {
      Server server = new Server("www.baidu" + zone + ".com", index);
      server.setZone(zone);
      return server;
    }
  }

模拟请求让增加服务器的负载

  private void request(ZoneAwareLoadBalancer zoneAwareLoadBalancer){
    for (int i = 0; i < 3; i++) {
      new Thread(()->{
        while (true){
          LoadBalancerStats loadBalancerStats = zoneAwareLoadBalancer.getLoadBalancerStats();
          zoneAwareLoadBalancer.getAllServers().forEach(server -> {
            ServerStats serverStats = loadBalancerStats.getServerStats(server);
            serverStats.incrementActiveRequestsCount();
            serverStats.incrementNumRequests();
            long rt = randomTime(500);
            sleep(rt);
            // 请求结束, 记录响应耗时
            serverStats.noteResponseTime(rt);
            serverStats.decrementActiveRequestsCount();
          });
        }
      }).start();
    }
  }

测试 AwareLoadBalaner

  @Test
  public void zoneAwareLoadBalaner(){
    IClientConfig config = DefaultClientConfigImpl.getClientConfigWithDefaultValues("coredy");
    IPing ping  = new DummyPing();
    ServerListFilter filter = new ZoneAffinityServerListFilter();
    IRule rule = new ZoneAvoidanceRule();
    ServerListUpdater updater = new PollingServerListUpdater();
    ServerList serverList = new ServerListForHardCode();
    ZoneAwareLoadBalancer zoneAwareLoadBalancer =
        new ZoneAwareLoadBalancer(config , rule , ping , serverList ,filter ,  updater);
    //模拟请求 让服务有一定的负载    
    request(zoneAwareLoadBalancer);
    while (true){
      sleep(1000);
      System.out.println(zoneAwareLoadBalancer.chooseServer(null));
    }
  }

打印结果:

www.baiduHB.com:2
hn 平均负载:0.0
hb 平均负载:1.5
计算之后的可用区域:[hn]
www.baiduHN.com:2
hn 平均负载:1.5
hb 平均负载:0.0
计算之后的可用区域:[hb]
www.baiduHB.com:1
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:2

上面模拟3个线程不停的向服务器发起请求,可以看到上面的结果 计算出的可用区域永远是 一个。因为我们的区域负载很容易就超过了阈值 0.2所以一般情况下都是会过滤掉一个区域的。这里应该是默认配置设置的不合理造成的,所以我们在生产环境可以自定义该配置

结语

ZoneAwareLoadBalancer主要就是对zone具有识别能力。可以过滤掉熔断较高 负载较高的区域。它是SpringCloud默认的负载均衡器。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值