Spring Cloud系列(九)Spring Cloud Ribbon负载均衡器和负载均衡策略

上一节我们了解了虽然在Spring Cloud 中定义了LoadBalancerClient作为负载均衡器的通用接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它在具体实现客户端负载均衡的时候还是通过Ribbon的ILoadBalancer接口实现的,现在我们逐个了解ILoadBalancer接口的实现类。

AbstractLoadBalancer

AbstractLoadBalancer是ILoadBalancer接口的抽象实现。在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,它包含以下三种不同类型。

public enum ServerGroup{
        ALL,//所有服务实例
        STATUS_UP,//正常服务的实例
        STATUS_NOT_UP //停止服务的实例
    }

另外还实现了一个chooseServer()方法,该方法通过调用接口中的chooseServer(Object key)实现,其中参数key为null,表示在选择具体服务实例时忽略key的条件判断。最后还定义了两个抽象方法:

List<Server> getServerList(ServerGroup serverGroup):根据分组类型获取不同的服务实例列表

LoadBalancerStats getLoadBalancerStats():获取LoadBalancerStats 对象,LoadBalancerStats 对象被用来存储负载均衡器中各个服务实例当前的属性和统计信息。这些信息非常有用,我们可以利用这些信息来观察负载均衡的运行情况,同时这些信息也是制定负载均衡策略的重要依据。

BaseLoadBalancer

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

①定义并维护了两个存储服务实例Server的对象的列表。一个用于存储所有实例清单,一个用于存储正常服务的实例清单。

@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
           .synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
           .synchronizedList(new ArrayList<Server>());

②定义了刚刚提到的用来存储负载均衡各服务实例属性和统计信息的LoadBalancerStats对象。

③定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认认为null,需要在构造时注入它的具体实现。

protected IPing ping = null;

④定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了该类中定义的静态内部类SerialPingStrategy实现。根据源码,我们可以看到该策略采用线性遍历ping服务实例的方式实现检查。该策略在当IPing的实现速度不理想或者Server列表过大时可能会影响系统性能,这时需要通过实现IPingStrategy接口并重写pingServers(IPing ping,Server[] servers)方法去扩展ping的执行策略。

private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();

protected IPingStrategy pingStrategy = DEFAULT_PING_STRATEGY;
private static class SerialPingStrategy implements IPingStrategy {

        @Override
        public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];

            logger.debug("LoadBalancer:  PingTask executing [{}] servers configured", numCandidates);

            for (int i = 0; i < numCandidates; i++) {
                results[i] = false; /* Default answer is DEAD. */
                try {
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } catch (Exception e) {
                    logger.error("Exception while pinging Server: '{}'", servers[i], e);
                }
            }
            return results;
        }
    }

⑤定义了负载均衡的处理规则IRule对象,从BaseLoadBalancer中chooseServer(Object key)的实现源码,负载均衡器实际将服务实例选择任务委托给了IRule实例中的choose方法来实现,这里默认使用了RoundRobinRule为IRule的实现对象。RoundRobinRule实现了最基本且常用的线性负载均衡规则。

private final static IRule DEFAULT_RULE = new RoundRobinRule();
protected IRule rule = DEFAULT_RULE;
public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        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;
            }
        }
    }

⑥启动ping任务:在BaseLoadBalancer中的默认构造器中,会直接启动一个用于检查Server是否健康的任务,该任务默认的执行时间间隔为10s。

void setupPingTask() {
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
        forceQuickPing();
    }

⑦实现了ILoadBalancer接口中定义的负载均衡器应具备的以下基本操作。

1.addServers(List newServers):向负载均衡器中增加新的服务实例列表,该实现将原本维护着的所有服务实例清单allServerList和新传入的服务实例清单newServers都加入到newList中,然后通过调用setServersList方法对newList进行处理,在BaseLoadBalancer中实现的时候会使用新的列表覆盖旧的列表。

@Override
    public void addServers(List<Server> newServers) {
        if (newServers != null && newServers.size() > 0) {
            try {
                ArrayList<Server> newList = new ArrayList<Server>();
                newList.addAll(allServerList);
                newList.addAll(newServers);
                setServersList(newList);
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Exception while adding Servers", name, e);
            }
        }
    }

2.chooseServer(Object key):挑选一个具体的服务实例,刚刚介绍Rule时已经介绍过。

3.markServerDown(Server server):标记某个实例暂停服务。

public void markServerDown(Server server) {
        if (server == null || !server.isAlive()) {
            return;
        }

        logger.error("LoadBalancer [{}]:  markServerDown called on [{}]", name, server.getId());
        server.setAlive(false);
        // forceQuickPing();

        notifyServerStatusChangeListener(singleton(server));
    }

4.getReachableServers():获取可用的服务实例列表。由于BaseLoadBalancer单独维护了一个正常服务的实例清单所以直接返回即可。

