Spring Cloud 源码分析(二)—— 负载均衡策略

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】
免费领取 2000+ 道 Java 面试题

前言

Spring Cloud Ribbon是一个基于 HTTP 和 TCP 的客户端负载均衡工具。通过 Spring Cloud 封装,我们可以将面向服务的REST目标请求自动转换成客户端负载均衡的服务调用。IRule 是Ribbon 中负载均衡器服务选择策略要实现的接口,我们可以看一下它的类图:
20191123100143\_1.png

上图涵盖了 Ribbon 所有的负载均衡策略,我们看一下IRule的源码:

    public interface IRule{
        public Server choose(Object key);

        public void setLoadBalancer(ILoadBalancer lb);

        public ILoadBalancer getLoadBalancer();    
    }

上述方法中最核心的便是choose方法,子类重写该方法以实现不同的负载均衡策略。再看一下AbstractLoadBalancerRule类,此类为抽象类,类中定义了负载均衡器对象,负载均衡策略都是以此负载均衡器中维护的信息为依据。

    public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {

        private ILoadBalancer lb;

        @Override
        public void setLoadBalancer(ILoadBalancer lb){
            this.lb = lb;
        }

        @Override
        public ILoadBalancer getLoadBalancer(){
            return lb;
        }      
    }

RandomRule

RandomRule策略实现从服务实例清单中随机选择一个服务实例的功能,重点看一下该类的choose方法,

    public Server choose(ILoadBalancer lb, Object key) {
            if (lb == null) {
                return null;
            }
            Server server = null;

            while (server == null) {
                if (Thread.interrupted()) {
                    return null;
                }
                List<Server> upList = lb.getReachableServers();
                List<Server> allList = lb.getAllServers();

                int serverCount = allList.size();
                if (serverCount == 0) {
                    return null;
                }

                int index = rand.nextInt(serverCount);
                server = upList.get(index);

                if (server == null) {
                    Thread.yield();
                    continue;
                }

                if (server.isAlive()) {
                    return (server);
                }

                server = null;
                Thread.yield();
            }

            return server;
        }

该策略使用负载均衡器来获取可用实例列表 upList 和全量实例列表 allList,通过生成一个不大于服务实例总数量的随机值,并将其作为索引返回具体实例。

RoundRobinRule

RoundRobinRule按照线性轮询的方式依次选择每个服务实例,看一下choose方法,

    public Server choose(ILoadBalancer lb, Object key) {
            if (lb == null) {
                log.warn("no load balancer");
                return null;
            }

            Server server = null;
            int count = 0;
            while (server == null && count++ < 10) {
                List<Server> reachableServers = lb.getReachableServers();
                List<Server> allServers = lb.getAllServers();
                int upCount = reachableServers.size();
                int serverCount = allServers.size();

                if ((upCount == 0) || (serverCount == 0)) {
                    log.warn("No up servers available from load balancer: " + lb);
                    return null;
                }

                int nextServerIndex = incrementAndGetModulo(serverCount);
                server = allServers.get(nextServerIndex);

                if (server == null) {
                    /* Transient. */
                    Thread.yield();
                    continue;
                }

                if (server.isAlive() && (server.isReadyToServe())) {
                    return (server);
                }
                server = null;
            }

            if (count >= 10) {
                log.warn("No available alive servers after 10 tries from load balancer: "
                        + lb);
            }
            return server;
        }

可以看到,RoundRobinRule的具体实现跟RandomRule类似,在循环条件中增加了一个名为count的计数器,如果选择不到server超过10次,便会结束尝试并打印告警信息。

WeightedResponseTimeRule

