前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 四十七、Ribbon多区域选择

[享学Netflix] 四十七、Ribbon多区域选择

作者头像
YourBatman
发布2020-03-19 17:13:45
2.1K0
发布2020-03-19 17:13:45
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

质量、速度、廉价,只能选择其中两个。 代码下载地址:https://github.com/f641385712/netflix-learning

目录
  • 前言
  • 正文
    • LoadBalancerStats#getAvailableZones实例方法
    • ZoneAvoidanceRule静态工具方法
      • randomChooseZone()
      • getAvailableZones()
        • 该方法使用处
    • 不合理的默认值
    • 代码示例
  • 总结
    • 声明

前言

随着微服务、云源生的流行,多云、多区域(zone)、跨机房部署的case越来越多。Ribbon作为微服务领域的优秀组件,自然也提供了对多区域支持的负载均衡能力。

作为基础,本文将介绍多zone负载均衡中最为重要的一个方法:ZoneAvoidanceRule.getAvailableZones(),它解决了根据LoadBalancerStats状态信息仲裁出可用区出来。


正文

关于getAvailableZones方法,其实有两处地方都叫这个名字,但是它们的功能是不一样的,且存在依赖的关系,为了避免读者迷糊,现分别进行阐述。


LoadBalancerStats#getAvailableZones实例方法

它是LoadBalancerStats里的一个实例方法:

代码语言:javascript
复制
LoadBalancerStats:

	volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<>();
    public Set<String> getAvailableZones() {
        return upServerListZoneMap.keySet();
    }

若有下面逻辑的存在,其实我觉得该方法的命令是颇具歧义的,或许叫getAllAvailableZones()会更合适一些。因为它仅是一个普通的获取方法,并不考虑对应zone内Server的负载情况、可用情况,这些都交给下面这个工具方法进行完成。


ZoneAvoidanceRule静态工具方法

首先我吐槽一下:作为static工具方法,为毛放在ZoneAvoidanceRule里呢?统一放在LoadBalancerStats里内聚起来不香吗?


randomChooseZone()

在开始之前,我们先了解下这个选择支持方法,但是它是非public。调用方仅有两处:

  • 下面的getAvailableZones()方法
  • ZoneAwareLoadBalancer#chooseServer()方法(后文重点阐述,非常重要)
代码语言:javascript
复制
ZoneAvoidanceRule:

    static String randomChooseZone(
    		Map<String, ZoneSnapshot> snapshot,
            Set<String> chooseFrom) {
        if (chooseFrom == null || chooseFrom.size() == 0) {
            return null;
        }
		// 注意:默认选择的是第一个zone区域
		// 若总共就1个区域,那就是它了。若有多个,那就需要随机去选
		String selectedZone = chooseFrom.iterator().next();
        if (chooseFrom.size() == 1) {
        	return chooseFrom.iterator().next();
        }

		// 所有的区域中总的Server实例数
        int totalServerCount = 0;
        for (String zone : chooseFrom) {
            totalServerCount += snapshot.get(zone).getInstanceCount();
        }
        // 从所有的实例总数中随机选个数字。比如总数是10台机器
        // 那就是从[1-10]之间随机选个数字,比如选中为6
        int index = random.nextInt(totalServerCount) + 1;
		
		// sum代表当前实例统计的总数
		// 它的逻辑是:当sum超过这个index时,就以这个区域为准
        int sum = 0;
        for (String zone : chooseFrom) {
            sum += snapshot.get(zone).getInstanceCount();
            if (index <= sum) {
                selectedZone = zone;
                break;
            }
        }
	
	}

这个随机算法最核心的就是最后面的indexsum算法,看完后你应该有如下疑问:为何不来个从chooseFrom这个集合里随机弹出一个zone就成,而非弄的这么麻烦呢?

其实这么做的是很有意义的,这么做能保证:zone里面机器数越多的话,被选中的概率是越大的,这样随机才是最合理的


getAvailableZones()