@Override
    public List<Server> getReachableServers() {
        return Collections.unmodifiableList(upServerList);
    }

5.getAllServers():获取所有的服务实例列表。由于BaseLoadBalancer单独维护了一个所有服务的实例清单所以直接返回即可。

@Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }

DynamicServerListLoadBalancer

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


volatile ServerList<T> serverListImpl;

volatile ServerListFilter<T> filter;

①ServerList<T> serverListImpl:实现实例清单的动态更新,PollingServerListUpdater是动态服务列表更新的默认策略,他通过定时任务进行服务列表的更新,在启动定时任务时有两个参数:

//开始执行时间,默认1s
private final long initialDelayMs;
//重复执行的时间,默认30s
private final long refreshIntervalMs;

也就是说在动态更新实例初始化后延迟1s开始执行,并以30s为周期重复执行。

②ServerListFilter<T> filter:定义了一个方法List getFilteredListOfServers(List servers),主要用于实现服务实例列表的过滤,通过传入实例清单,根据一些规则返回过滤后的服务实例清单。该接口的实现如下:

除了ZonePreferenceServerListFilter实现是Spring Cloud Ribbon中对NetFlix Ribbon的扩展外,其他均是NetFlix Ribbon的原生实现。

1.AbstractServerListFilter:这是一个抽象过滤器,在这里定义了过滤时需要的重要依据对象LoadBalancerStats,上面有过介绍。

2.ZoneAffinityServerListFilter:该过滤器基于“区域感知”的方式实现服务实例的过滤,它会根据提供服务的实例所在的区域(Zone)和于消费者自身所处区域(Zone)进行比较,过滤掉那些不是同出同一区域的实例。

3.DefaultNIWSServerListFilter:该过滤器完全继承自ZoneAffinityServerListFilter,是默认的NIWS(NetFlix Internal Web Service)过滤器。

4.ServerListSubsetFilter:该过滤器也继承自ZoneAffinityServerListFilter,他非常适用拥有大规模服务器集群(上百或更多)的系统。他可以产生一个“区域感知”结果的子集列表,同时还能通过比较服务实例的通信失败数量和并发连接数来判定服务是否健康来选择性从服务实例列表剔除相对不健康的实例。

5.ZonePreferenceServerListFilter:使用Spring Cloud 整合Ribbon默认会使用该过滤器,他实现了通过配置或者Eureka实例元数据的所属区域(Zone)来过滤出同区域的服务实例,首先通过父类ZoneAffinityServerListFilter获取“区域感知”的服务实例列表,然后遍历取出根据消费者配置预设的区域Zone来进行过滤,如果结果为空则直接返回父类获取的结果,不为空返回通过消费者配置的Zone过滤后的结果。

ZoneAwareLoadBalancer

ZoneAwareLoadBalancer是对DynamicServerListLoadBalancer的扩展,在DynamicServerListLoadBalancer中并没有重写chooseServer方法,它依然会采用在BaseLoadBalancer中实现的算法,使用RoundRobinRule规则以线性轮询的方式选择调用服务实例。该算法没有区域(Zone)的概念,所以他会把所有实例视为一个Zone下的节点来看待,这样就会周期性的产生跨区域(Zone)访问的情况,会产生更高的延迟,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<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个时才会使用次实现,否则仍然使用父类的实现(线性轮询)。当数量大于1时会选择出可用的Zone区域集合,然后随机选择一个Zone区域,然后在此选择的Zone区域内使用ZoneAvoidanceRule实现来挑选具体的服务实例。

负载均衡策略

IRule接口有如下实现:

1.AbstractLoadBalancerRule:在该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现服务选择策略时获取到一些负载均衡器中维护的信息来作为分配依据。

2.RandomRule:该策略实现了从服务实例清单中随机选择一个服务实例的功能。

3.RoundRobinRule:采用线性轮询的方式依次选择每个服务实例的功能。

4.RetryRule:该策略实现了一个具备重试机制的实例选择功能,它内部默认使用了RoundRobinRule实例来进行反复尝试选择。若期间能选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阈值(maxRetryMillis参数定义的值+choose方法开始执行的时间戳),当超过该阈值就返回null。

5.WeightedResponseTimeRule:该策略是对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,根据权重来挑选实例。该策略在初始化时会启动一个定时任务来为每个服务实例计算权重,默认30s执行一次。

6.ClientConfigEnabledRoundRobinRule:该策略功能和RoundRobinRule相同,我们一般不直接使用,而是通过继承该策略,使其可以当作子类的备选策略。

7.BestAvailableRule:该策略继承于ClientConfigEnabledRoundRobinRule,用于选择最空闲的服务实例。

8.ZoneAvoidanceRule:将服务实例进行Zone过滤后通过线性轮询选择一个具体的服务实例。

猜你喜欢

转载自blog.csdn.net/WYA1993/article/details/81871117
今日推荐