Ribbon的负载均衡策略解读

489 阅读12分钟

Ribbon的负载均衡策略解读

目录

[TOC]

环境

pom配置

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-dependencies</artifactId>
     <version>Finchley.SR3</version>
     <type>pom</type>
     <scope>import</scope>
</dependency>

大纲

策略名解释
RoundRobinRule/轮询策略轮询所有的服务实例
RandomRule/随机策略随机挑选一个服务实例
WeightedResponseTimeRule/平均响应时间加权策略平均响应时间越小的服务实例越有机会被调用到
ZoneAvoidanceRule/区域回避策略根据相同区域及可用性筛选轮询策略,这个也是默认策略
AvailabilityFilteringRule/可用性过滤策略过滤掉ribbon认为熔断的或者高负载的服务(服务活跃链接数超过限制)
BestAvailableRule/最小连接数选中当前应用中剔除已熔断的服务对应最小链接数的服务实例
RetryRule/重试策略在500ms内反复用轮询策略反复重试

轮询策略 RoundRobinRule

指遍历调用服务的注册中心的服务列表,每次请求都选择不同实例,讲究一个雨露均衡

打开RoundRobinRule类,主要看choose方法,实现了netflix提供的IRule接口


private AtomicInteger nextServerCyclicCounter;

    public RoundRobinRule() {
        //下一次轮询的值
        nextServerCyclicCounter = new AtomicInteger(0);
    }


private int incrementAndGetModulo(int modulo) {
        for (;;) {
            //每次这个位置都向前+1保证和上次的不一样 并且mod所有服务实例的数量
            //假如第一次请求是2个实例 那么这个值就是(0+1)%2=1
            //假如第二次请求是3个实例 那么这个值就是(1+1)%3=2
            //假如第三次请求是3个实例 那么这个值就是 (2+1)%3=0
            //不一定每次都完全轮询 但是基本按照轮询这个规则走
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
 }

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;
            }
  		   //这里其实我有个问题 为什么要使用所有服务列表的数量呢 而不是可达服务列表的数量呢
            //明显可达服务列表的数量更加可靠
            //https://github.com/Netflix/ribbon/issues/372 这个哥们有一样的问题 但是截至现在还是没人回答
            //我盲猜这是个bug
            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                //假如此时服务列表发生了改变 那么暂时让出cpu控制权 进入下次循环 如果超过10次拿到的还是null 那么返回null给调用方
                //有点类似与我们平常写的Thread.Sleep
                Thread.yield();
                continue;
            }

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

            // Next.
            server = null;
        }

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

这个负载均衡比较有意思的地方在于计算下个调用实例怎么计算的呢,其实很简单 计算方法就在incrementAndGetModulo方法里面,使用了一个原子整型类,每次计算服务列表索引记录当前+1并且mod服务列表的值,保证始终能够获取到下一个索引

随机策略 RandomRule

随机调用一个服务列表中的服务,这个其实应该没什么难度,大家都想到多半是nextInt实现,但是这里有点意思的地方不在于算法实现,我还是贴出来给大家

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) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }

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

            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }

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

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }

我觉得有意思的地方在这里

image-20220504222633067

假如说所有的服务实例(allList)加起来是10个,可达的服务实例只有8个,那么按理说 upList.get(8)这个会抛出IndexOutOfBoundsException异常吧,,感觉这个是一个显而易见的bug,然后我去翻了下ribbon的pr,看到github.com/Netflix/rib… 这个pr中确实有人提出了这个疑问,而且也做了修复,实际上修复完是这样的(只不过是我用的版本比较老了):

image-20220504223119551

嗯,改动基本符合我想法,确实是去用了可达服务列表的数量来获取服务实例。但是我发现版本和我现阶段差的过大,然后又特意去看了下这个chooseRandomInt(upCount)方法,发现改的完全不认识了,卧槽这改的有点大啊。然后我看了下这个提交

image-20220504224443667

这位史密斯麦克老哥是这么说的,使用ThreadLoacalRandom 来代替Random,为了性能优化。我打开Random这个类里面,发现里面实现方式其实是这样的