该方法是一个静态工具方法,顾名思义它用于获取真实的可用区,它在LoadBalancerStats#getAvailableZones方法的基础上,结合每个zone对应的ZoneSnapshot的情况再结合阈值设置,筛选真正可用的zone区域。

代码语言:javascript
复制
ZoneAvoidanceRule:

	// snapshot:zone对应的ZoneSnapshot的一个map
	// triggeringLoad:
	// triggeringBlackoutPercentage:
	public static Set<String> getAvailableZones(
			Map<String, ZoneSnapshot> snapshot,
			double triggeringLoad,
			double triggeringBlackoutPercentage) {
		// 为毛一个都木有不返回空集合???有点乱啊。。。。不过没关系
        if (snapshot.isEmpty()) {
            return null;
        }
		
		//最终需要return的可用区,中途会进行排除的逻辑
		Set<String> availableZones = new HashSet<>(snapshot.keySet());
		// 如果有且仅有一个zone可用,再糟糕也得用,不用进行其他逻辑了
        if (availableZones.size() == 1) {
            return availableZones;
        }
		
		// 记录很糟糕
		Set<String> worstZones = new HashSet<>();
		// 所有zone中,平均负载最高值
		double maxLoadPerServer = 0;
		// true:zone有限可用
		// false:zone全部可用
		boolean limitedZoneAvailability = false;

		// 对每个zone的情况逐一分析
		for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
            String zone = zoneEntry.getKey();
            ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
            int instanceCount = zoneSnapshot.getInstanceCount();
            
            // 若该zone内一个实例都木有了,那就是完全不可用,那就移除该zone
            // 然后标记zone是有限可用的(并非全部可用喽)
            if (instanceCount == 0) {
                availableZones.remove(zone);
                limitedZoneAvailability = true;
            } else {
            	// 该zone的平均负载
				double loadPerServer = zoneSnapshot.getLoadPerServer();
				
				// 机器的熔断总数 / 总实例数已经超过了阈值(默认为1,也就是全部熔断才会认为该zone完全不可用)
				// 或者 loadPerServer < 0 (啥时候小于0???下面说)
                if (((double) zoneSnapshot.getCircuitTrippedCount()) / instanceCount >= triggeringBlackoutPercentage
                        || loadPerServer < 0) {
					// 证明这个zone完全不可用,就移除掉
                    availableZones.remove(zone);
                    limitedZoneAvailability = true;
				} else { // 并不是完全不可用,就看看状态是不是很糟糕

					// 若当前负载和最大负载相当,那认为已经很糟糕了
					if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
						worstZones.add(zone);
					
					// 或者若当前负载大于最大负载了
					} else if (loadPerServer > maxLoadPerServer) {
						maxLoadPerServer = loadPerServer;
						worstZones.clear();
						worstZones.add(zone);
					}
				}
            }
		}

		// 若最大负载小于设定的负载阈值 并且limitedZoneAvailability=false
		// 就是说全部zone都可用,并且最大负载都还没有达到阈值,那就把全部zone返回
        if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
            // zone override is not needed here
            return availableZones;
        }
        String zoneToAvoid = randomChooseZone(snapshot, worstZones);
        if (zoneToAvoid != null) {
            availableZones.remove(zoneToAvoid);
        }
        return availableZones;
	}

