基于springcloud开发的微服务,由于条件注解@ConditionalOnProperty导致负载均衡路由失效的问题

由于笔者水平有限,整理的可能和您理解的略有偏差,还望在评论区指正并讨论。

本项目基于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

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值