public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);

        int r = next(31);
        int m = bound - 1;
        if ((bound & m) == 0)  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> 31);
        else {
            for (int u = r;
                 u - (r = u % bound) + m < 0;
                 u = next(31))
                ;
        }
        return r;
    }
    
     protected int next(int bits) {
        long oldseed, nextseed;
         //这里seed是一个实例变量,是原子变量,这里通过cas不断的重试到成功为止,同时避免不同的线程获取到相同的seed
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

对比下ThreadLocalRandom.current().nextInt方法,这个Random.nextInt相当于每条线程都要来竞争seed,虽然是多条线程,但是用的同一个IRule实例,也用的同一个rand变量,虽然是通过cas的方式,但是对比ThreadLocalRandom.current().nextInt在每条线程中维护一个seed变量无疑是更加低效的。总结一下改进方式就是用多余的空间(几乎微不足道的)换取了效率,降低了竞争。

平均响应时间加权 WeightedResponseTimeRule

这个rule什么意思呢?其实它代表的意思是根据服务列表的平均响应时间加权选择服务实例,平均响应时间越短代表服务实例权重越大,那么它是怎么实现的呢,具体算法如下:

 // holds the accumulated weight from index 0 to current index
//每个槽里是0-当前槽位积累的权重
// for example, element at index 2 holds the sum of weight of servers from 0 to 2
//打个比方,第二个槽里包含0 1 2的权重
private volatile List<Double> accumulatedWeights = new ArrayList<Double>();

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
            //假如没有服务被选中那么使用super的策略 这个super指的就是轮询调用
            if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
                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;
    }

我们能看到核心的算法就在这里

image-20220505113544386

currentWeights是复制accumulatedWeights 的引用,accumulatedWeights里面存着什么呢,里面存着所有服务实例的权重区间,权重区间又是怎么存的呢

假如第一个实例的权重是8,第二个实例的权重是9,第三个实例权重是5。每个数组槽里存的都是前面所有槽里的元素加上当前槽里的元素的值

那么这个数组里面的元素就是[8,17,22],也就是说currentWeights里面就是这个数组,接下来再看这个算法就会变的很简单了:

  • 假如randomWeight落在0-8之间,那么serverIndex=0,选中0号的服务列表
  • 假如randomWeight落在8-17之间,那么serverIndex=1,选中一号的服务列表
  • 假如randomWeight落在17-22之间,那么serverIndex=2,选中二号服务列表

这样就能看出,假如服务实例权重越高,那么权重区间就会越宽(权重区间宽度相当于服务实例的权重):

image-20220505114131214

这个还是挺有意思的

ZoneAvoidanceRule 区域回避策略

接下来到ZoneAvoidanceRule 这个策略,这个类我们先看看他的UML图:

image-20220505155924433

可以看到这个类名实属相当的规范

在上图我们可以发现,ZoneAvoidanceRule 这个类通过父类实现了IRule接口,它本身并没有实现Choose方法,所以我们看看PredicateBasedRule这个类的choose方法:

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
   
    /**
     * Method that provides an instance of {@link AbstractServerPredicate} to be used by this class.
     * 
     */
    //可以看到这是个抽象方法,那么作为子类的ZoneAvoidanceRule 必定实现了这个方法,通过重写这个方法来达成自己的筛选服务实例的方式
    public abstract AbstractServerPredicate getPredicate();
        
    /**
     * Get a server by calling {@link AbstractServerPredicate#chooseRandomlyAfterFiltering(java.util.List, Object)}.
     * The performance for this method is O(n) where n is number of servers to be filtered.
     */
    @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;
        }       
    }
    //假如经过getEligibleServers过滤得到的合格的
     public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
         //筛选出可用的服务列表后再做轮询 
         //这个 incrementAndGetModulo 是轮询的算法
        return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
    }
}

然后是ZoneAvoidanceRule 怎么实现getPredicate方法的:

	private CompositePredicate compositePredicate;
    
    public ZoneAvoidanceRule() {
        super();
        ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
        AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
        compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
    }

	private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
        return CompositePredicate.withPredicates(p1, p2)
                             .addFallbackPredicate(p2)
                             .addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
                             .build();
        
    }

	@Override
    public AbstractServerPredicate getPredicate() {
        return compositePredicate;
    } 
	

image-20220505163440266

这两个过滤器类,最终会被执行其中的apply方法来筛选服务实例。

先执行ZoneAvoidancePredicate.apply过滤得到可用的服务区域,再执行AvailabilityPredicate.apply获取的可用服务实例

