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

软件工程的目标是控制复杂度,而不是增加复杂性。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

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

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

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

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


正文

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


DynamicServerListLoadBalancer

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


成员属性

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方法,在初始化时进行调用。

DynamicServerListLoadBalancer:

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

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


updateListOfServers()更新服务列表

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

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()方法完成初始化。此处需要注意的是,本子类复写了此初始化方法:
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,就会延迟较大,很容易造成超时

成员属性

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这两个属性:

DynamicServerListLoadBalancer:

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

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

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实例的???

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

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来使用,请注意划分职责边界~
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览