这个选择可用区的步骤还是比较重要的,毕竟现在多区域部署、多云部署都比价常见,现在对它的处理过程做如下文字总结:

  1. 若zone为null,返回null。若只有一个zone,就返回当前zone,不用再继续判断。否则默认返回所有zone:availableZones。接下来会一步步做remove()移除动作
  2. 使用变量Set<String> worstZones记录所有zone中比较糟糕的zone们;用maxLoadPerServer表示所有zone中负载最高的区域;用limitedZoneAvailability表示是否是部分zone可用(true:部分可用,false:全部可用)
  3. 遍历所有的zone,根据其对应的快照ZoneSnapshot来判断负载情况
  4. 若当前zone的instanceCount也就是实例总数是0,那就remove(当前zone),并且标记limitedZoneAvailability=true(因为移除了一个,就不是全部了嘛)。若当前zone的实例数>0,那就继续
  5. 拿到当前总的平均负载loadPerServer,如果zone内的熔断实例数 / 总实例数 >= triggeringBlackoutPercentage阈值 或者 loadPerServer < 0的话,那就执行remove(当前zone),并且limitedZoneAvailability=true
    1. 熔断实例数 / 总实例数 >= 阈值标记为当前zone就不可用了(移除掉),这个很好理解。这个阈值为0.99999d也就说所有的Server实例被熔断了,该zone才算不可用了
    2. loadPerServer < 0是什么鬼?那么什么时候loadPerServer会是负数呢?它在LoadBalancerStats#getZoneSnapshot()方法里:if (circuitBreakerTrippedCount == instanceCount)的时候,loadPerServer = -1,也就说当所有实例都熔断了,那么loadPerServer也无意义了嘛,所以赋值为-1。
    3. 总的来说1和2触达条件差不多,只是1的阈值是可以配置的,比如你配置为0.9那就是只有当90%机器都熔断了就认为该zone不可用了,而不用100%(请原谅我把0.99999d当1来看待)
  6. 经过以上步骤,说明所有的zone是基本可用的,但可能有些负载高有些负载低,因此接下来需要判断区域负载情况,就是如下这段代码。这段代码的总体意思是:从所有zone中找出负载最高的区域们(若负载差在0.000001d只能被认为是相同负载,都认为是负载最高的们)。
    1. 说明:worstZones里面装载着负载最高的zone们,也就是top1(当然可能多个并列第一的情况)
代码语言:javascript
复制
	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);
	}
  1. 分析好数据后,最后准备返回结果。若统计完所有的区域后,最高负载maxLoadPerServer仍旧小于提供的triggeringLoad阈值,并且并且limitedZoneAvailability=false(就是说所有zone都可用的情况下),那就返回所有的zone吧:availableZones
    1. 这个很好理解:所有的兄弟们负载都很低,并且一个哥们都没“死”,那就都返回出去呗
    2. triggeringLoad阈值的默认值是0.2,负载的计算方式是:loadPerServer = 整个zone的活跃请求总数 / 整个zone内可用实例总数
      1. 注意:一定是活跃连接数。也就是说正在处理中的链接数才算做服务压力嘛
  2. 若最大负载超过阈值(或者死了一个/N个兄弟),那么就不能返回全部拉。那就从负载最高的兄弟们中(因为可能多个,可能1个,大概率是只有1个值的)随机选择一个出来:randomChooseZone(snapshot, worstZones),然后执行移除remove(zoneToAvoid)掉,这么处理的目的是把负载最高的那个哥们T除掉,再返回结果。
    1. 说明:这里使用的随机算法就是上面所讲述的(谁的zone里面实例数最多,就越可能被选中)

总而言之:选择可用区的原则是T除掉不可用的、T掉负载最高的区域,其它区域返回结果,这样处理后返回的结果才是健康程度综合最好的。

另外,该方法还有个重载的,便捷使用方法:

代码语言:javascript
复制
ZoneAvoidanceRule:

	// 实际调用仍旧为getAvailableZones方法~
	// 它友好的只需要传参LoadBalancerStats即可,内部帮你构建snapshot这个Map
    public static Set<String> getAvailableZones(LoadBalancerStats lbStats,
            double triggeringLoad, double triggeringBlackoutPercentage) {
        if (lbStats == null) {
            return null;
        }
        Map<String, ZoneSnapshot> snapshot = createSnapshot(lbStats);
        return getAvailableZones(snapshot, triggeringLoad,
                triggeringBlackoutPercentage);
    }

该方法使用处

getAvailableZones()方法的调用处主要有两个地方:

  • ZoneAvoidancePredicate#apply():用于过滤掉哪些超过阈值的、不可用的zone区域们
  • ZoneAwareLoadBalancer#chooseServer():通过此方法拿到可用区域们availableZones,然后再通过randomChooseZone()方法从中随机选取一个出来,再从zone里选择一台Server就是最佳的Server