那我们具体看下ZoneAvoidancePredicateAvailabilityPredicate类的apply方法:

 @Override
    public boolean apply(@Nullable PredicateKey input) {
        if (!ENABLED.get()) {
            return true;
        }
        String serverZone = input.getServer().getZone();
        //如果服务实例没有地区信息那么不做筛选直接返回
        if (serverZone == null) {
            // there is no zone information from the server, we do not want to filter
            // out this server
            return true;
        }
        LoadBalancerStats lbStats = getLBStats();
        //LoadBalancerStats类存储了每个服务实例的状态和统计信息
        //如果还没有记录服务状态实例 那么也返回
        if (lbStats == null) {
            // no stats available, do not filter
            return true;
        }
        //如果所有可用服务对于的地区只有一个 那么也没什么必要筛选 直接返回筛选成功
        if (lbStats.getAvailableZones().size() <= 1) {
            // only one zone is available, do not filter
            return true;
        }
        Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
        if (!zoneSnapshot.keySet().contains(serverZone)) {
            // The server zone is unknown to the load balancer, do not filter it out 
            return true;
        }
        logger.debug("Zone snapshots: {}", zoneSnapshot);
        //非常关键的在这里 如何获取可用的区域的?
        Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
        logger.debug("Available zones: {}", availableZones);
        if (availableZones != null) {
            return availableZones.contains(input.getServer().getZone());
        } else {
            return false;
        }
    }    
public static Set<String> getAvailableZones(
            Map<String, ZoneSnapshot> snapshot, double triggeringLoad,
            double triggeringBlackoutPercentage) {
        if (snapshot.isEmpty()) {
            return null;
        }
        Set<String> availableZones = new HashSet<String>(snapshot.keySet());
    	//假如快照里只有一个实例 那么不用也得用 直接返回
        if (availableZones.size() == 1) {
            return availableZones;
        }
        Set<String> worstZones = new HashSet<String>();
        double maxLoadPerServer = 0;
        boolean limitedZoneAvailability = false;
		
    	//遍历服务区域的快照
        for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
            String zone = zoneEntry.getKey();
            ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
            int instanceCount = zoneSnapshot.getInstanceCount();
            if (instanceCount == 0) {
                //如果当前区域快照对应的服务实例一个都没有了 那么将这个服务实例移除可用服务
                //并且limitedZoneAvailability = true标记
                availableZones.remove(zone);
                limitedZoneAvailability = true;
            } else {
                //这个loadPerServer是什么呢?这是zoneSnapshot创建出来的时候计算出来的一个值
                //计算公式是             
                //loadPerServer = ((double) activeConnectionsCountOnAvailableServer) / (instanceCount - circuitBreakerTrippedCount);
                //即激活连接数/未熔断的服务实例
               	//即每个平均的在线连接数 也叫 区域的平均负载
                double loadPerServer = zoneSnapshot.getLoadPerServer();
                //circuitTrippedCount这个值是指区域内熔断的服务实例数
                //这个if的意思是 熔断服务数/总服务数 >= 熔断阙值(这里默认值是0.99999d) 或者 平均负载小于0 这两种情况 该区域都视为不可用
                //平均负载小于0按理说根据公式来说不太可能。。
                if (((double) zoneSnapshot.getCircuitTrippedCount())
                        / instanceCount >= triggeringBlackoutPercentage
                        || loadPerServer < 0) {
                    availableZones.remove(zone);
                    limitedZoneAvailability = true;
                } else {
                    //这里if的意思是指假如平均负载和最大负载非常接近(相差不超过0.000001d),那么就将当前遍历到的区域加入最差负载中
                    if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
                        // they are the same considering double calculation
                        // round error
                        worstZones.add(zone);
                        //如果当前区域的平均负载大于当前的最大负载 更新最大负载并且加入到最糟糕负载区域集合中
                    } else if (loadPerServer > maxLoadPerServer) {
                        maxLoadPerServer = loadPerServer;
                        worstZones.clear();
                        worstZones.add(zone);
                    }
                }
            }
        }
		//如果此时最大负载小于配置的阈值 而且没有区域熔断比例或者平均负载小于0或没有没有服务实例的区域(limitedZoneAvailability=false)
        if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
            // zone override is not needed here
            //此时说明所有的区域都是健康的 可以直接返回
            return availableZones;
        }
    //三种情况会走到这里
    //1、最大负载大于配置的阈值
    //2、有无服务实例的区域
    //3、有区域熔断比例或者平均负载小于0
    //randomChooseZone从最糟糕的区域中随机选择一个区域出来 在可用区域中踢掉 再返回到调用方
        String zoneToAvoid = randomChooseZone(snapshot, worstZones);
        if (zoneToAvoid != null) {
            availableZones.remove(zoneToAvoid);
        }
        return availableZones;

    }