WeightedResponseTimeRule继承自RoundRobinRule,在选择服务实例的时候把权重因素也考虑进去。WeightedResponseTimeRule在初始化的时候会启动一个定时任务用来计算每个服务实例的权重,默认间隔为30秒。

    public Server choose(ILoadBalancer lb, Object key) {
            if (lb == null) {
                return null;
            }
            Server server = null;

            while (server == null) {
                // get hold of the current reference in case it is changed from the other thread
                List<Double> currentWeights = accumulatedWeights;
                if (Thread.interrupted()) {
                    return null;
                }
                List<Server> allList = lb.getAllServers();

                int serverCount = allList.size();

                if (serverCount == 0) {
                    return null;
                }

                int serverIndex = 0;

                // last one in the list is the sum of all weights
                double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); 
                // No server has been hit yet and total weight is not initialized
                // fallback to use round robin
                if (maxTotalWeight < 0.001d) {
                    server =  super.choose(getLoadBalancer(), key);
                    if(server == null) {
                        return server;
                    }
                } else {
                    // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
                    double randomWeight = random.nextDouble() * maxTotalWeight;
                    // pick the server index based on the randomIndex
                    int n = 0;
                    for (Double d : currentWeights) {
                        if (d >= randomWeight) {
                            serverIndex = n;
                            break;
                        } else {
                            n++;
                        }
                    }

                    server = allList.get(serverIndex);
                }

                if (server == null) {
                    /* Transient. */
                    Thread.yield();
                    continue;
                }

                if (server.isAlive()) {
                    return (server);
                }

                // Next.
                server = null;
            }
            return server;
        }

该策略累加每个服务的权重值获得权重之和,并生成一个不大于权重之和的随机值,根据随机值所属的权重区间索引,返回相应的服务实例。RoundRobinRule、WeightedResponseTimeRule两种策略与Dubbo中的随机策略相似,读者可以自行比较一番。

RetryRule

顾名思义,RetryRule实现了一个具备重试机制的实例选择功能,看一下具体实现,

    public class RetryRule extends AbstractLoadBalancerRule {
        IRule subRule = new RoundRobinRule();
        long maxRetryMillis = 500;

        ……

        public Server choose(ILoadBalancer lb, Object key) {
            long requestTime = System.currentTimeMillis();
            long deadline = requestTime + maxRetryMillis;

            Server answer = null;

            answer = subRule.choose(key);

            if (((answer == null) || (!answer.isAlive()))
                    && (System.currentTimeMillis() < deadline)) {

                InterruptTask task = new InterruptTask(deadline
                        - System.currentTimeMillis());

                while (!Thread.interrupted()) {
                    answer = subRule.choose(key);

                    if (((answer == null) || (!answer.isAlive()))
                            && (System.currentTimeMillis() < deadline)) {
                        /* pause and retry hoping it's transient */
                        Thread.yield();
                    } else {
                        break;
                    }
                }

                task.cancel();
            }

            if ((answer == null) || (!answer.isAlive())) {
                return null;
            } else {
                return answer;
            }
        }
        ……
    }

RetryRule内部定义了一个IRule对象,默认使用RoundRobinRule实例,并且在choose方法内部反复尝试内部定义的策略。如果在设置的尝试结束时间阈值内选择不到服务实例就返回null。

ClientConfigEnabledRoundRobinRule

ClientConfigEnabledRoundRobinRule 策略比较特殊,一般不直接用它。看一下它的源码,

    public class ClientConfigEnabledRoundRobinRule extends AbstractLoadBalancerRule {

        RoundRobinRule roundRobinRule = new RoundRobinRule();
        ……
        @Override
        public Server choose(Object key) {
            if (roundRobinRule != null) {
                return roundRobinRule.choose(key);
            } else {
                throw new IllegalArgumentException(
                        "This class has not been initialized with the RoundRobinRule class");
            }
        }
    }

ClientConfigEnabledRoundRobinRule 内部定义了一个RoundRobinRule 策略,而choose方法也是直接使用RoundRobinRule的线性轮询机制,这是为何呢?

其实从文章开头的类图中可以看到,有些策略继承自ClientConfigEnabledRoundRobinRule,父类的choose方法可以在子类无法获取服务实例的时候作为一种备选方案。后文介绍的策略均为ClientConfigEnabledRoundRobinRule的扩展。

BestAvailableRule

