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

前言

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

上图涵盖了 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;
    }

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

    原文作者:Spring Cloud
    原文地址: https://blog.csdn.net/tjreal/article/details/79890232
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