[享学Netflix] 五十一、Ribbon的LoadBalancer五大组件之:IRule(一)轮询和加权轮询

无论你多有天分,也不论你多么努力,有些事情就是需要时间。让九个女人同时怀孕,小孩也不可能在一个月内出生。

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

前言

在介绍完了IPing、ServerList、ServerListFilter、ServerListUpdater之后,终于来到了LoadBalancer五大核心组件中的最后一个组件:IRule。它是Ribbon负载均衡器的核心中的核心,是五大组件中最为重要,也是最难理解的部分,本系列将此部分内容将分多篇进行叙述。

Ribbon最重要的能力是负载均衡(注意:Ribbon并不等于负载均衡,只是负载均衡器是它最出名也是最重要的模块),而负载均衡的核心是负载均衡算法,这些算法都将通过IRule的子类一一体现。

正文

IRule一共提供7种规则算法:

  1. RoundRobinRule轮询规则:线性轮询
  2. WeightedResponseTimeRule加权轮询规则:Server的rt响应时间作为权重参考,响应时间越短,权重越高,从而越容易被选中
  3. RandomRule随机规则:使用ThreadLocalRandom.current().nextInt(serverCount)随机选择一个Server
  4. AvailabilityFilteringRule可用性过滤规则:过滤掉已熔断/负载过高的Server们,然后剩下的Server们使用线性轮询
  5. ZoneAvoidanceRule区域可用性规则:评估zone可用区的性能,然后从多个可用区中按权重选择一个zone,在从zone里面线性轮询出一台Server
  6. RetryRule重试规则:它可以包装一个IRule subRule(默认是RoundRobinRule),当一个周期内没找到Server时,进行重试
  7. BestAvailableRule最小并发请求规则:选择一个最小并发数(也就是ServerStats.activeRequestsCount最小)的Server

本文将介绍轮询和加权轮询规则。


IRule介绍

为LoadBalancer定义“规则”的接口,它是负载均衡算法的实现。负载均衡策略,根据特定算法中从服务列表中选取一个要访问的Server。

public interface IRule {

	// 最为重要的一个方法:从lb.allServers/lb.upServers根据key找到一台Server
	// 若没找到返回null
	public Server choose(Object key);

	public void setLoadBalancer(ILoadBalancer lb);
	public ILoadBalancer getLoadBalancer();
}

它的继承图谱如下(Spring Cloud下并无单独实现):

在这里插入图片描述
在这里插入图片描述

AbstractLoadBalancerRule

该抽象类仅事把IRule和ILoadBalancer完成绑定,所以根据Rule选择的Server列表均来自ILoadBalancer

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

其它所有实现类均基于它来实现。



RoundRobinRule 轮询

轮循算法实现,最广为人知和最基本的负载均衡策略(也叫线性轮询),算法也比较容易理解。


成员属性

public class RoundRobinRule extends AbstractLoadBalancerRule {
    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;
}
  • nextServerCyclicCounter:类似于index,用于控制线性轮询规则
  • AVAILABLE_ONLY_SERVERS/ALL_SERVERS:本来是用来控制是轮询所有还是只轮询up的机器的,但其实代码里并没有使用到它俩。(规则是轮询所有Server

算法逻辑

轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。

代码实现非常简单,就一小戳:

RoundRobinRule:

    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

该算法逻辑在AbstractServerPredicate#incrementAndGetModulo()也有一个一毛一样的方法,代表都是轮询算法喽。


choose()选择逻辑

依照算法,最终会选择出唯一一个Server供以使用。

RoundRobinRule:

	// 注意:它仅是个public方法,并非接口方法
	// 另外,它轮询的是lb里的allServers,而并非只是up的哦~~~~
	public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

		Server server = null;
		// 一次choose中最多轮询10次,如果还没有找到可用的,那就放弃 避免你有100台机器,一直轮询下去
		int count = 0; 
		while (server == null && count++ < 10) {
			// 说明:up的机器仅仅只是做一个数量的判断,并不参与整整的轮询
			// 真正轮询的是allServers,这点特别注意
            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);

			// 若选出的这个server是活的并且可以服务了,那就return
			// 否则,继续下一轮的循环获取
			// 所以,,,为毛不直接轮询upServers呢???这个后文会给出解释
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }
            server = null;
		}
		
		// 10次都还没找到一台可用的,那就不用再找了。返回null
        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "  + lb);
        }
        return server;
	}

