前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 五十七、Ribbon负载均衡器ILoadBalancer(二):ZoneAwareLoadBalancer具备区域意识、动态服务列表的负载均衡器

[享学Netflix] 五十七、Ribbon负载均衡器ILoadBalancer(二):ZoneAwareLoadBalancer具备区域意识、动态服务列表的负载均衡器

作者头像
YourBatman
发布2020-03-24 10:35:42
3.4K0
发布2020-03-24 10:35:42
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

代码下载地址:https://github.com/f641385712/netflix-learnin

前言

上文介绍了负载均衡器ILoadBalancer的基本内容,并且详述了基本实现:BaseLoadBalancer。它实现了作为ILoadBalancer负载均衡器的基本功能,比如:服务列表维护、服务定时探活、负载均衡选择Server等。

但是BaseLoadBalancer虽完成了基本功能,但还稍显捡漏,无法应对一些复杂场景如:

  • 动态服务器列表(因为可用Server可能会重启、停机、新增等,希望被动态发现)
  • Server过滤(因为某台Server可能负载偏高、已被熔断,此时希望此些Server被过滤掉)
  • zone区域意识(服务之间的调用希望尽量是同区域进行的,减少延迟)

本文将继续介绍ILoadBalancer的实现,它们均是BaseLoadBalancer的子类,在此技术上增强功能,应对如上复杂场景。


正文

本文介绍的是在面试中会问、工作中实际会用到的两个负载均衡器实现。在这之前,我希望你已经掌握了如下内容:


DynamicServerListLoadBalancer

它是BaseLoadBalancer子类,具有动态源获取服务器列表的功能。即服务器列表在运行时可能会更改,此外,它还包含一些工具,其中包含服务器列表可以通过筛选条件过滤掉不需要的服务器。


成员属性
代码语言:javascript
复制
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {

	// 这两个属性木有任何用处,也不知道是不是开发人员忘记删了  哈哈
	boolean isSecure = false;
	boolean useTunnel = false;
	
    protected AtomicBoolean serverListUpdateInProgress = new AtomicBoolean(false);
    volatile ServerList<T> serverListImpl;
    volatile ServerListFilter<T> filter;
    protected final ServerListUpdater.UpdateAction updateAction = () -> updateListOfServers();
    protected volatile ServerListUpdater serverListUpdater;
}
  • serverListUpdateInProgress:跟踪服务器列表的修改,当正在修改列表时,赋值为true,放置多个线程重复去操作,有点上锁的意思
  • serverListImpl:提供服务列表。默认实现是ConfigurationBasedServerList,也就是Server列表来自于配置文件,如:account.ribbon.listOfServers = xxx,xxx
    • 具体实现类可以通过key:NIWSServerListClassName来配置,比如你的自定义实现。当然你也可以通过set方法/构造器初始化时指定
    • ribbon下默认使用的ConfigurationBasedServerList,但是eureka环境下默认给你配置的是DomainExtractingServerList(详见EurekaRibbonClientConfiguration
  • filter:对ServerList执行过滤。默认使用的ZoneAffinityServerListFilter,可以通过key:NIWSServerListFilterClassName来配置指定
  • updateAction:执行更新动作的action:updateListOfServers()更新所有
  • serverListUpdater:更新器。默认使用PollingServerListUpdater 30轮询一次的更新方式,当然你可以通过ServerListUpdaterClassName这个key自己去指定。当然set/构造器传入进来也是可以的

可以看到,ILoadBalancer管理的五大核心组件至此全部齐活

  • 父类BaseLoadBalancer管理2个:IPing、IRule。负责了Server isAlive的探活,负责了负载均衡算法选择Server
  • 子类DynamicServerListLoadBalancer管理3个:ServerList、ServerListFilter、ServerListUpdater负责动态管理、更新服务列表

初始化方法

它有个restOfInit方法,在初始化时进行调用。

代码语言:javascript
复制
DynamicServerListLoadBalancer:

    void restOfInit(IClientConfig clientConfig) {
    	...
        updateListOfServers();
        // ~~~~~update之后马上进行首次连接~~~~~
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections().primeConnections(getReachableServers());
        }
        ...
    }

该初始化方法会在其它初始化事项完成后(如给各属性赋值)执行:对当前的Server列表进行初始化、更新。


updateListOfServers()更新服务列表

该方法是维护动态列表的核心方法,它在初始化的时候便会调用一次,后续作为一个ServerListUpdater.UpdateAction动作每30s便会执行一次。

