每日一句
办天下大事,贵实心,尤贵虚心。非真知灼见不能办事,亦不能论事,贵耳贱目,最足误事。
目录
上一篇我们介绍了Ribbon负载均衡器对服务列表的获取方式ServerList。
ServerListFilter
该接口用来过滤已配置或动态获取的服务器列表。
继承关系
- AbstractServerListFilter规定了从负载均衡器LoadBalancer的服务器列表里面筛选出可用的Server
- ZoneAffinityServerListFilter借助于ZoneAffinityPredicate来过滤出和zone相关的服务器
- ServerListSubsetFilter 它将负载均衡器使用的服务器数量限制为所有服务器的子集。
- ZonePreferenceServerListFilter SpringCloud提供的服务过滤器 可以优先调用筛选和调用方处于同一区域的服务列表
AbstractServerListFilter
可以看到AbstractServerListFilter只是添加了获取设置LoadBalancerStats的方法。也就是和负载均衡器关联。
关于LoadBalancerStats 我们后面会说,这里可以简单的理解存储了区域状态 服务状态的信息,信息包括请求总数,成功请求数,失败数,活跃请求数量等等一些服务器指标信息。我们可以通过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相关的服务器的过滤。
ZoneAffinityPredicate
该断言就是留下某个zone的服务器。这个zone就是通过DeploymentContext的ContextKey.zone来指定。逻辑很简单 如果没有指定zone 那么全部过滤掉。
示例
@Test
public void testZoneAffinityPredicate(){
ConfigurationManager.getConfigInstance().setProperty(DeploymentContext.ContextKey.zone.getKey() , "aliyun");
List<Server> serverList = new ArrayList<>();
serverList.add(new Server("172.16.10.1" , 8080){{setZone("aliyun");}});
serverList.add(new Server("172.16.10.2" , 8080){{setZone("aliyun");}});
serverList.add(new Server("172.16.10.3" , 8080){{setZone("tengxunyun");}});
serverList.add(new Server("172.16.10.4" , 8080){{setZone("tengxunyun");}});
ZoneAffinityPredicate predicate = new ZoneAffinityPredicate();
List<Server> eligibleServers = predicate.getEligibleServers(serverList);
assertThat(eligibleServers.size()).isEqualTo(2);
assertThat(eligibleServers.get(0).getHost()).isEqualTo("172.16.10.1");
assertThat(eligibleServers.get(1).getHost()).isEqualTo("172.16.10.2");
}
我们通过设置了DeploymentContext.ContextKey.zone.getKey()为aliyun 就过滤掉了非aliyun主机。源码太过简单 就不贴源码了。
ZoneAffinityServerListFilter示例
效果和上面的ZoneAffinityPredicate是一样的 因为ZoneAffinityServerListFilter内部就是使用ZoneAffinityPredicate来完成过滤的。
@Test
public void testZoneAffinityServerFilter(){
List<Server> serverList = getServerList();
IClientConfig clientConfig = DefaultClientConfigImpl.getClientConfigWithDefaultValues("user-center");
clientConfig.set(CommonClientConfigKey.EnableZoneAffinity , true);
ZoneAffinityServerListFilter serverListFilter = new ZoneAffinityServerListFilter(clientConfig);
List<Server> filteredListOfServers = serverListFilter.getFilteredListOfServers(serverList);
System.out.println(filteredListOfServers);
assertThat(filteredListOfServers.size()).isEqualTo(2);
assertThat(filteredListOfServers.get(0).getHost()).isEqualTo("172.16.10.1");
assertThat(filteredListOfServers.get(1).getHost()).isEqualTo("172.16.10.2");
}
CommonClientConfigKey.EnableZoneAffinity 和CommonClientConfigKey.EnableZoneExclusivity这两个参数默认为false。所以默认情况下 ZoneAffinityServerListFilter的逻辑是不会执行过滤的。
结合源码
//如果该方法返回true 那么最终会留下指定区域的服务器
//返回false 最终会返回所有的服务器
private boolean shouldEnableZoneAffinity(List<T> filtered) {
//这两个属性如果都是false 返回所有服务器
if (!zoneAffinity && !zoneExclusive) {
return false;
}
//如果现实开启 区域过滤 留下指定区域的服务器
if (zoneExclusive) {
return true;
}
//获取负载平台状态机 前面专门花了3篇文章来讲解 不了解的可以回去看一遍
LoadBalancerStats stats = getLoadBalancerStats();
//如果没有设置 则根据zoneAffinity设置的值来决定是否返回指定区域的服务器
//所以 如果开启了zoneAffinity 并且设置了stats 那么还需要判断区域的负载情况熔断状态
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();
//下面有三个表达式 其中满足一个就会返回所有服务器
//1,如果熔断服务器数量与总服务器数量比 大于0.8了 也就是80%的服务器都熔断了
//2,平均负载大于0.6 这个平均负载已经很小了 差不多两台服务器有一个请求的样子
//3,如果活着的服务器小于2个了
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;
}
}
}
//这个方法的逻辑主要是在shouldEnableZoneAffinity()中 根据shouldEnableZoneAffinity的返回值来决定要不要返回所有的服务器
@Override
public List<T> getFilteredListOfServers(List<T> servers) {
if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){
//下面这一行代码就和上面的zoneAffinityPredicate联系上了。留下指定区域的服务器。
List<T> filteredServers = Lists.newArrayList(Iterables.filter(
servers, this.zoneAffinityPredicate.getServerOnlyPredicate()));
if (shouldEnableZoneAffinity(filteredServers)) {
return filteredServers;
} else if (zoneAffinity) {
//这个是来统计 有朋友会问 这里是统计什么的?
//我们试想一下 假如zoneAffinity 开启 shouldEnableZoneAffinity一直返回false 那么说明服务器熔断 负载高 等情况 所以一定要监控
overrideCounter.increment();
}
}
return servers;
}
上面的shouldEnableZoneAffinity方法非常重要。下面有几个配置可以根据实际情况来设定。
ribbon.clientName.EnableZoneAffinity: 这个值不能完全决定是否返回指定区域的服务器。还需要根据LoadBalancerStats来决定。默认值为false。
ribbon.clientName.EnableZoneExclusivity: 这个值可以完全决定要不要返回指定区域的服务器。默认值为false 不建议设置为true。
ribbon.clientName.zoneAffinity.maxLoadPerServer: 最大平均负载 默认0.6 如果所有服务器的平均负载超过0.6 不会进行指定区域返回(感觉这个默认值 有问题)
ribbon.clientName.zoneAffinity.maxBlackOutServesrPercentage: 熔断比例 默认值0.8 ,如果80%的服务器都被熔断 也不进行指定区域返回
ribbon.clientName.zoneAffinity.minAvailableServers: 最大的活跃数量 默认值2 如果只有1台服务器存活 也不需要返回指定区域的服务器
ZoneAffinityServerListFilter还有两个子类ServerListSubsetFilter和ZonePreferenceServerListFilter
ServerListSubsetFilter
服务器列表过滤器,它将负载均衡器使用的服务器数量限制为所有服务器的子集。 *如果服务器场很大(例如,成百上千个)并且不需要使用它们中的每一个并且将连接保持在http客户端的连接池中,则这很有用。通过比较总的网络故障和并发连接,它还具有驱逐相对不健康服务器的能力。
该过滤器就是进一步对服务器进行剔除。
示例
这里我们模拟了 getServerList()返回40个服务器
@Test
public void serverListSubsetFilter(){
ServerListSubsetFilter filter = new ServerListSubsetFilter();
List<Server> filteredListOfServers = filter.getFilteredListOfServers(getServerList());
assertThat(filteredListOfServers.size()).isEqualTo(20);
}
上面之所以返回的服务器数量是20 是因为ribbon.clientName.ServerListSubsetFilter.size 默认值是20。下面有聊到。
源码解析
@Override
public List<T> getFilteredListOfServers(List<T> servers) {
/***获取父类过滤的结果***/
List<T> zoneAffinityFiltered = super.getFilteredListOfServers(servers);
Set<T> candidates = Sets.newHashSet(zoneAffinityFiltered);
/**currentSubset 这个值是上一次计算的结果**/
Set<T> newSubSet = Sets.newHashSet(currentSubset);
LoadBalancerStats lbStats = getLoadBalancerStats();
/**如果有值 遍历上一次计算的结果**/
for (T server: currentSubset) {
// this server is either down or out of service
/**有可能这次父类计算出来的结果不包含当前服务器了 那么久从当前的列表中删除**/
if (!candidates.contains(server)) {
newSubSet.remove(server);
} else {
ServerStats stats = lbStats.getSingleServerStat(server);
/** 下面这个条件就很苛刻 如果当前活跃的请求数量大于0 当前失败的数量大于0 那么就会移除服务器
* 留下更健康的服务器
*/
if (stats.getActiveRequestsCount() > eliminationConnectionCountThreshold.get()
|| stats.getFailureCount() > eliminationFailureCountThreshold.get()) {
newSubSet.remove(server);
// also remove from the general pool to avoid selecting them again
candidates.remove(server);
}
}
}
/**需要返回的服务器个数**/
int targetedListSize = sizeProp.get();
/**计算出 废弃的服务数量 因为上面做了几次移除 如果第一次就会是0**/
int numEliminated = currentSubset.size() - newSubSet.size();
/**最少需要废弃的服务器数量 20*0.1 **/
int minElimination = (int) (targetedListSize * eliminationPercent.get());
int numToForceEliminate = 0;
/**如果 计算出来的服务器数量大于上限**/
if (targetedListSize < newSubSet.size()) {
/** 计算出强制压缩服务器个数**/
numToForceEliminate = newSubSet.size() - targetedListSize;
} else if (minElimination > numEliminated) {/**如果最小的需要废弃服务器的数量大于 废弃服务器的数量**/
/** 计算需要强制压缩的个数**/
numToForceEliminate = minElimination - numEliminated;
}
/**如果强制压缩的个数 大于当前计算之后剩余服务器的数量 将压缩个数设置为当前服务器数量**/
if (numToForceEliminate > newSubSet.size()) {
numToForceEliminate = newSubSet.size();
}
/**下面就对服务器进行缩减 根据numToForceEliminate这个值**/
if (numToForceEliminate > 0) {
List<T> sortedSubSet = Lists.newArrayList(newSubSet);
/***这里需要注意 当前的filter重写了compare方法 会优先根据失败数量排序 然后根据负载排序**/
Collections.sort(sortedSubSet, this);
List<T> forceEliminated = sortedSubSet.subList(0, numToForceEliminate);
newSubSet.removeAll(forceEliminated);
candidates.removeAll(forceEliminated);
}
/**在强制删除服务器之后 可能值小于目标大小
* 然后就只是随机添加服务器 充数
* */
if (newSubSet.size() < targetedListSize) {
int numToChoose = targetedListSize - newSubSet.size();
candidates.removeAll(newSubSet);
if (numToChoose > candidates.size()) {
candidates = Sets.newHashSet(zoneAffinityFiltered);
candidates.removeAll(newSubSet);
}
List<T> chosen = randomChoose(Lists.newArrayList(candidates), numToChoose);
for (T server: chosen) {
newSubSet.add(server);
}
}
currentSubset = newSubSet;
return Lists.newArrayList(newSubSet);
}
ZonePreferenceServerListFilter
SpringCloud扩展的一个过滤器,能够优先筛选于请求在同一区域的服务器。也是SpringCloud默认的过滤器。
这个也是很简单 我们就不适用案例说明了
@Override
public List<Server> getFilteredListOfServers(List<Server> servers) {
List<Server> output = super.getFilteredListOfServers(servers);
//如果父过滤器没有整明白 没有做过滤逻辑 那么才会做筛选具体某一个区域的服务器
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;
}
其实个人觉得这个过滤器和他的父类做得事情是一样的 上面说ZoneAffinityServerListFilter的时候 它有一个配置ribbon.clientName.EnableZoneExclusivity 默认为false 如果开启 则会强制返回指定区域的服务。那这个类其实目的就是 如果父类没有做出筛选 它会强制筛选指定区域。如果默认启用这个类的话 其实和我们开启ribbon.clientName.EnableZoneExclusivity这个配置没有区别的。我们上面也说了 不建议把这个配置开启 这样的话就不会根据负载均衡状态机里面的指标来做进一步的判断的。
所以这个扩展的filter作为默认filter是否真的合适还有待商榷。有特殊需求最好还是能够自定义。
总结
对于Ribbon的ServerListFilter我们就到这里了。ZoneAffinityServerListFilter是核心。其他的扩展出来的都比较简单。
还有就是几个配置
ribbon.clientName.EnableZoneAffinity 是指定区域的开关,但是无法直接决定是否返回指定区域的配置。还需要根据负载均衡器的各项指标来决定
ribbon.clientName.EnableZoneExclusivity 可以直接确定返回指定区域的服务列表。但是个人建议不要开启。
ribbon.clientName.zoneAffinity.maxLoadPerServer最大的平均负载,当EnableZoneAffinity为true 、EnableZoneExclusivity为false的时候 如果所有机器的平均负载大于该值 将不会返回指定区域。注意该默认值是0.6 不知道是不是开发者的疏忽 我是觉得这个值由问题的。
ZonePreferenceServerListFilter:这个过滤器为SpringCloud扩展的过滤器 但我觉得它没有让人眼前一亮。反而有点累赘的感觉。