本文基于Spring Cloud Fincheley SR3
背景介绍
目前项目多个区域多个集群,这些集群共用同一个Eureka集群。通过设置eureka.instance.metadata-map.zone设置不同实例所属的zone,zone之间不互相调用,只有zone内部调用(其实这里用zone做了集群隔离,实际上集群肯定是跨可用区的,这里的eureka中的zone在我们项目里面并不是可用区的概念)。
对应配置(假设调用的服务名字是service-provider)
1# 当前实例所在区域,同时由于NIWSServerListFilterClassName配置的是ZoneAffinityServerListFilter并且EnableZoneAffinity和EnableZoneExclusivity都是true,只有处于同一个zone的实例才会被调用 2eureka.instance.metadata-map.zone=local 3 4service-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.AvailabilityFilteringRule 5service-provider.ribbon.NIWSServerListFilterClassName=com.netflix.loadbalancer.ZoneAffinityServerListFilter 6service-provider.ribbon.EnableZoneAffinity=true 7service-provider.ribbon.EnableZoneExclusivity=true 8 9service-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.AvailabilityFilteringRule 10 11# ribbon.ServerListRefreshInterval时间内有多少断路次数就触发断路机制 12niws.loadbalancer.service-provider.connectionFailureCountThreshold=3 13niws.loadbalancer.service-provider.circuitTripTimeoutFactorSeconds=10 14niws.loadbalancer.service-provider.circuitTripMaxTimeoutSeconds=30
但是,统一管理后台就比较麻烦了。理想情况下,应该是每个微服务做自己的管理接口封装为OpenFeignClient给管理后台调用,但是在这种场景下,只能每个集群部署一个管理后台。这样很不方便。
能不能通过简单地改造还有配置,实现传入zone来指定OpenFeignClient调用哪个zone的实例呢?
分析
首先,Eureka是同一个集群。在Eureka上面有service-provider的所有不同zone的实例信息
Ribbon拉下来的本地缓存,是有定时任务从EurekaClient中拉取的(参考我的另一个系列:https://blog.csdn.net/zhxdick/article/category/7290495和https://blog.csdn.net/zhxdick/article/category/7367278对于Ribbon基本组成和Eureka联系的部分)
拉下来之后,通过NIWSServerListFilter进行过滤,如果我们制定过滤类为com.netflix.niws.loadbalancer.DefaultNIWSServerListFilter,那么就是什么也不过滤,直接返回从Eureka上面拉取的,也就是返回所有zone的所有对应实例,这里放上源码:
DynamicServerListLoadBalancer.java
1public void updateListOfServers() { 2 List<T> servers = new ArrayList<T>(); 3 if (serverListImpl != null) { 4 servers = serverListImpl.getUpdatedListOfServers(); 5 LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", 6 getIdentifier(), servers); 7 8 if (filter != null) { 9 //通过指定NIWSServerListFilter过滤 10 servers = filter.getFilteredListOfServers(servers); 11 LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", 12 getIdentifier(), servers); 13 } 14 } 15 updateAllServerList(servers); 16}
默认的LoadBalancer是什么呢?
通过查看org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration的源代码:
1public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { 2 return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater)); 3 }
我们知道,只要没自定义(通过@RibbonClient注解),或者配置(通过ribbon.NFLoadBalancerClassName),默认就是ZoneAwareLoadBalancer。注意这里构造器也和其他的LoadBalancer不一样,其他的都是调用IClientConfigAware接口方法,这里是直接构造器。
ZoneAwareLoadBalancer的选择Server源码:
1if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { 2 logger.debug("Zone aware logic disabled or there is only one zone"); 3 return super.chooseServer(key); 4} 5Server server = null; 6try { 7 LoadBalancerStats lbStats = getLoadBalancerStats(); 8 Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); 9 logger.debug("Zone snapshots: {}", zoneSnapshot); 10 if (triggeringLoad == null) { 11 triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty( 12 "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d); 13 } 14 15 if (triggeringBlackoutPercentage == null) { 16 triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty( 17 "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d); 18 } 19 Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get()); 20 logger.debug("Available zones: {}", availableZones); 21 if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) { 22 //核心看这里,我们只要指定了zone,而不是随机,就能通过getLoadBalancer获取到对应zone的loadbalancer从而返回对应zone的实例 23 String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones); 24 logger.debug("Zone chosen: {}", zone); 25 if (zone != null) { 26 BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone); 27 server = zoneLoadBalancer.chooseServer(key); 28 } 29 } 30} catch (Exception e) { 31 logger.error("Error choosing server using zone aware logic for load balancer={}", name, e); 32} 33if (server != null) { 34 return server; 35} else { 36 logger.debug("Zone avoidance logic is not invoked."); 37 return super.chooseServer(key); 38}
我们来实现我们自己的LoadBalancer,扩展ZoneAwareLoadBalancer即可
实现
1package com.netflix.loadbalancer; 2 3import com.netflix.client.config.IClientConfig; 4import lombok.extern.log4j.Log4j2; 5import org.apache.commons.lang.StringUtils; 6 7@Log4j2 8public class ZoneChosenLoadBalancer<T extends Server> extends ZoneAwareLoadBalancer { 9 //通过ThreadLocal指定Zone,所以不能开启Hystrix 10 //所以配置:feign.hystrix.enabled=false 11 //开启hystrix会导致切换线程执行 12 private static ThreadLocal<String> zoneThreadLocal = new ThreadLocal<>(); 13 14 public static void setZone(String zone) { 15 zoneThreadLocal.set(zone); 16 } 17 18 /** 19 * 必须调用这个方法传入对应的Bean初始化,其他构造器是不完整的 20 * @see org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration 21 * @param clientConfig 22 * @param rule 23 * @param ping 24 * @param serverList 25 * @param filter 26 * @param serverListUpdater 27 */ 28 public ZoneChosenLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList serverList, ServerListFilter filter, ServerListUpdater serverListUpdater) { 29 super(clientConfig, rule, ping, serverList, filter, serverListUpdater); 30 } 31 32 @Override 33 public Server chooseServer(Object key) { 34 try { 35 String zone = zoneThreadLocal.get(); 36 if (StringUtils.isBlank(zone)) { 37 log.info("zone is blank, use base loadbalancer"); 38 return super.chooseServer(key); 39 } 40 BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone); 41 Server server = zoneLoadBalancer.chooseServer(key); 42 if (server != null) { 43 return server; 44 } else { 45 log.info("server is null for zone {}, use base loadbalancer", zone); 46 return super.chooseServer(key); 47 } 48 } finally { 49 //无论如何都要remove 50 zoneThreadLocal.remove(); 51 } 52 } 53} 54
配置类(注意不能通过文件配置实现类,走IClientConfigAware,上面源代码里说明了原因,ZoneAwareLoadBalancer的构造本来就特殊):
1import com.netflix.loadbalancer.MultiZoneLoadBalancerConfiguration; 2import org.springframework.cloud.netflix.ribbon.RibbonClient; 3import org.springframework.context.annotation.Configuration; 4 5@Configuration 6//name对应要调用的微服务 7@RibbonClient(name = "service-provider", configuration = MultiZoneLoadBalancerConfiguration.class) 8public class ServiceScaffoldProviderLoadBalancerConfiguration { 9} 10 11 12package com.netflix.loadbalancer; 13 14import com.netflix.client.config.IClientConfig; 15import org.springframework.context.annotation.Bean; 16import org.springframework.context.annotation.Configuration; 17 18@Configuration 19public class MultiZoneLoadBalancerConfiguration { 20 @Bean 21 public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { 22 return new ZoneChosenLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater); 23 } 24} 25
需要修改的配置:
1#关闭feign hystrix 2feign.hystrix.enabled=false 3#指定对应微服务的list不过滤 4service-provider.ribbon.NIWSServerListFilterClassName=com.netflix.niws.loadbalancer.DefaultNIWSServerListFilter