接着是AvailabilityPredicate的apply方法,这个方法就简单的多:

 @Override
public boolean apply(@Nullable PredicateKey input) {
    LoadBalancerStats stats = getLBStats();
    if (stats == null) {
        return true;
    }
    return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}	
//这里是关键 决定该服务实例是否应该被过滤掉
//条件是什么呢
//1、如果ribbion熔断器开启并且当前服务实例状态已被熔断
//2、或者当前服务实例活跃链接数》=定义的服务活跃链接数限制(在这里默认值是Integer.MAX_VALUE,理论上不可能超越的,但是如果我们有设置,那么这个语义就是指高负载的服务实例)
//简单的说的话那就是已经被ribbon判断熔断的服务或者非常高负载的服务那就不在纳入调用范围内
private boolean shouldSkipServer(ServerStats stats) {
        if ((circuitBreakerFiltering.getOrDefault() && stats.isCircuitBreakerTripped())
                || stats.getActiveRequestsCount() >= getActiveConnectionsLimit()) {
            return true;
        }
        return false;
    }

这个ZoneAvoidanceRule还是比较难懂的,我总结下调用链路:

ZoneAvoidanceRule.choose()-> PredicateBasedRule.choose()->ZoneAvoidancePredicate.apply->AvailabilityPredicate.apply->incrementAndGetModulo(eligible.size()

也就是说顺序是这样的

  1. 执行区域回避策略过滤掉不可用区域的服务
  2. 执行可用性策略过滤掉已经熔断的服务或高负载的服务
  3. 在过滤剩下的可用服务中做轮询挑选服务实例

AvailabilityFilteringRule 可用性策略

image-20220506095437425

我们能看到这个AvailabilityFilteringRule也是继承了PredicateBasedRule类,说明也是基于过滤器实现的策略类

我们还是看这个策略的choose方法

	private AbstractServerPredicate predicate;
    
    public AvailabilityFilteringRule() {
    	super();
        //最后使用的过滤器类是AvailabilityPredicate
        predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, null))
                .addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
                .build();
    }


	@Override
    public Server choose(Object key) {
        int count = 0;
        //使用轮询算法挑选可用的server
        //这里为什么使用轮询
        //其实是为了不直接传所有的服务实例去过滤 提高性能
        Server server = roundRobinRule.choose(key);
        while (count++ <= 10) {
            //关键还是这个过滤
            if (server != null && predicate.apply(new PredicateKey(server))) {
                return server;
            }
            server = roundRobinRule.choose(key);
        }
        return super.choose(key);
    }
	
	@Override
    public boolean apply(@Nullable PredicateKey input) {
        LoadBalancerStats stats = getLBStats();
        if (stats == null) {
            return true;
        }
        return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
    }
    //其实这里和上面的ZoneAvoidanle用的过滤器是同一个AvailabilityPredicate 就不做重复解析了
    private boolean shouldSkipServer(ServerStats stats) {
        if ((circuitBreakerFiltering.getOrDefault() && stats.isCircuitBreakerTripped())
                || stats.getActiveRequestsCount() >= getActiveConnectionsLimit()) {
            return true;
        }
        return false;
    }

这个策略类区别其实不大 只是做了小小的优化:用轮询算法挑选服务而不是直接遍历所有服务列表,然后再使用了可用性过滤器

BestAvailableRule 最小连接数策略

这个策略比较简单,只是过滤掉那些已经熔断的服务实例并且通过选择排序选择当前最小激活连接的服务实例

这个激活连接数怎么来的呢 每次开始请求的时候这个数量会+1 每次完成请求的时候这个数量会-1 也就是说当前最小激活链接指当前应用对应调用的服务中还未完成的请求数最少的服务

 @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;
        }
    }

RetryRule 轮询重试策略

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)) {
			//启动一个通知当前线程的终止线程任务 这个线程执行的时间是在接下来的500ms
			InterruptTask task = new InterruptTask(deadline
					- System.currentTimeMillis());
			//这500ms内不断地循环直到找到一个可用的服务实例
            //或者因为超时被终止线程任务通知 
			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;
		}
    
    public class InterruptTask extends TimerTask {
		
		static Timer timer = new Timer("InterruptTimer", true); 
		
		protected Thread target = null;

		public InterruptTask(long millis) {
				target = Thread.currentThread();
				timer.schedule(this, millis);
		}
    }

这个策略也算比较简单,但是ribbon提供了一种让人耳目一新的重试写法