轮询过程用一句话总结:把所有的Server使用线性轮询算法选出一台Server供以使用。有如下几点有必要提出:

  • 轮询的是allServers,而非reachableServers
  • 轮询最多只会尝试10次,如果10次还没找到可以提供服务的Server,也不继续往下走
  • 其中你应该有个疑问:这里是拿出来再判断,那为毛不直接轮询reachableServers?这个在后文会给出答案

接口方法实现:

RoundRobinRule:

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

使用场景

轮询调度算法假设所有服务器的处理性能都相同(实际上大多数情况确实是这样的),不关心每台服务器的当前连接数和响应速度。当请求服务间隔时间变化比较大时,轮询调度算法容易导致服务器间的负载不平衡。

所以此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。另外因为这种算法具有人为基本可预测性,所以调试、定位问题方面相对容易点

轮询策略是Ribbon的默认策略,也几乎是所有的负载均衡器的默认策略。基于它扩展出了带权重的轮询策略,它能满足机器性能不一样的case,这便是它的子类们。

在这里插入图片描述


WeightedResponseTimeRule 加权轮询

它继承自轮询算法,相较于它,增加了响应时间作为权重,为每个Server动态分配权重,然后按照权重轮询(加权循环)。

当没有为服务器收集足够的统计信息时,此规则将回退到RoundRobinRule。所以他是依赖于LoadBalancerStats统计数据的。


成员属性
public class WeightedResponseTimeRule extends RoundRobinRule {
	
	public static final IClientConfigKey<Integer> WEIGHT_TASK_TIMER_INTERVAL_CONFIG_KEY = CommonClientConfigKey.valueOf("ServerWeightTaskTimerInterval");
	public static final int DEFAULT_TIMER_INTERVAL = 30 * 1000;

	private int serverWeightTaskTimerInterval = DEFAULT_TIMER_INTERVAL;
	private volatile List<Double> accumulatedWeights = new ArrayList<Double>();
	private final Random random = new Random();
	protected Timer serverWeightTimer = null;
	protected AtomicBoolean serverWeightAssignmentInProgress = new AtomicBoolean(false);
	String name = "unknown";

}
  • serverWeightTaskTimerInterval:多久去重新计算一次Server的权重。默认是30s定时去计算一次。当然你可以通过ServerWeightTaskTimerInterval这个key去配置,如<clientName>.ribbon.ServerWeightTaskTimerInterval = xxx
  • accumulatedWeights:保存从索引0到当前索引的累计权重,例如,index 2处的值表示拥有从0到2的服务器权重之和(每30会重新计算一次)
  • random:生成随机值。这样权重越大,随机落入的概率就越大
  • serverWeightTimer:Timer,用于定时调度
  • serverWeightAssignmentInProgress:标志位。标志是否正在进行server权重的分配
  • nameILoadBalancer的名称

算法逻辑
WeightedResponseTimeRule:

    @Override
    public Server choose(ILoadBalancer lb, Object key) {
    
    	Server server = null;
    	// 循环内找出一台Server
    	while (server == null) {
    		// 获取当前引用,以防它被其他线程更改
    		List<Double> currentWeights = accumulatedWeights;
    		// 轮询的也是所有的Server
    		List<Server> allList = lb.getAllServers();
    		int serverCount = allList.size();
            if (serverCount == 0) {
                return null;
            }

			// 列表中的最后一个是所有权值的和
			double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); 
			// 没有服务器被击中,总重量没有初始化,那就回退到正常的轮询逻辑
			if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
				server =  super.choose(getLoadBalancer(), key);
                if(server == null) {
                    return server;
                }
			} else {
			
				//============权重轮询逻辑=========
				// 随机生成一个权重值(0-最大权重),然后选取一片
				// 这个算法同ZoneAvoidanceRule.randomChooseZone()算法是一样的
				double randomWeight = random.nextDouble() * maxTotalWeight;
	            int n = 0;
	            for (Double d : currentWeights) {
	                if (d >= randomWeight) {
	                    serverIndex = n;
	                    break;
	                } else {
	                    n++;
	                }
	            }
	            server = allList.get(serverIndex);
    		}
    		
            if (server.isAlive()) {
                return (server);
            }
            // Next.
            server = null;
    	}
    	return server;
    }