不合理的默认值

可以先看下面代码示例。计算可用区的两个阈值是:

  • triggeringLoad:平均负载阈值。该阈值可配置
    • ZoneAvoidancePredicate:默认值均为0.2d
      • 默认key:ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold
      • 个性化key:"ZoneAwareNIWSDiscoveryLoadBalancer." + clientConfig.getClientName() + ".triggeringLoadPerServerThreshold"
    • ZoneAwareLoadBalancer:默认值亦为0.2
      • 配置同上
  • triggeringBlackoutPercentage:触发“熄灭”的百分比阈值(简单的说当你的实例挂了%多少时,就移除掉此区域)。

triggeringBlackoutPercentage这个阈值尚且合理(默认所有实例挂了才会移除这个zone),但是triggeringLoad这个阈值仅设置为0.2,what a fuck???也就说一个zone里面有10台机器的话,超过2个请求打进来就算负载过重,从而最终结果会移除掉一个负载最高的可用区,这么设定脑子不是怕陪驴砸了吧?

这么配置有何后果? 0.2的阈值等于所有zone都处于过载状态,因此选择可用区的时候永远会T除掉一个(当然你只有一个可用区除外),假如你总共只有2个可用区,这将使得负载均衡策略完全失效~~~~

说明:我强烈怀疑老外是想表达负载超过20%了就算负载过重了,只是它没考虑到ZoneSnapshot.loadPerServer它并不是一个百分比值~~~

在实际生产中:我个人强烈建议你增加默认配置ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold = 100。表示单台机器超过100个并发后认为负载过高了(当然100这个数值你可以根据机器配置具体设定,此处仅供参考),这样能极大的提高zone之间的负载均衡能力

说明:这一切都建立在你的应用部署在多zone的情况下,若你仅有一个zone,那么请忽略本文内容~


代码示例

代码语言:javascript
复制
// 单独线程模拟刷页面,获取监控到的数据
private void monitor(LoadBalancerStats lbs) {
    List<String> zones = Arrays.asList("华南", "华东", "华北");
    new Thread(() -> {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleWithFixedDelay(() -> {
            // 打印当前可用区
            // 获取可用区
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(lbs, 0.2d, 0.99999d);
            System.out.println("=====当前可用区为:" + availableZones);

            zones.forEach(zone -> {
                System.out.printf("区域[" + zone + "]概要:");
                int instanceCount = lbs.getInstanceCount(zone);
                int activeRequestsCount = lbs.getActiveRequestsCount(zone);
                double activeRequestsPerServer = lbs.getActiveRequestsPerServer(zone);
                // ZoneSnapshot zoneSnapshot = lbs.getZoneSnapshot(zone);

                System.out.printf("实例总数:%s,活跃请求总数:%s,平均负载:%s\n", instanceCount, activeRequestsCount, activeRequestsPerServer);
                // System.out.println(zoneSnapshot);
            });
            System.out.println("======================================================");
        }, 5, 5, TimeUnit.SECONDS);
    }).start();
}


// 请注意:请必须保证Server的id不一样,否则放不进去List的(因为Server的equals hashCode方法仅和id有关)
// 所以此处使用index作为port,以示区分
private Server createServer(String zone, int index) {
    Server server = new Server("www.baidu" + zone + ".com", index);
    server.setZone(zone);
    return server;
}


// 多线程,模拟请求
private void request(ServerStats serverStats) {
    new Thread(() -> {
        // 每10ms发送一个请求(每个请求处理10-200ms的时间),持续不断
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleWithFixedDelay(() -> {
            new Thread(() -> {
                // 请求之前 记录活跃请求数
                serverStats.incrementActiveRequestsCount();
                serverStats.incrementNumRequests();
                long rt = doSomething();
                // 请求结束, 记录响应耗时
                serverStats.noteResponseTime(rt);
                serverStats.decrementActiveRequestsCount();
            }).start();
        }, 10, 10, TimeUnit.MILLISECONDS);
    }).start();
}

