质量、速度、廉价,只能选择其中两个。
–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
随着微服务、云源生的流行,多云、多区域(zone)、跨机房部署的case越来越多。Ribbon作为微服务领域的优秀组件,自然也提供了对多区域支持的负载均衡能力。
作为基础,本文将介绍多zone负载均衡中最为重要的一个方法:ZoneAvoidanceRule.getAvailableZones()
,它解决了根据LoadBalancerStats
状态信息仲裁出可用区出来。
正文
关于getAvailableZones
方法,其实有两处地方都叫这个名字,但是它们的功能是不一样的,且存在依赖的关系,为了避免读者迷糊,现分别进行阐述。
LoadBalancerStats#getAvailableZones实例方法
它是LoadBalancerStats
里的一个实例方法:
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()
方法(后文重点阐述,非常重要)
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;
}
}
}
这个随机算法最核心的就是最后面的index
和sum
算法,看完后你应该有如下疑问:为何不来个从chooseFrom
这个集合里随机弹出一个zone就成,而非弄的这么麻烦呢?
其实这么做的是很有意义的,这么做能保证:zone里面机器数越多的话,被选中的概率是越大的,这样随机才是最合理的。
getAvailableZones()
该方法是一个静态工具方法,顾名思义它用于获取真实的可用区,它在LoadBalancerStats#getAvailableZones
方法的基础上,结合每个zone对应的ZoneSnapshot
的情况再结合阈值设置,筛选真正可用的zone区域。
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;
}
这个选择可用区的步骤还是比较重要的,毕竟现在多区域部署、多云部署都比价常见,现在对它的处理过程做如下文字总结:
- 若zone为null,返回null。若只有一个zone,就返回当前zone,不用再继续判断。否则默认返回所有zone:
availableZones
。接下来会一步步做remove()移除动作 - 使用变量
Set<String> worstZones
记录所有zone中比较糟糕的zone们;用maxLoadPerServer
表示所有zone中负载最高的区域;用limitedZoneAvailability
表示是否是部分zone可用(true:部分可用,false:全部可用) - 遍历所有的zone,根据其对应的快照
ZoneSnapshot
来判断负载情况 - 若当前zone的
instanceCount
也就是实例总数是0,那就remove(当前zone),并且标记limitedZoneAvailability=true
(因为移除了一个,就不是全部了嘛)。若当前zone的实例数>0,那就继续 - 拿到当前总的平均负载
loadPerServer
,如果zone内的熔断实例数 / 总实例数 >= triggeringBlackoutPercentage阈值
或者loadPerServer < 0
的话,那就执行remove(当前zone),并且limitedZoneAvailability=true
熔断实例数 / 总实例数 >= 阈值
标记为当前zone就不可用了(移除掉),这个很好理解。这个阈值为0.99999d
也就说所有的Server实例被熔断了,该zone才算不可用了loadPerServer < 0
是什么鬼?那么什么时候loadPerServer会是负数呢?它在LoadBalancerStats#getZoneSnapshot()
方法里:if (circuitBreakerTrippedCount == instanceCount)
的时候,loadPerServer = -1
,也就说当所有实例都熔断了,那么loadPerServer
也无意义了嘛,所以赋值为-1。- 总的来说1和2触达条件差不多,只是1的阈值是可以配置的,比如你配置为0.9那就是只有当90%机器都熔断了就认为该zone不可用了,而不用100%(请原谅我把
0.99999d
当1来看待)
- 经过以上步骤,说明所有的zone是基本可用的,但可能有些负载高有些负载低,因此接下来需要判断区域负载情况,就是如下这段代码。这段代码的总体意思是:从所有zone中找出负载最高的区域们(若负载差在
0.000001d
只能被认为是相同负载,都认为是负载最高的们)。- 说明:
worstZones
里面装载着负载最高的zone们,也就是top1(当然可能多个并列第一的情况)
- 说明:
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);
}
- 分析好数据后,最后准备返回结果。若统计完所有的区域后,最高负载
maxLoadPerServer
仍旧小于提供的triggeringLoad阈值
,并且并且limitedZoneAvailability=false
(就是说所有zone都可用的情况下),那就返回所有的zone吧:availableZones
。- 这个很好理解:所有的兄弟们负载都很低,并且一个哥们都没“死”,那就都返回出去呗
triggeringLoad
阈值的默认值是0.2,负载的计算方式是:loadPerServer = 整个zone的活跃请求总数 / 整个zone内可用实例总数
。- 注意:一定是活跃连接数。也就是说正在处理中的链接数才算做服务压力嘛
- 若最大负载超过阈值(或者死了一个/N个兄弟),那么就不能返回全部拉。那就从负载最高的兄弟们中(因为可能多个,可能1个,大概率是只有1个值的)随机选择一个出来:
randomChooseZone(snapshot, worstZones)
,然后执行移除remove(zoneToAvoid)
掉,这么处理的目的是把负载最高的那个哥们T除掉,再返回结果。- 说明:这里使用的随机算法就是上面所讲述的(谁的zone里面实例数最多,就越可能被选中)
总而言之:选择可用区的原则是T除掉不可用的、T掉负载最高的区域,其它区域返回结果,这样处理后返回的结果才是健康程度综合最好的。
另外,该方法还有个重载的,便捷使用方法:
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"
- 默认key:
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,那么请忽略本文内容~
代码示例
// 单独线程模拟刷页面,获取监控到的数据
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);
}
运行程序,打印:
=====当前可用区为:[华南, 华东]
区域[华南]概要:实例总数: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
的理解具有核心要意。
这部分逻辑理解起来稍显费力,建议多读几遍,并且结合自己脑补的场景便可完成,当然喽,若有不知道的概念,请参阅前面相关文章,毕竟学习就像砌砖,跳不过去的。
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
- [享学Netflix] 一、Apache Commons Configuration:你身边的配置管理专家
- [享学Netflix] 二、Apache Commons Configuration事件监听机制及使用ReloadingStrategy实现热更新
- [享学Netflix] 三、Apache Commons Configuration2.x全新的事件-监听机制
- [享学Netflix] 四、Apache Commons Configuration2.x文件定位系统FileLocator和FileHandler
- [享学Netflix] 五、Apache Commons Configuration2.x别样的Builder模式:ConfigurationBuilder
- [享学Netflix] 六、Apache Commons Configuration2.x快速构建工具Parameters和Configurations
- [享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?
- [享学Netflix] 八、Apache Commons Configuration2.x相较于1.x使用上带来哪些差异?
- [享学Netflix] 九、Archaius配置管理库:初体验及基础API详解
- [享学Netflix] 十、Archaius对Commons Configuration核心API Configuration的扩展实现
- [享学Netflix] 十一、Archaius配置管理器ConfigurationManager和动态属性支持DynamicPropertySupport
- [享学Netflix] 十二、Archaius动态属性DynamicProperty原理详解(重要)
- [享学Netflix] 十三、Archaius属性抽象Property和PropertyWrapper详解
- [享学Netflix] 十四、Archaius如何对多环境、多区域、多云部署提供配置支持?
- [享学Netflix] 十五、Archaius和Spring Cloud的集成:spring-cloud-starter-netflix-archaius
- [享学Netflix] 十六、Hystrix断路器:初体验及RxJava简介
- [享学Netflix] 十七、Hystrix属性抽象以及和Archaius整合实现配置外部化、动态化
- [享学Netflix] 十八、Hystrix配置之:全局配置和实例配置
- [享学Netflix] 十九、Hystrix插件机制:SPI接口介绍和HystrixPlugins详解
- [享学Netflix] 二十、Hystrix跨线程传递数据解决方案:HystrixRequestContext
- [享学Netflix] 二十一、Hystrix指标数据收集(预热):滑动窗口算法(附代码示例)
- [享学Netflix] 二十二、Hystrix事件源与事件流:HystrixEvent和HystrixEventStream
- [享学Netflix] 二十三、Hystrix桶计数器:BucketedCounterStream
- [享学Netflix] 二十四、Hystrix在滑动窗口内统计:BucketedRollingCounterStream、HealthCountsStream
- [享学Netflix] 二十五、Hystrix累计统计流、分发流、最大并发流、配置流、功能流(附代码示例)
- [享学Netflix] 二十六、Hystrix指标数据收集器:HystrixMetrics(HystrixDashboard的数据来源)
- [享学Netflix] 二十七、Hystrix何为断路器的半开状态?HystrixCircuitBreaker详解
- [享学Netflix] 二十八、Hystrix事件计数器EventCounts和执行结果ExecutionResult
- [享学Netflix] 二十九、Hystrix执行过程核心接口:HystrixExecutable、HystrixObservable和HystrixInvokableInfo
- [享学Netflix] 三十、Hystrix的fallback回退/降级逻辑源码解读:getFallbackOrThrowException
- [享学Netflix] 三十一、Hystrix触发fallback降级逻辑的5种情况及代码示例
- [享学Netflix] 三十二、Hystrix抛出HystrixBadRequestException异常为何不会触发熔断?
- [享学Netflix] 三十三、Hystrix执行目标方法时,如何调用线程池资源?
- [享学Netflix] 三十四、Hystrix目标方法执行逻辑源码解读:executeCommandAndObserve
- [享学Netflix] 三十五、Hystrix执行过程集大成者:AbstractCommand详解
- [享学Netflix] 三十六、Hystrix请求命令:HystrixCommand和HystrixObservableCommand
- [享学Netflix] 三十七、源生Ribbon介绍 — 客户端负载均衡器
- [享学Netflix] 三十八、Ribbon核心API源码解析:ribbon-core(一)IClient请求客户端
- [享学Netflix] 三十九、Ribbon核心API源码解析:ribbon-core(二)IClientConfig配置详解
- [享学Netflix] 四十、Ribbon核心API源码解析:ribbon-core(三)RetryHandler重试处理器
- [享学Netflix] 四十一、Ribbon核心API源码解析:ribbon-core(四)ClientException客户端异常
- [享学Netflix] 四十二、Ribbon的LoadBalancer五大组件之:IPing心跳检测
- [享学Netflix] 四十三、Ribbon的LoadBalancer五大组件之:ServerList服务列表
- [享学Netflix] 四十四、netflix-statistics详解,手把手教你写个超简版监控系统
- [享学Netflix] 四十五、Ribbon服务器状态:ServerStats及其断路器原理
- [享学Netflix] 四十六、Ribbon负载均衡策略服务器状态总控:LoadBalancerStats