质量、速度、廉价,只能选择其中两个。 代码下载地址:https://github.com/f641385712/netflix-learning
随着微服务、云源生的流行,多云、多区域(zone)、跨机房部署的case越来越多。Ribbon作为微服务领域的优秀组件,自然也提供了对多区域支持的负载均衡能力。
作为基础,本文将介绍多zone负载均衡中最为重要的一个方法:ZoneAvoidanceRule.getAvailableZones()
,它解决了根据LoadBalancerStats
状态信息仲裁出可用区出来。
关于getAvailableZones
方法,其实有两处地方都叫这个名字,但是它们的功能是不一样的,且存在依赖的关系,为了避免读者迷糊,现分别进行阐述。
它是LoadBalancerStats
里的一个实例方法:
LoadBalancerStats:
volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<>();
public Set<String> getAvailableZones() {
return upServerListZoneMap.keySet();
}
若有下面逻辑的存在,其实我觉得该方法的命令是颇具歧义的,或许叫getAllAvailableZones()
会更合适一些。因为它仅是一个普通的获取方法,并不考虑对应zone内Server的负载情况、可用情况,这些都交给下面这个工具方法进行完成。
首先我吐槽一下:作为static工具方法,为毛放在ZoneAvoidanceRule
里呢?统一放在LoadBalancerStats
里内聚起来不香吗?
在开始之前,我们先了解下这个选择支持方法,但是它是非public。调用方仅有两处:
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里面机器数越多的话,被选中的概率是越大的,这样随机才是最合理的。
该方法是一个静态工具方法,顾名思义它用于获取真实的可用区,它在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;
}
这个选择可用区的步骤还是比较重要的,毕竟现在多区域部署、多云部署都比价常见,现在对它的处理过程做如下文字总结:
availableZones
。接下来会一步步做remove()移除动作Set<String> worstZones
记录所有zone中比较糟糕的zone们;用maxLoadPerServer
表示所有zone中负载最高的区域;用limitedZoneAvailability
表示是否是部分zone可用(true:部分可用,false:全部可用)ZoneSnapshot
来判断负载情况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。0.99999d
当1来看待)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内可用实例总数
。 randomChooseZone(snapshot, worstZones)
,然后执行移除remove(zoneToAvoid)
掉,这么处理的目的是把负载最高的那个哥们T除掉,再返回结果。 总而言之:选择可用区的原则是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 ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold
"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,那么请忽略本文内容~
// 单独线程模拟刷页面,获取监控到的数据
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
的理解具有核心要意。
这部分逻辑理解起来稍显费力,建议多读几遍,并且结合自己脑补的场景便可完成,当然喽,若有不知道的概念,请参阅前面相关文章,毕竟学习就像砌砖,跳不过去的。