// 模拟请求耗时,返回耗时时间
private long doSomething() {
    try {
        int rt = randomValue(10, 200);
        TimeUnit.MILLISECONDS.sleep(rt);
        return rt;
    } catch (InterruptedException e) {
        e.printStackTrace();
        return 0L;
    }
}

// 本地使用随机数模拟数据收集
private int randomValue(int min, int max) {
    return min + (int) (Math.random() * ((max - min) + 1));
}

// ============单元测试
@Test
public void fun6() throws InterruptedException {
    LoadBalancerStats lbs = new LoadBalancerStats("YoutBatman");

    // 添加Server
    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));
    lbs.updateServerList(serverList);

    Map<String, List<Server>> zoneServerMap = new HashMap<>();
    // 模拟向每个Server发送请求  记录ServerStatus数据
    serverList.forEach(server -> {
        ServerStats serverStat = lbs.getSingleServerStat(server);
        request(serverStat);

        // 顺便按照zone分组
        String zone = server.getZone();
        if (zoneServerMap.containsKey(zone)) {
            zoneServerMap.get(zone).add(server);
        } else {
            List<Server> servers = new ArrayList<>();
            servers.add(server);
            zoneServerMap.put(zone, servers);
        }
    });
    lbs.updateZoneServerMapping(zoneServerMap);

    // 从lbs里拿到一些监控数据
    monitor(lbs);

    TimeUnit.SECONDS.sleep(500);
}

运行程序,打印:

代码语言:javascript
复制
=====当前可用区为:[华南, 华东]
区域[华南]概要:实例总数:1,活跃请求总数:10,平均负载:10.0
区域[华东]概要:实例总数:2,活跃请求总数:18,平均负载:9.0
区域[华北]概要:实例总数:4,活跃请求总数:41,平均负载:10.25
======================================================
=====当前可用区为:[华南, 华北]
区域[华南]概要:实例总数:1,活跃请求总数:9,平均负载:9.0
区域[华东]概要:实例总数:2,活跃请求总数:22,平均负载:11.0
区域[华北]概要:实例总数:4,活跃请求总数:34,平均负载:8.5
======================================================
=====当前可用区为:[华南, 华东]
区域[华南]概要:实例总数:1,活跃请求总数:9,平均负载:9.0
区域[华东]概要:实例总数:2,活跃请求总数:18,平均负载:9.0
区域[华北]概要:实例总数:4,活跃请求总数:37,平均负载:9.25
======================================================
=====当前可用区为:[华北, 华东]
区域[华南]概要:实例总数:1,活跃请求总数:10,平均负载:10.0
区域[华东]概要:实例总数:2,活跃请求总数:17,平均负载:8.5
区域[华北]概要:实例总数:4,活跃请求总数:39,平均负载:9.75
======================================================
...

从中可以明显的看出:每次会把负载最高的Zone给T除掉(请认真观察输出的数据来发现规律),这是完全符合预期的。

说明:因为平均负载均超过阈值0.2,所以会从所有zone中排除掉一个负载最高的zone~


总结

关于Ribbon可用区选择逻辑就先介绍这,这里有必要再次强调:虽然它为static静态方法,但是它是可用区过滤逻辑、可用区选择的核心逻辑,这对后面的具有区域意识的LoadBalancer的理解具有核心要意。

这部分逻辑理解起来稍显费力,建议多读几遍,并且结合自己脑补的场景便可完成,当然喽,若有不知道的概念,请参阅前面相关文章,毕竟学习就像砌砖,跳不过去的。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 前言
  • 正文
    • LoadBalancerStats#getAvailableZones实例方法
      • ZoneAvoidanceRule静态工具方法
        • randomChooseZone()
        • getAvailableZones()
      • 不合理的默认值
        • 代码示例
        • 总结
        相关产品与服务
        负载均衡
        负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档