由于笔者水平有限,整理的可能和您理解的略有偏差,还望在评论区指正并讨论。
本项目基于eureka+zuul+openfeign等组件搭建的微服务架构,因业务需求需要设计一套类似于灰度发布的路由转发,可以理解为就是根据接口参数路由到哪一个版本的服务,本系统将服务设计为
hd和prd,在每一个服务下面都会添加一个eureka的元数据信息如:-Deureka.instance.metadata-map.version=hd,就是根据这个参数实现的路由转换。
下面请看路由规则的代码:
路由规则器:
@Slf4j
public class GrayPublishRule extends ZoneAvoidanceRule {
private static String serviceVersion;
public GrayPublishRule() {
}
public GrayPublishRule(String serviceVersion) {
GrayPublishRule.serviceVersion = serviceVersion;
}
@Override
public Server choose(Object key) {
String routeVersion;
try {
routeVersion = HystrixRequestContextHolder.version.get();
} catch (Exception ex) {
routeVersion = serviceVersion;
}
log.info("[路由version]:{}", routeVersion);
if (StringUtils.isNotBlank(routeVersion)) {
List<Server> serverList = getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
List<Server> grayList = new ArrayList<>();
for (Server server : serverList) {
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String grayVersion = metadata.get("version");
if (StringUtils.equals(grayVersion, routeVersion)) {
grayList.add(server);
}
}
Optional<Server> optional = getPredicate().chooseRoundRobinAfterFiltering(grayList, key);
if (optional.isPresent()) {
return optional.get();
}
}
return super.choose(key);
}
}
配置类:
@Configuration
@RibbonClients(defaultConfiguration = LbConfig.class)
public class LbConfig {
@Autowired
private Environment environment;
@Bean
@ConditionalOnProperty(value = "version.hd")
public IRule rule() {
String version = environment.resolvePlaceholders("${eureka.instance.metadata-map.version:}");
return new GrayPublishRule(version);
}
}
因为要基于动态配置,当不需要这个灰度的功能时,直接动态干掉,无需修改代码就在方法上加了一个条件注解 @ConditionalOnProperty(value = "version.hd"),注意:此时是加在方法上的,此时路由是没有什么影响的,整个服务能正常工作,
但是当条件注解加载类上时,就会出现问题
@Configuration
@RibbonClients(defaultConfiguration = LbConfig.class)
@ConditionalOnProperty(value = "version.hd")
public class LbConfig {
@Autowired
private Environment environment;
@Bean
public IRule rule() {
String version = environment.resolvePlaceholders("${eureka.instance.metadata-map.version:}");
return new GrayPublishRule(version);
}
}
此时服务启动后,你调用第一个服务没问题,但调用其它服务就会报404或者,无法路由到服务,报重定向错误
但我第一次通过swagger访问dfs服务时正常
当我访问一个未启动的服务后,此时服务报错,因为未启动mock服务,正常
当我再次访问dfs服务,dfs也无法访问了
此时这个现象困惑了很久,当我去掉类上的条件注解就能正常工作了。
@Configuration
@RibbonClients(defaultConfiguration = LbConfig.class)
//@ConditionalOnProperty(value = "version.hd")
public class LbConfig {
@Autowired
private Environment environment;
@Bean
public IRule rule() {
String version = environment.resolvePlaceholders("${eureka.instance.metadata-map.version:}");
return new GrayPublishRule(version);
}
}
那为什么加上就不能生效了呢,但我又想实现这个条件注解使我的路由能动态生效,此时萌生的想法就是面向百度,但百度了一会就没找到相关文章,可能和我的描述有关,因为我也不知道该怎么描述~~。
后来就干脆去读一下相关源码,看一下ribbon是怎么个工作机制,才发现了问题所有。咱们接着往下看:
一般争对springboot的项目来说,若集成了某个组件,都会有一个和自动配置相关的类,ribbon也不列外和ribbon相关的就是RibbonAutoConfiguration。
我们先来看看这个类的注解信息
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
}
1.@RibbonClients里面导入了一个@Import(RibbonClientConfigurationRegistrar.class)类,熟悉spring的伙伴都应该了解,这里面肯定会做一些beanDefinition相关的操作,这里就是将RibbonClients的配置类注入到BeanDefinnitionMap中,以我们配置的@RibbonClients为列name=default.net.szvoc.wbs.gray.LbConfig,beanDefinition=RibbonClientSpecification.class
public class RibbonClientSpecification implements NamedContextFactory.Specification {
//beanName
private String name;
//配置类
private Class<?>[] configuration;
public RibbonClientSpecification() {
}
2.这个SpringClintFactory至关重要,我们点击类去看翻译,
A factory that creates client, load balancer and client configuration instances. It creates a Spring ApplicationContext per client name, and extracts the beans that it needs from there. 可以看到它得意思是为每一个客户端都创建一个spring ApplicationContext,并从中获取bean,意思就是要为我们每一个要路由的客户端创建一个spring上下文。
3.可以看到LoadBalancerClient 是传入了一个springClientFactory,这里面有一个choose方法,这就是找服务的方法,因为我们调用服务报错,很明显是找服务的时候,出现的问题,那么这个就是我们查找问题的入口,excute负责执行请求,若有时间这个执行流程可在后面的文章在整理出来
接下来我们就来看一看这个方法
ServiceInstanceChooser#choose
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
//选择那个服务
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
//获取负载均衡器
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
//获取服务
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
if (loadBalancer == null) {
return null;
}
// Use 'default' on a null hint, or just pass it on?
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
从上到下我们可以找到找服务的流程,先通过clientFactory获取到一个LoadBalancer负载均衡器,那么我们先看一下是如何获取到负载均衡器的
public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}
@Override
public <C> C getInstance(String name, Class<C> type) {
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
//通过super.getInstance(name, type);调到这里
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
return context.getBean(type);
}
return null;
}
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
//获取相关得容器
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
//创建容器
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
我们可以通过getInstance中看到,对象实列是从spring容器中获取的 容器又是从一个map中获取的 ,那么该容器是如何创建的呢?
for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } }
看这个代码,通过遍历configurations来执行context.register(configuration);注册配置类,那么这个配置类是什么东西呢
这个配置是通过
SpringClientFactory的父类NamedContextFactory调用如下方法设置进去的
public void setConfigurations(List<C> configurations) { for (C client : configurations) { this.configurations.put(client.getName(), client); } }
List<C> configurations参数又是通过RibbonAutoConfiguration这个自动配置类中通过
@Autowired(required = false) private List<RibbonClientSpecification> configurations = new ArrayList<>(); 自动注入的,那么这个RibbonClientSpecification是从那里来的呢?
可以看到上面我们讨论过@RibbonClients注解这个注解的作用就是往IOC容器中添加一个
RibbonClientSpecification类型的Bean,那这里就很好的关联起来了,遍历的就是@RibbonClients配置的类。
entry.getValue().getConfiguration()获取到的就是我们配置的LBConfig.class类,
context.register(configuration)意思就是这个类作为spring的配置类创建容器。
接下来就是问题的核心了:
这个类作为配置类的时候我们如果在类上加了条件注解
@Configuration @RibbonClients(defaultConfiguration = LbConfig.class) @ConditionalOnProperty(value = "version.hd") public class LbConfig {}
此时上下文中有version.hd这个配置吗?答案是肯定没有的 因为在这之前没看到任何相关属性设置到这个新创建的spring容器中。所以如果LbConfig根本不会注入到这个新创建的容器中,LbConfig不生效那么通过@Bean配置的类也不会注入到容器中。
那么为什么@ConditionalOnProperty(value = "version.hd")这个注解加在方法就能生效呢?
看下面这行代码
if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); // jdk11 issue // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101 context.setClassLoader(this.parent.getClassLoader()); }
这里居然设置了一个父容器,父容器是什么?父容器肯定是外面我们通过springbot的run()方法启动的容器,这个容器中含有version.hd这个属性
当设置父容器的时候会调用这个方法
public void setParent(@Nullable ApplicationContext parent) {
this.parent = parent;
if (parent != null) {
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}
这行代码getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
合并environment,我们知道这些属性都是存在于environment中,那把父亲的属性合并儿子,那么儿子就肯定会获取到version.hd这个属性了。在调用refresh()初始化上下文的时候,@Bean肯定添加到新容器中,就没什么好疑问的了。
其实这里还没完,如果spring容器存在父容器,若往该容器getBean()获取不到会向父容器getBean()。所以当获取IRule获取不到得时候这里所有存储客户端的spring ApplicationContext都会获取到同一个IRule类型的bean,因为共用的是一个bean,当别的客户端修改了IRule里面的东西,那么全部都会受影响,所以当调用mock服务报错时,在回过头去调用dfs服务时,dfs也跟着报错,其实此时IRule服务路由的时候还是路由到mock服务了,所以此时报错。当你的mock服务正常启动时,但是你的url是dfs的url,但你却去掉mock服务,那么肯定会报404.
所以接下来我们要分析这个IRule是如何被修改的呢?
在创建spring子容器时,还执行了一行代码,每一个子容器(即每一个服务)都会有一个RibbonClientConfiguration配置类,每个服务都会有自己的IRule和ILoadBalancer。
服务的拉取就是在ZoneAwareLoadBalancer()实列话过程发生的,主要就是在构造函数的这个方法 restOfInit(clientConfig)--enableAndInitLearnNewServersFeature()--updateAction()--updateListOfServers;
RibbonNacosAutoConfigration会根据RibbonClients注解往每个子容器配置一个bean--NacosRibbonClientConfiguration,这个bean主要是设置serviceId用于从nacos服务的拉取
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType)
往容器注册了一个defaultConfigType类型的配置类,这个defaultConfigType是什么呢?
通过代码分析,可以看到它就是RibbonClientConfiguration
@Configuration
@EnableConfigurationProperties
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {
//默认的负载均衡策略
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
//负载均衡器
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
}
在创建ribbonLoadBalancer时会根据构造函数注入rule,前面分析的如果@ConditionalOnProperty(value = "version.hd")加在配置类上,rule是不会添加到子容器的,但是会从父容中获取所以
public IRule ribbonRule(IClientConfig config)这个bean的配置逻辑是不会执行的,因为加了 @ConditionalOnMissingBean注解,这也是所以子容器共享一个rule的原因。
但是每一个容器都会有自己的ILoadBalancer负载均衡器。
但是所有的负载据衡器都会共享同一个IRule路由规则,每一个balancer都会维护一份自己的服务列表allServerList,
我自己实现的GrayPublishRule是继承了ZoneAvoidanceRule类,这个类实现了AbstractLoadBalancerRule
AbstractLoadBalancerRule维护了一个ILoadBalancer变量这个就是每一个子容器的负载均衡器,由于所有ILoadBalancer共享一个Rule,但Rule又维护了一个ILoadBalancer,当有一个新服务进来,就会创建一个新的ILoadBalancer,此时rule是共享变量,rule就会重新设置ILoadBalancer,ILoadBalancer会加载自己负载的服务,那么之前的也会受影响,又因为每一个balancer都有一份allServerList,此时所有的服务都共享一份allServerList,那肯定路由转换都会出问题,即谁最后修改了rule的ILoadBalancer就以那个服务的serverList为准。
一句话:IRule负载均衡策略不能全局共享,一个服务对应一个IRule,一个IRule对应一个ILoadBalancer