代码语言:javascript
复制
DynamicServerListLoadBalancer:

    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            servers = serverListImpl.getUpdatedListOfServers();
            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
            }
        }
        updateAllServerList(servers);
    }
	
	// 该方法作用:能保证同一时间只有一个线程可以进来做修改动作~~~
	// 把准备好的Servers更新到列表里。该方法是protected,子类并无复写
	// 但是,但是,但是setServersList()方法子类是有复写的哦
    protected void updateAllServerList(List<T> ls) {
        if (serverListUpdateInProgress.compareAndSet(false, true)) {
            try {
            	// 显示标注Server均是活的(至于真活假活交给forceQuickPing()去判断)
                for (T s : ls) {
                    s.setAlive(true); 
                }
                setServersList(ls);
                super.forceQuickPing();
            } finally {
                serverListUpdateInProgress.set(false);
            }
        }
    }

逻辑简单,描述如下:

  1. ServerList拿到所有的Server们(如从配置文件中读取、从配置中心里拉取)
  2. 经过ServerListFilter过滤一把:如过滤掉zone负载过高的 / Server负载过高或者已经熔断了的Server
  3. 经过1、2后生效的就是有效的Servers了,交给setServersList()方法完成初始化。此处需要注意的是,本子类复写了此初始化方法:
代码语言:javascript
复制
DynamicServerListLoadBalancer:

    @Override
    public void setServersList(List lsrv) {
        super.setServersList(lsrv);
        List<T> serverList = (List<T>) lsrv;
        Map<String, List<Server>> serversInZones = new HashMap<String, List<Server>>();
        for (Server server : serverList) {
        	// 调用这句的作用:确保初始化的时候就把ServerStats创建好
            getLoadBalancerStats().getSingleServerStat(server);

			// 把Server们按照zone进行分类,最终放到LoadBalancerStats.upServerListZoneMap属性里面去
            String zone = server.getZone();
            if (zone != null) {
                zone = zone.toLowerCase();
                List<Server> servers = serversInZones.get(zone);
                if (servers == null) {
                    servers = new ArrayList<Server>();
                    serversInZones.put(zone, servers);
                }
                servers.add(server);
            }
        }
        // 该方法父类实现很简单:getLoadBalancerStats().updateZoneServerMapping(zoneServersMap)
        //子类有复写哦
        setServerListForZones(serversInZones);
    }

在父类super.setServersList(lsrv)初始化的基础上,完成了对LoadBalancerStats、ServerStats的初始化。

可是,你是否有疑问,为毛这段初始化逻辑不放在父类上呢??? 解答:父类BaseLoadBalancer的服务列表是静态的,一旦设置上去将不会再根据负载情况、熔断情况等Stats动态的去做移除等操作,所以放在父类上并无意义。


ZoneAwareLoadBalancer

它是最强王者:具有zone区域意识的负载均衡器。它是Spring Cloud默认的负载均衡器,是对DynamicServerListLoadBalancer的扩展。

ZoneAwareLoadBalancer的出现主要是为了弥补DynamicServerListLoadBalancer的不足:

  • DynamicServerListLoadBalancer木有重写chooseServer()方法,所以它的负载均衡算法依旧是BaseLoadBalancer中默认的线性轮询(所有Server没区分概念,一视同仁,所以有可能这次请求打到区域A,下次去了区域B了~)
  • 这样如果出现跨区域调用时,就会产生高延迟。比如你华北区域的服务A调用华南区域的服务B,就会延迟较大,很容易造成超时

成员属性
代码语言:javascript
复制
ublic class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {

	private static final DynamicBooleanProperty ENABLED = DynamicPropertyFactory.getInstance().getBooleanProperty("ZoneAwareNIWSDiscoveryLoadBalancer.enabled", true);
	private ConcurrentHashMap<String, BaseLoadBalancer> balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
    private volatile DynamicDoubleProperty triggeringLoad;
    private volatile DynamicDoubleProperty triggeringBlackoutPercentage; 
}
  • ENABLED:是否启用区域意识的choose选择Server。默认是true,你可以通过配置ZoneAwareNIWSDiscoveryLoadBalancer.enabled=false来禁用它,如果你只有一个zone区域的话
    • 注意这是配置,并不是IClientConfigKey哦~
  • balancers:缓存zone对应的负载均衡器。每个zone都可以有自己的负载均衡器,从而可以有自己的IRule负载均衡策略~
    • 这个很重要:它能保证zone之间的负载策略隔离,从而具有更好的负载均衡效果
  • triggeringLoad/triggeringBlackoutPercentage:正两个参数讲解的次数太多遍了,请参考ZoneAvoidancePredicate

改进方案setServerListForZones()

实际生产环境中,但凡稍微大点的应用,跨区域部署几乎是必然的。因此ZoneAwareLoadBalancer重写了setServerListForZones()方法。

该方法在其父类DynamicServerListLoadBalancer的中仅仅是根据zone进入了分组,赋值了Map<String, List<? extends Server>> upServerListZoneMapMap<String, ZoneStats> zoneStatsMap这两个属性:

代码语言:javascript
复制
DynamicServerListLoadBalancer:

    protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
        getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
    }