顾名思义,此策略返回的是“”最可用“”的服务实例。如何定义“最可用”呢?其实就是当前并发请求数量最少的可用服务实例,换句话说,也就是“最闲的”实例。

    public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {

        private LoadBalancerStats loadBalancerStats;

        @Override
        public Server choose(Object key) {
            if (loadBalancerStats == null) {
                return super.choose(key);
            }
            List<Server> serverList = getLoadBalancer().getAllServers();
            int minimalConcurrentConnections = Integer.MAX_VALUE;
            long currentTime = System.currentTimeMillis();
            Server chosen = null;
            for (Server server: serverList) {
                ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
                if (!serverStats.isCircuitBreakerTripped(currentTime)) {
                    int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                    if (concurrentConnections < minimalConcurrentConnections) {
                        minimalConcurrentConnections = concurrentConnections;
                        chosen = server;
                    }
                }
            }
            if (chosen == null) {
                return super.choose(key);
            } else {
                return chosen;
            }
        }
        ……
    }

BestAvailableRule 通过遍历负载均衡器中维护的所有服务实例,过滤掉故障的实例,找出并发请求最小的一个并返回。如果因为某些因素无法找到,则采用父类的线性轮询策略。

PredicateBasedRule

这是一个基于Predicate实现的抽象策略。Predicate 是 Google Guava Collection 工具对集合进行过滤的条件接口。

    public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {

        public abstract AbstractServerPredicate getPredicate();

        @Override
        public Server choose(Object key) {
            ILoadBalancer lb = getLoadBalancer();
            Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
            if (server.isPresent()) {
                return server.get();
            } else {
                return null;
            }       
        }
    }

接下来的两个策略是基于此抽象策略实现的,只是他们使用了不同Predicate 实现来完成过滤逻辑达到不同的实例选择效果。

AvailabilityFilteringRule

AvailabilityFilteringRule 继承自 PredicateBasedRule,基本处理逻辑也是“先过滤清单、再轮询选择”。

    public class AvailabilityFilteringRule extends PredicateBasedRule {    

        private AbstractServerPredicate predicate;

        @Override
        public Server choose(Object key) {
            int count = 0;
            Server server = roundRobinRule.choose(key);
            while (count++ <= 10) {
                if (predicate.apply(new PredicateKey(server))) {
                    return server;
                }
                server = roundRobinRule.choose(key);
            }
            return super.choose(key);
        }
    }

过滤条件使用了 AvailabilityPredicate,看一下 AvailabilityPredicate 的apply方法,

    public boolean apply(@Nullable PredicateKey input) {
            LoadBalancerStats stats = getLBStats();
            if (stats == null) {
                return true;
            }
            return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
        }

        private boolean shouldSkipServer(ServerStats stats) {        
            if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped()) 
                    || stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
                return true;
            }
            return false;
        }

源码中过滤逻辑位于shouldSkipServer方法中,条件为是否故障和实例的并发请求数大于阈值,二者满足其一便返回true。

ZoneAvoidanceRule

ZoneAvoidanceRule 采用了CompositePredicate来进行服务实例清单的过滤,这是一个组合条件,以 ZoneAvoidancePredicate 为主过滤条件,AvailabilityPredicate 为次过滤条件。

    public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
            List<Server> result = super.getEligibleServers(servers, loadBalancerKey);
            Iterator<AbstractServerPredicate> i = fallbacks.iterator();
            while (!(result.size() >= minimalFilteredServers && result.size() > (int) (servers.size() * minimalFilteredPercentage))
                    && i.hasNext()) {
                AbstractServerPredicate predicate = i.next();
                result = predicate.getEligibleServers(servers, loadBalancerKey);
            }
            return result;
        }

过滤逻辑需要判断两个条件:过滤后的实例总数不小于最小过滤实例数、过滤后的实例比例大于最小过滤百分比,任何一个条件不满足,则将当前结果返回以供线性轮询算法选择。


来源:http://ddrv.cn/a/88268

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Spring Cloud 源码分析(二)—— 负载均衡策略

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