前言
上一节我们介绍了Ribbon的一个功能很强大的负载均衡器集成了Ribbon的五大组件的能力,其实有了它基本就能满足我们平时的需求了。但是Netflix开发人员继续发扬精益求精的代码精神 又默默为我们奉献了一个更高级的负载均衡器ZoneAwareLoadBalancer
。从名字可以看出是一个具备区域意识的负载均衡器。例如当前的服务自华南、华北、华东、西北四个区域那么ZoneAwareLoadBalancer
会过滤掉负载较高或者几乎不可用的区域。
回顾 ZoneAvoidanceRule.getAvailableZones
因为ZoneAwareLoadBalancer
本身代码比较简单,主要的逻辑是基于ZoneAvoidanceRule
所以这里有必要对它进行回顾。这里可以直达之前的文章。
【你好Ribbon】十一:Ribbon负载均衡服务选取规则组件IRule-客户端可配置规则ClientConfigEnabledRoundRobinRule
简单对ZoneAvoidanceRule
的静态方法getAvailableZones()
做一个回顾(也可点击上面连接看更详细介绍):
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());
/**如果只有一个zone 直接返回不需要判断*/
if (availableZones.size() == 1) {
return availableZones;
}
/**保存最坏的zone*/
Set<String> worstZones = new HashSet<String>();
/**所有的zone中平均负载最高值*/
double maxLoadPerServer = 0;
/**是否有一部分可用的负载 false时意为全部可用 true的时候有限可用*/
boolean limitedZoneAvailability = false;
for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
String zone = zoneEntry.getKey();
ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
int instanceCount = zoneSnapshot.getInstanceCount();
/**如果zone内没有可用实例 从可用的zone中移除 将部分可用赋值为true*/
if (instanceCount == 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
double loadPerServer = zoneSnapshot.getLoadPerServer();
/**如果熔断率大于传过来的阈值 或者实例的平均负载小于0 也从可用的zone中移除
* 前半部分条件:熔断实例数/总实例数 >= 阈值(阈值为0.99999d) 也就差不多是所有的server都被
* 熔断了 该zone才不可用
* 后半部分条件:loadPerServer < 0 这里是什么意思?什么情况下loadPerServer才会小于0 那就要
* 去看看 LoadBalancerStats#getZoneSnapshot()
* if (circuitBreakerTrippedCount == instanceCount)
* loadPerServer = -1
* 也就是所有的实例都熔断了 那么loadPerServer久没有任何意义了 所以就赋值为-1
* 那前半部分的条件和后半部分岂不是一样的?前半部分是可以配置的 通过下面
* ZoneAwareNIWSDiscoveryLoadBalancer.clientName.avoidZoneWithBlackoutPercetage 默认是0.99999d
* */
if (((double) zoneSnapshot.getCircuitTrippedCount())
/ instanceCount >= triggeringBlackoutPercentage
|| loadPerServer < 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
/**当前的平均负载和上一次最大的负载一样大 那就将当前的区域加入 最坏负载集合
* 这样worstZones 就会有多个值 一般情况下worstZones这个集合会出现一个值
* 但是如果有两个区域负载相同时 就会有2个值 以此类推
* */
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);
}
}
}
}
/**如果全部区域都可用 并且最大负载没达到阈值 返回全部的可用区域
* triggeringLoad 负载阈值 默认值为0.2 通过
* ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold来设置
* 请求量/实例数 = 平均负载阈值 默认值为0.2有点不合理 加入一个区域10台服务器 有两个活跃的请求
* 这个zone的平均负载就是0.2。超过两个请求进来就会负载过重。
*
* 这么设置的后果:假如有两个zone区域 总会有一个被移除掉 会导致负载均衡策略完全失效。
* (对于这个结论我们可以在AvailableZonesTest中模拟)
*
* 所以生产环境如果有多个zone区域的话 建议不要使用默认值
* ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold = 50
* */
if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
// zone override is not needed here
return availableZones;
}
/**
* 这一步是吧最坏的负载区域移除掉 worstZones这个集合上面说了 一般会是一个值
- List item
* 当出现最大负载有多个区域时 才会有多个值
*
* 程序走到这里 availableZones肯定是不止一个zone的。因为在最开始 就进行了判断
* availableZones 如果仅有一个值 直接返回了。
* */
String zoneToAvoid = randomChooseZone(snapshot, worstZones);
if (zoneToAvoid != null) {
availableZones.remove(zoneToAvoid);
}
return availableZones;
}
以上是选择可用区域的逻辑比较重要。代码比注释较多 需要用心看。
下面简要的回顾一下选择可用区域的逻辑:
两个配置:
ZoneAwareNIWSDiscoveryLoadBalancer.<clientName>.triggeringLoadPerServerThreshold
平均负载阈值。默认0.2
这个值我们上面也说了不合理ZoneAwareNIWSDiscoveryLoadBalancer.<clientName>.avoidZoneWithBlackoutPercetage
区域熔断率 默认0.99999d
相当于是要全部熔断才会触发该值。
触发移除zone
的条件:
zone
没有服务器- 同一区域的
熔断数 / 实例总数 > 0.9999d
可配置 - 所有的服务器都熔断了
- 如果所有区域的最大平均负载大于 配置的平均负载值
0.2
随机从worstZones
移除一个区域
chooseServer
@Override
public Server chooseServer(Object key) {
//ENABLED是一个开关 如果没有开启 或者区域数量小于等于1 就不使用该负载均衡器使用DynamicServerListLoadBalancer
//这也是SpringCloud选择它的原因
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
//从LoadBalancerStats获取区域的快照信息
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
//平均负载值阈值
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
//获取可用的区域列表 这个我们上面做了介绍
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
//这里一个判断要注意 如果可用区域没有任何问题 也就是可用区域里面的服务都很正常。
//使用父类的chooseServer 也就是BaseLoadBalancer的chooseServer方法
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
//随机选择一个区域
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
if (zone != null) {
//获取当前区域对应的负载均衡
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
}
if (server != null) {
return server;
} else {
return super.chooseServer(key);
}
}
ZoneAwareNIWSDiscoveryLoadBalancer.enabled
该属性来控制是否启用ZoneAwareLoadBalancer
ZoneAvoidanceRule.createSnapshot
通过LoadBalancerStats
来获取区域对应的快照信息ZoneAvoidanceRule.getAvailableZones
获取可用的区域ZoneAvoidanceRule.randomChooseZone
随机获取一个可用的区域BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone)
获取对应区域的负载均衡器 这里ZoneAwareLoadBalancer
是为每一个zone
分配一个ILoadBalancer
。
为每一个Zone分配一个ILoadBalancer
//该方法是父类更新服务列表的时候调用的
protected void setServerListForZones(
Map<String, List<Server>> zoneServersMap) {
LOGGER.debug("Setting server list for zones: {}", zoneServersMap);
getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
}
//该方法是子类对setServerListForZones的重写
@Override
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
super.setServerListForZones(zoneServersMap);
if (balancers == null) {
balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
}
for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
String zone = entry.getKey().toLowerCase();
getLoadBalancer(zone).setServersList(entry.getValue());
}
for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
existingLBEntry.getValue().setServersList(Collections.emptyList());
}
}
}
setServerListForZones
这个方法是重写父类的方法,除了向LoadBalancerStats
中设置区域服务列表映射之外 还为每一个Zone
分配了一个ILoadBalancer
。
看一下ZoneAwareLoadBalancer
如何为每一个Zone
指定一个ILoadBalancer
的。进入上面的getLoadBalancer(zone)
方法:
BaseLoadBalancer getLoadBalancer(String zone) {
zone = zone.toLowerCase();
//从缓存中获取对应的负载均衡器
BaseLoadBalancer loadBalancer = balancers.get(zone);
if (loadBalancer == null) {
//赋值规则 如果当前规则不存在使用 AvailabilityFilteringRule
IRule rule = cloneRule(this.getRule());
//实例化一个 BaseLoadBalancer
loadBalancer = new BaseLoadBalancer(this.getName() + "_" + zone, rule, this.getLoadBalancerStats());
BaseLoadBalancer prev = balancers.putIfAbsent(zone, loadBalancer);
if (prev != null) {
loadBalancer = prev;
}
}
return loadBalancer;
}
上面代码是一个创建BaseLoadBalancer
并加入缓存的一个过程。IRule
使用的是当前设置的,如果没有设置使用AvailabilityFilteringRule
。那为什么要创建一个BaseLoadBalancer
作为zone
的默认的负载均衡器,其实这里只是使用BaseLoadBalancer
的chooseServer
的能力,而这个能力DynamicServerListLoadBalancer
并没有对BaseLoadBalancer
进行扩展。
示例
自定义ServerList
static class ServerListForHardCode implements ServerList<Server>{
static List<Server> allServer = new ArrayList<>();
static {
allServer.add(createServer("HN" , 1));
allServer.add(createServer("HN" , 2));
allServer.add(createServer("HB" , 1));
allServer.add(createServer("HB" , 2));
}
@Override
public List<Server> getInitialListOfServers() {
return allServer;
}
@Override
public List<Server> getUpdatedListOfServers() {
return allServer;
}
private static Server createServer(String zone, int index) {
Server server = new Server("www.baidu" + zone + ".com", index);
server.setZone(zone);
return server;
}
}
模拟请求让增加服务器的负载
private void request(ZoneAwareLoadBalancer zoneAwareLoadBalancer){
for (int i = 0; i < 3; i++) {
new Thread(()->{
while (true){
LoadBalancerStats loadBalancerStats = zoneAwareLoadBalancer.getLoadBalancerStats();
zoneAwareLoadBalancer.getAllServers().forEach(server -> {
ServerStats serverStats = loadBalancerStats.getServerStats(server);
serverStats.incrementActiveRequestsCount();
serverStats.incrementNumRequests();
long rt = randomTime(500);
sleep(rt);
// 请求结束, 记录响应耗时
serverStats.noteResponseTime(rt);
serverStats.decrementActiveRequestsCount();
});
}
}).start();
}
}
测试 AwareLoadBalaner
@Test
public void zoneAwareLoadBalaner(){
IClientConfig config = DefaultClientConfigImpl.getClientConfigWithDefaultValues("coredy");
IPing ping = new DummyPing();
ServerListFilter filter = new ZoneAffinityServerListFilter();
IRule rule = new ZoneAvoidanceRule();
ServerListUpdater updater = new PollingServerListUpdater();
ServerList serverList = new ServerListForHardCode();
ZoneAwareLoadBalancer zoneAwareLoadBalancer =
new ZoneAwareLoadBalancer(config , rule , ping , serverList ,filter , updater);
//模拟请求 让服务有一定的负载
request(zoneAwareLoadBalancer);
while (true){
sleep(1000);
System.out.println(zoneAwareLoadBalancer.chooseServer(null));
}
}
打印结果:
www.baiduHB.com:2
hn 平均负载:0.0
hb 平均负载:1.5
计算之后的可用区域:[hn]
www.baiduHN.com:2
hn 平均负载:1.5
hb 平均负载:0.0
计算之后的可用区域:[hb]
www.baiduHB.com:1
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:1
hn 平均负载:1.0
hb 平均负载:0.5
计算之后的可用区域:[hb]
www.baiduHB.com:2
hn 平均负载:0.5
hb 平均负载:1.0
计算之后的可用区域:[hn]
www.baiduHN.com:2
上面模拟3个线程不停的向服务器发起请求,可以看到上面的结果 计算出的可用区域永远是 一个。因为我们的区域负载很容易就超过了阈值 0.2所以一般情况下都是会过滤掉一个区域的。这里应该是默认配置设置的不合理造成的,所以我们在生产环境可以自定义该配置
结语
ZoneAwareLoadBalancer
主要就是对zone
具有识别能力。可以过滤掉熔断较高 负载较高的区域。它是SpringCloud默认的负载均衡器。