而本类的该方法实现如下:

代码语言:javascript
复制
ZoneAwareLoadBalancer:

    @Override
    protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
    	// 完成父类的赋值~~~~~
        super.setServerListForZones(zoneServersMap);
        if (balancers == null) {
            balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
        }
        
        // getLoadBalancer(zone)的意思是获得指定zone所属的负载均衡器
        // 这个意思是给每个LB设置它所管理的服务列表
        for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
        	String zone = entry.getKey().toLowerCase();
            getLoadBalancer(zone).setServersList(entry.getValue());
        }
        
		// 这一步属一个小优化:若指定zone不存在了(木有机器了),就把balancers对应zone的机器置空
        for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
            if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
                existingLBEntry.getValue().setServersList(Collections.emptyList());
            }
        }
    }

除了像父类一样完成相关属性的初始化、赋值外,它还做了两件事:

  • 调用getLoadBalancer方法来创建负载均衡器为每个zone创建一个LB实例(这是本子类最大增强),并且拥有自己独立的IRule负载均衡策略
  • 如果对应的Zone下已经没有实例了,则将Zone区域的实例列表清空,防止zone节点选择时出现异常
    • 该操作的作用是为了后续选择节点时,防止过多的Zone区域统计信息干扰具体实例的选择算法

那么,它是如何给每个zone创建一个LB实例的???

代码语言:javascript
复制
ZoneAwareLoadBalancer:

    BaseLoadBalancer getLoadBalancer(String zone) {
        zone = zone.toLowerCase();
        BaseLoadBalancer loadBalancer = balancers.get(zone);
        if (loadBalancer == null) {
        	// ~~~~~~~~为每个zone都指定一个独立的IRule实例~~~~~~~~
        	IRule rule = cloneRule(this.getRule());
            loadBalancer = new BaseLoadBalancer(this.getName() + "_" + zone, rule, this.getLoadBalancerStats());
            BaseLoadBalancer prev = balancers.putIfAbsent(zone, loadBalancer);
            if (prev != null) {
            	loadBalancer = prev;
            }
        } 
        return loadBalancer; 
    }

这是一个default访问权限的方法:每个zone对应的LB实例是BaseLoadBalancer类型,使用的IRule是克隆当前的单独实例(因为规则要完全隔离开来,所以必须用单独实例~),这么一来每个zone内部的负载均衡算法就可以达到隔离,负载均衡效果更佳。


改进版的chooseServer()
代码语言:javascript
复制
ZoneAwareLoadBalancer:

    @Override
    public Server chooseServer(Object key) {
    	// 如果禁用了区域意识。或者只有一个zone,那就遵照父类逻辑
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            return super.chooseServer(key);
        }

        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
			...     		
			// 核心方法:根据triggeringLoad等阈值计算出可用区~~~~
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
			
				// 从可用区里随机选择一个区域(zone里面机器越多,被选中概率越大)
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    // 按照IRule从该zone内选择一台Server出来
                    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 {
        	// 回退到父类逻辑~~~兜底
            return super.chooseServer(key);
        }
    }

在已经掌握了ZoneAvoidanceRule#getAvailableZones、randomChooseZone以及ZoneAvoidancePredicate的工作原理后,对这部分代码的解读就非常的简单了:

  1. 若开启了区域意识,且zone的个数 > 1,就继续区域选择逻辑
  2. 根据ZoneAvoidanceRule.getAvailableZones()方法拿到可用区们(会T除掉完全不可用的区域们,以及可用但是负载最高的一个区域)
  3. 从可用区zone们中,通过ZoneAvoidanceRule.randomChooseZone随机选一个zone出来
    1. 该随机遵从权重规则:谁的zone里面Server数量最多,被选中的概率越大
  4. 在选中的zone里面的所有Server中,采用该zone对对应的Rule,进行choose

代码示例

略。


总结

关于Ribbon负载均衡器ILoadBalancer(二):ZoneAwareLoadBalancer就先介绍到这了,它是Ribbon的最强负载均衡器,也是Spring Cloud默认使用的负载均衡器,因此本文内容重要。

另外需要注意的是:本负载均衡器只是对zone进行了感知,能保证每个zone里面的负载均衡策略都是隔离的。但是,但是,但是:它并不能保证你A区域的请求一定会打到A区域的Server内,而这个事是由过滤器如ZonePreferenceServerListFilter/ZoneAffinityServerListFilter它们来完成的,它能过滤出和本地zone同一个zone的Servers来使用,请注意划分职责边界~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • DynamicServerListLoadBalancer
      • 成员属性
      • 初始化方法
      • updateListOfServers()更新服务列表
    • ZoneAwareLoadBalancer
      • 成员属性
      • 改进方案setServerListForZones()
      • 改进版的chooseServer()
    • 代码示例
    • 总结
    相关产品与服务
    负载均衡
    负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档