spring cloud 实现灰度发布,多个服务指向同一个路由的错误解决及源码分析

前文中,我自定义一个gatewayRule,实现了根据参数进行路由,从而实现灰度发布的功能。在本地自测的时候,运行良好。放到测试环境后,发现一个重大bug。所有的服务都会走同一个路由,所以服务A请求服务B时,却会调用到服务C!

自己引入的bug,自己跪着也要解决。开始经历了层层排查历史,并最终解决问题。

一,接着前文讲起,RibbonLoadBalancerClient中

public ServiceInstance choose(String serviceId, Object hint) {
//查询server
		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) {
	//通过SpringClientFactory 查询LoadBalancer
		return this.clientFactory.getLoadBalancer(serviceId);
	}

看到需要通过SpringClientFactory,查找 ILoadBalancer

二,我们看下SpringClientFactory

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {

	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);
	}

	@Override
	protected AnnotationConfigApplicationContext getContext(String name) {
		return super.getContext(name);
	}

可以看到进入了父类,即NamedContextFactory

三,NamedContextFactory

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
		implements DisposableBean, ApplicationContextAware {
		private Map<String, C> configurations = new ConcurrentHashMap<>();
		//获取实例,先获取该name的context
	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;
	}
	//不存在该context,则创建新context
	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);
	}
//具体创建Context的过程,并在该context中初始加载一些类
	protected AnnotationConfigApplicationContext createContext(String name) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		//将包含本name的类注册
		if (this.configurations.containsKey(name)) {
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) {
				context.register(configuration);
			}
		}
		//将所有detault开头的类注册
		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)));
				//将本上下文注册父类及ClassLoader
		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;
	}

可以看到,会默认注册configurations下面的default开头,和key为本name的类。
那么就需要看下这些configurations类究竟是哪些了。
很容易查到,这些configurations的注册地点为RibbonAutoConfiguration
四,RibbonAutoConfiguration

@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
//注意这个RibbonClients注解
@RibbonClients
@AutoConfigureAfter(
		name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
		AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
		ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
	
	// configurations是一个List<RibbonClientSpecification>
	@Autowired(required = false)
	private List<RibbonClientSpecification> configurations = new ArrayList<>();
	
	@Bean
	@ConditionalOnMissingBean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		//在生成SpringClientFactory时注入了configurations
		factory.setConfigurations(this.configurations);
		return factory;
	}

	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}

那么这些List又是从哪来的呢?
我们查看RibbonClientSpecification被使用的地方,发现RibbonClientConfigurationRegistrar类在用,进入看一下

五,RibbonClientConfigurationRegistrar

public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
	//获取被@RibbonClient和@RibbonClients注释的类,将其注册为RibbonClientSpecification
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(RibbonClients.class.getName(), true);
		if (attrs != null && attrs.containsKey("value")) {
			AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
			for (AnnotationAttributes client : clients) {
				registerClientConfiguration(registry, getClientName(client),
						client.get("configuration"));
			}
		}
		if (attrs != null && attrs.containsKey("defaultConfiguration")) {
			String name;
			if (metadata.hasEnclosingClass()) {
				name = "default." + metadata.getEnclosingClassName();
			}
			else {
				name = "default." + metadata.getClassName();
			}
			registerClientConfiguration(registry, name,
					attrs.get("defaultConfiguration"));
		}
		Map<String, Object> client = metadata
				.getAnnotationAttributes(RibbonClient.class.getName(), true);
		String name = getClientName(client);
		if (name != null) {
			registerClientConfiguration(registry, name, client.get("configuration"));
		}
	}
	//注册为RibbonClientSpecification
	private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
			Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(RibbonClientSpecification.class);
		builder.addConstructorArgValue(name);
		builder.addConstructorArgValue(configuration);
		registry.registerBeanDefinition(name + ".RibbonClientSpecification",
				builder.getBeanDefinition());
	}

这里需要重点说一下,实现了ImportBeanDefinitionRegistrar接口,该接口是spring实现bean动态注册的流程。所有实现了该接口的类都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,最终注册到容器中。
不过ImportBeanDefinitionRegistrar需要跟@Configuration和@Import配合使用。我们查找看一下,使用到该类的有@RibbonClient 和@RibbonClients两个注解。

@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {

	RibbonClient[] value() default {};

	Class<?>[] defaultConfiguration() default {};

}

所以这一段总体上是,在RibbonClientConfigurationRegistrar中,所有被@RibbonClient和@RibbonClients注解的类,都被Sping注册为RibbonClientSpecification。从而在RibbonAutoConfiguration中被SpringClientFactory使用,被SpringClientFactory父类NamedContextFactory注册到新生成的context 上下文中。
所以如果想被context注册使用,那么该bean就需要在@RibbonClients或@RibbonClient注释的类中。
这也是目前市面上所有做gateway灰度发布路由的文章都存在多个服务被指向同一个IRule的原因。
也就是我本次遇到的bug。
理解了源码,解决起来也很简单,将我们的自定义路由注册到Context中就好啦。
新建一个Configuration,引入路由Irule的bean,并使用@RibbonClients和@Configuration注解,就好啦。

以上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值