在LoadBalancer的学习中,我们最后还看到了filter的身影,接下来就在本文一探究竟
为什么要了解?
因为在运行过程中,并不是每台Server一直都持续可用,另外多台Server很有可能分部在不同的可用区zone,而很多时候我们希望是获取到同区域的机器以加速访问,这些都是交由由ServerListFilter来完成的。
回顾一下代码:
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
找到filter的定义
volatile ServerListFilter<T> filter;
public interface ServerListFilter<T extends Server> {
public List<T> getFilteredListOfServers(List<T> servers);
}
查看继承结构
AbstractServerListFilter
这是一个抽象过滤器,在这里定义了过滤时需要的一个重要依据对象LoadBalancerStats,在我们之前介绍过,该对象存储了关于负载均衡器的一些属性和统计信息等
public abstract class AbstractServerListFilter<T extends Server> implements ServerListFilter<T> {
private volatile LoadBalancerStats stats;
public void setLoadBalancerStats(LoadBalancerStats stats) {
this.stats = stats;
}
public LoadBalancerStats getLoadBalancerStats() {
return stats;
}
}
ZoneAffinityServerListFilter
它借助于ZoneAffinityPredicate来过滤出和zone相关的服务器,即:只留下指定zone下的Server们
看一下它的成员属性
private volatile boolean zoneAffinity = DefaultClientConfigImpl.DEFAULT_ENABLE_ZONE_AFFINITY;
private volatile boolean zoneExclusive = DefaultClientConfigImpl.DEFAULT_ENABLE_ZONE_EXCLUSIVITY;
private DynamicDoubleProperty activeReqeustsPerServerThreshold;
private DynamicDoubleProperty blackOutServerPercentageThreshold;
private DynamicIntProperty availableServersThreshold;
private Counter overrideCounter;
private ZoneAffinityPredicate zoneAffinityPredicate = new ZoneAffinityPredicate();
private static Logger logger = LoggerFactory.getLogger(ZoneAffinityServerListFilter.class);
- zoneAffinity:控制是否要开启ZoneAffinity的开关,默认是false
可以通过EnableZoneAffinity来配置。也就是xxx.ribbon.EnableZoneAffinity或者全局默认ribbon.EnableZoneAffinity - zoneExclusive:同样是可以控制是否要开启ZoneAffinity的开关。同时它在Filter过滤Server的时候还起到开关的通,默认是false
可以通过EnableZoneExclusivity这个key进行配置(全局or定制) - activeReqeustsPerServerThreshold:最大负载阈值,实例平均负载>=0.6,可通过.ribbon.zoneAffinity.maxLoadPerServer = xxx来配置
- blackOutServerPercentageThreshold:故障实例百分比(断路器断开数/实例数量)>=0.8,可通过.ribbon.zoneAffinity.maxBlackOutServesrPercentage= xxx来配置
- availableServersThreshold:可用Server的阈值,默认值是2。
可通过.ribbon.zoneAffinity.minAvailableServers = xxx来配置 - zoneAffinityPredicate:断言器,用于筛选出配置的指定zone的server们。这在上篇文章中重点阐述过
- zone:当前的zone。来自于配置的上下文:ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone)
接下来找到getFilteredListOfServers()方法看一下过滤逻辑
public List<T> getFilteredListOfServers(List<T> servers) {
//分区存在并且区域感知的开关打开,服务器个数大于0
if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){
//断言过滤
List<T> filteredServers = Lists.newArrayList(Iterables.filter(
servers, this.zoneAffinityPredicate.getServerOnlyPredicate()));
//如果满足shouldEnableZoneAffinity 的条件则返回过滤后的列表
if (shouldEnableZoneAffinity(filteredServers)) {
return filteredServers;
} else if (zoneAffinity) {
overrideCounter.increment();
}
}
return servers;
}
此处有两个个重要方法
1,this.zoneAffinityPredicate.getServerOnlyPredicate
public class ZoneAffinityPredicate extends AbstractServerPredicate {
private final String zone = ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone);
public ZoneAffinityPredicate() {
}
@Override
public boolean apply(PredicateKey input) {
Server s = input.getServer();
String az = s.getZone();
if (az != null && zone != null && az.toLowerCase().equals(zone.toLowerCase())) {
return true;
} else {
return false;
}
}
}
通过比较服务器的分区和当前分区是否一致来决定是否返回
2,shouldEnableZoneAffinity
private boolean shouldEnableZoneAffinity(List<T> filtered) {
if (!zoneAffinity && !zoneExclusive) {
return false;
}
if (zoneExclusive) {
return true;
}
LoadBalancerStats stats = getLoadBalancerStats();
if (stats == null) {
return zoneAffinity;
} else {
logger.debug("Determining if zone affinity should be enabled with given server list: {}", filtered);
ZoneSnapshot snapshot = stats.getZoneSnapshot(filtered);
double loadPerServer = snapshot.getLoadPerServer();
int instanceCount = snapshot.getInstanceCount();
int circuitBreakerTrippedCount = snapshot.getCircuitTrippedCount();
if (((double) circuitBreakerTrippedCount) / instanceCount >= blackOutServerPercentageThreshold.get()
|| loadPerServer >= activeReqeustsPerServerThreshold.get()
|| (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold.get()) {
logger.debug("zoneAffinity is overriden. blackOutServerPercentage: {}, activeReqeustsPerServer: {}, availableServers: {}",
new Object[] {(double) circuitBreakerTrippedCount / instanceCount, loadPerServer, instanceCount - circuitBreakerTrippedCount});
return false;
} else {
return true;
}
}
}
- 若你配置了zoneAffinity或者zoneExclusive任何一个为true,则将开启此筛选逻辑
- 若你是zoneExclusive=true,说明你同意这种排除逻辑,那就直接生效开启返回true
- circuitBreakerTrippedCount/instanceCount >= blackOutServerPercentageThreshold,也就是说被熔断的占比率超过0.8,也就是80%的机器都被熔断了,那就返回false(毕竟此zone已基本不可用了,那还是返回所有Server保险点)
- loadPerServer >= activeReqeustsPerServerThreshold,若平均负载超过0.6,那就返回fasle(因为没必要把负载过高的zone返回出去,还是返回所有Server较好)
- (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold,如果“活着的(没熔断的)”实例总数不足2个(仅有1个了),那就返回false
- 若以上三种情况均没发生,那就返回true
这么做的目的是:担心你配置的zone里面的Server情况不乐观,如果这个时候只返回该zone的Server的话,反倒不好,还不如把所有Server都返回更为合适。
ServerListSubsetFilter
它将负载平衡器使用的服务器数量限制为所有服务器的子集。如果服务器场很大(例如,数百个)并且使用其中的每一个并且将连接保持在 http 客户端的连接池中是不必要的,那么这很有用。它还具有通过比较总网络故障和并发连接来驱逐相对不健康的服务器的能力
本文不做重点,感兴趣的小伙伴可以自行研究
ZonePreferenceServerListFilter
Spring Cloud整合时新增的过滤器。它是Spring Cloud默认使用的筛选器。它的特点是:能够优先过滤出与请求调用方处于同区域的服务实例。
public List<Server> getFilteredListOfServers(List<Server> servers) {
List<Server> output = super.getFilteredListOfServers(servers);
// 若指定了zone,并且output.size() == servers.size()
// 也就说父类没有根据zone进行过滤的话,那这里就会继续处理逻辑
if (this.zone != null && output.size() == servers.size()) {
List<Server> local = new ArrayList<>();
for (Server server : output) {
if (this.zone.equalsIgnoreCase(server.getZone())) {
local.add(server);
}
}
if (!local.isEmpty()) {
return local;
}
}
return output;
}
需要注意的是虽然它作为Spring Cloud的默认筛选器,若父类没筛选出来,它简单的粗暴的仅根据zone进行选择,其实这么做是可能会有问题的:万一这台Server负载很高?万一熔断了呢?万一只有一个Server实例呢?这些都是父类足了过滤而它自己却没有去关心的指标
ZoneAwareLoadBalancer
仍然是在上一篇文章中,我们讲了DynamicServerListLoadBalancer,而ZoneAwareLoadBalancer是对DynamicServerListLoadBalancer的扩展。在DynamicServerListLoadBalancer中,我们可以看到它并没有重写选择具体服务实例的chooseServer函数,所以它依然会采用在BaseLoadBalancer中实现的算法。使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例,该算法实现简单并没有区域(Zone)的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性的产生跨区域访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问实例,所以在多区域部署的情况下会有一定的性能问题,而该负载均衡器则可以避免这样的问题。那么是如何实现的呢?
首先,在ZoneAwareLoadBalancer中,我们可以发现,它并没有重写setServerList,说明实现服务实例清单的更新主逻辑没有修改,但我们会发现它复写了setServerListForZones。看到这里可能会有点陌生,因为它并不是接口定义的必备函数,所以我们不妨去父类中DynamicServerListLoadBalancer寻找一下该函数,我们可以找到下面的定义:
public void setServersList(List lsrv) {
super.setServersList(lsrv);
List<T> serverList = (List<T>) lsrv;
Map<String, List<Server>> serversInZones = new HashMap<String, List<Server>>();
for (Server server : serverList) {
// make sure ServerStats is created to avoid creating them on hot
// path
getLoadBalancerStats().getSingleServerStat(server);
String zone = server.getZone();
if (zone != null) {
zone = zone.toLowerCase();
List<Server> servers = serversInZones.get(zone);
if (servers == null) {
servers = new ArrayList<Server>();
serversInZones.put(zone, servers);
}
servers.add(server);
}
}
setServerListForZones(serversInZones);
}
setServerListForZones函数的调用位于更新服务实例清单函数setServersList的最后,同时从其实现的内容来看,它在父类DynamicServerListLoadBalancer中的作用是根据按区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并放入Map集合中,每一个区域Zone对应一个ZoneStats,它用于存储每个Zone的一些状态跟统计信息
在ZoneAwareLoadBalancer中对setServerListForZones的复写如下:
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());
}
// check if there is any zone that no longer has a server
// and set the list to empty so that the zone related metrics does not
// contain stale data
for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
existingLBEntry.getValue().setServersList(Collections.emptyList());
}
}
}
可以看到,在该实现中创建了一个ConcurrentHashMap类型的balancers对象,他将用来存储每个Zone区域对应的负载均衡器new ConcurrentHashMap<String, BaseLoadBalancer>()
。而具体的负载均衡器的创建则是通过在下面的第一个循环中调用getLoadBalancer函数来完成的,同时在创建负载均衡器的时候,会创建它的规则(如果当前实现中没有IRule的实例,就创建一个AvaiilabilityFilteringRule规则;如果已经有具体实例,就克隆一个)。
BaseLoadBalancer getLoadBalancer(String zone) {
zone = zone.toLowerCase();
BaseLoadBalancer loadBalancer = balancers.get(zone);
if (loadBalancer == null) {
// We need to create rule object for load balancer for each zone
IRule rule = cloneRule(this.getRule());
loadBalancer = new BaseLoadBalancer(this.getName() + "_" + zone, rule, this.getLoadBalancerStats());
BaseLoadBalancer prev = balancers.putIfAbsent(zone, loadBalancer);
if (prev != null) {
loadBalancer = prev;
}
}
return loadBalancer;
}
private IRule cloneRule(IRule toClone) {
IRule rule;
if (toClone == null) {
rule = new AvailabilityFilteringRule();
} else {
String ruleClass = toClone.getClass().getName();
try {
rule = (IRule) ClientFactory.instantiateInstanceWithClientConfig(ruleClass, this.getClientConfig());
} catch (Exception e) {
throw new RuntimeException("Unexpected exception creating rule for ZoneAwareLoadBalancer", e);
}
}
return rule;
}
在创建完负载均衡器后又马上调用setServerList函数为其设置对应Zone区域的实例清单。而第二个循环则是对Zone区域中实例清单的检查,看看是否有Zone区域下已经没有实例了,是的话就将balancers中对应Zone区域的实例列表清空,该操作是为了后续选择节点时,防止过时的Zone区域统计信息干扰具体实例的选择算法
在了解了该负载均衡器是如何扩展服务实例清单的实现后,我们来看看它具体是如何选择服务实例,来实现对区域的识别的
public Server chooseServer(Object key) {
// 只有可用的Zone大于1的时候才会执行
// 否则直接执行父类的逻辑
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
// 为当前负载均衡器中所有的Zone区域分别创建快照,保存在map中,用于之后的算法
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);
}
// 获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
// 当获取的可用的Zone区域集合不为空,并且个数小于Zone区域总数,就随机选择一个
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
// 在确定了某个Zone区域后,则获取了对应Zone区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
我们最后看下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());
if (availableZones.size() == 1) {
return availableZones;
}
Set<String> worstZones = new HashSet<String>();
double maxLoadPerServer = 0;
boolean limitedZoneAvailability = false;
for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
String zone = zoneEntry.getKey();
ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
int instanceCount = zoneSnapshot.getInstanceCount();
if (instanceCount == 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
double loadPerServer = zoneSnapshot.getLoadPerServer();
if (((double) zoneSnapshot.getCircuitTrippedCount())
/ instanceCount >= triggeringBlackoutPercentage
|| loadPerServer < 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
//实例平均负载最高的Zone区域
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);
}
}
}
}
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区域:
- 所属实例数为0的Zone;
- Zone内实例的平均负载小于0
loadPerServer <
- 实例故障率(断路器断开次数/实例数)大于等于阈值(默认为0.999999)
zoneSnapshot.getCircuitTrippedCount()) / instanceCount >= triggeringBlackoutPercentage
然后根据Zone区域的实例平均负载计算出最差的Zone区域,这里的最差指的是实例平均负载最高的Zone区域
如果在上面的过程中没有符合剔除要求的区域,同时实例最大平均负载小于阈值(默认为20%)maxLoadPerServer < triggeringLoad && !limitedZoneAvailability
,就直接返回所有Zone区域为可用区域。
否则,从最坏Zone区域中随机选择一个randomChooseZone
,将它从可用Zone集合中剔除
课后作业
如果线上服务下线,怎么做到实时更新注册中心和服务消费者的本地注册缓存?