按照收集到的权重值数组,若收集的到的数据不够,回退到线性轮询规则;若够了,就随机生成一个权重值(范围在0-最大权重值之间),然后从中取出一个index,这便是加权轮询的算法。


定时统计权重值

权重算法的数据依据是定时统计权重值。

WeightedResponseTimeRule:

    serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval);
    class DynamicServerWeightTask extends TimerTask {
    	@Override
    	public void run() {
    		new ServerWeight().maintainWeights();
    	}
    }

WeightedResponseTimeRule.ServerWeight:

	// 维护权重的方法。权重值的计算规则
	public void maintainWeights() {
		
		// 1、得到所有Server总的rt时间(使用的平均值)
		double totalResponseTime = 0;
		for (Server server : nlb.getAllServers()) {
			totalResponseTime += ssstats.getSingleServerStat(server).getResponseTimeAvg();
		}
		
		// 每个服务器的权重为(所有服务器的响应时间总和- responseTime)
		// 因此,响应时间越长,权重越小,被选择的可能性就越小
		Double weightSoFar = 0.0;
		...
		setWeights(finalWeights);
	}

这个策略整合了随机算法和加权算法(根据响应时间),会开启定时任务,每30秒计算一次所有Provider的响应时间,以响应时间作为权重,响应时间越短的服务器被选中的概率越大。

比如Node1:Node2:Node3的平均响应时间为100ms:200ms:300ms,那么nodes的权重值对应是300:500:600(逆序)。然后每次以600为基础 * 随机值,那么落在 0–300的概率为50%,300–500的概率33%,100–600的概率为17%,也就是平均响应时间越短的节点,被选中的概率越大。


使用场景

该算法更加的智能一些,在父类的基础上增加了弹性(比如对网络的容错性更强),但是在木有分区,其它环境全一样的情况下必要性不大。

实际在生产环境下,我个人建议使用该策略代替默认的线性轮询策略,因为它的效果更好。当然喽,它的坏处是因为随机所以对于单次请求是具有不可预测性的,调试起来稍微困难点


ResponseTimeWeightedRule

已标记为过期,请使用WeightedResponseTimeRule代替。


代码示例

@Test
public void fun1() throws InterruptedException {
    List<Server> serverList = new ArrayList<>();
    serverList.add(createServer("华南", 1));
    serverList.add(createServer("华东", 1));
    serverList.add(createServer("华东", 2));

    serverList.add(createServer("华北", 1));
    serverList.add(createServer("华北", 2));
    serverList.add(createServer("华北", 3));
    serverList.add(createServer("华北", 4));

    // 轮询策略:因为Servers均来自于lb,所以必须关联上一个lb实例哦
    ILoadBalancer lb = new BaseLoadBalancer();
    lb.addServers(serverList);
    RoundRobinRule rule = new RoundRobinRule(lb);
    while (true) {
        System.out.println(rule.choose(null));
        TimeUnit.SECONDS.sleep(2);
    }
}


private Server createServer(String zone, int index) {
    Server server = new Server("www.baidu" + zone + ".com", index);
    server.setAlive(true);
    server.setReadyToServe(true);
    server.setZone(zone);
    return server;
}

运行程序,控制台打印:

www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
www.baidu华北.com:4
www.baidu华南.com:1
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
www.baidu华北.com:4
www.baidu华南.com:1
...

非常标准的线性轮询有木有。


总结

关于`Ribbon的LoadBalancer五大组件之:IRule的第一篇就先介绍到这,本文概述了IRule以及介绍了其7大负载均衡算法,且着重介绍了最为常用、最广为人知的轮询算法和加权轮询算法的实现,并且辅以示例来加强理解。
分隔线

声明

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

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

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

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

分享到微信朋友圈

×

扫一扫,手机浏览