今天看啥  ›  专栏  ›  xinlmain

Spring Cloud Netflix Ribbon及Spring Cloud OpenFeign探秘

xinlmain  · 掘金  ·  · 2019-07-11 01:30
阅读 54

Spring Cloud Netflix Ribbon及Spring Cloud OpenFeign探秘

使用Spring Boot及Spring Cloud全家桶,Eureka,Feign,Ribbon一般是必选套餐。在我们无脑使用了一段时间后,发现有的配置方式和预期不符,于是便进行了一番研究。本文将介绍Ribbon和Feign一些重要而不常听说的细节。

在阅读本文之前,你需要了解Spring Boot自动配置的原理。可以参考我前面一篇文章:Spring Boot Starter自动配置的加载原理

Ribbon

Ribbon是Netflix微服务体系中的一个核心组件。甚至是Java领域中不多见的客户端负载均衡组件,恕我孤陋寡闻。关于Ribbon的原理,其实不复杂。Github的文档倒也还算完整,只是我们一般不会直接使用Ribbon,而是使用Spring Cloud提供的Netflix Ribbon Starter,因此文档会有不少对不上的地方。

原生Ribbon简介

Ribbon五大组件:

  • ServerList:定义获取服务器列表
  • ServerListFilter:对ServerList服务器列表进行二次过滤
  • ServerListUpdater:定义服务更新策略
  • IPing:检查服务列表是否存活
  • IRule:根据算法中从服务列表中选取一个要访问的服务

Ribbon的主要接口:

  • ILoadBalancer:软件负载平衡器入口,整合以上所有的组件实现负载功能

代码简析

Ribbon原生代码有两个包特别重要,com.netflix.loadbalancer包和com.netflix.client

loadbalancer包核心类图:

loadbalancer

client包核心类图:

client

总结:

  1. loadblancer包中最外层及最重要的接口就是ILoadBalancer,但它只具有LB的功能,不具有发请求的功能,因此最终还是需要有包含ILoadBlancer的client
  2. 自然就需要IClient接口,在client包中定义
  3. LoadBalancerContext及其继承类AbstractLoadBalancerAwareClient是实现所有带LB功能的IClient子类的父类。而谁会实现这种client?答案是Spring Cloud的代码!
  4. 证据如下:
  • LoadBalancerContext的继承类,除了AbstractLoadBalancerAwareClient,全是Spring Cloud包的。
  1. AbstractLoadBalancerAwareClient的实现又用到了com.netflix.loadbalancer.reactive包里面的LoadBalancerCommand,后者利用RxJava封装了Retry逻辑,而Retry配置由RetryHandler配置。
  2. 即,Ribbon还支持重试,而重试原理是使用RxJava的retry。

Spring Cloud Netflix Ribbon Starter

上文极为概况地总结了Ribbon的重要组件。不管你看没看懂,我反正是懂了…… (啊,其实不是很重要,重要的是这一节)

自动配置的原理

关于Ribbon,你需要记住的是它是个中间层组件,只提供Load Balance功能。而我们使用Ribbon的原因一般都是发送客户端请求。在Spring Cloud环境下,往往就这么两种外层组件:RestTemplateFeign。因此,它们必然是封装了Ribbon的功能,才能实现负载均衡,(以及基于Eureka的服务发现)。

直接来看Ribbon Starter这个包。

META-INF/spring.factories文件如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration
复制代码

太好了,就这么一个自动配置类。来看看它做了什么:

// 省略不重要代码
@Configuration
@RibbonClients
public class RibbonAutoConfiguration {
    @Autowired(required = false)
    private List<RibbonClientSpecification> configurations = new ArrayList<>();
    
    @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());
    }
    
    @Bean
    @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
    @ConditionalOnMissingBean
    public LoadBalancedRetryFactory loadBalancedRetryPolicyFactory(
    		final SpringClientFactory clientFactory) {
    	return new RibbonLoadBalancedRetryFactory(clientFactory);
    }
    
    @Configuration
    @ConditionalOnClass(HttpRequest.class)
    @ConditionalOnRibbonRestClient
    protected static class RibbonClientHttpRequestFactoryConfiguration {
    
    	@Autowired
    	private SpringClientFactory springClientFactory;
    
    	@Bean
    	public RestTemplateCustomizer restTemplateCustomizer(
    			final RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory) {
    		return restTemplate -> restTemplate
    				.setRequestFactory(ribbonClientHttpRequestFactory);
    	}
    
    	@Bean
    	public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory() {
    		return new RibbonClientHttpRequestFactory(this.springClientFactory);
    	}
    }
    
    // TODO: support for autoconfiguring restemplate to use apache http client or okhttp
}

复制代码

捡重点的说,这段代码主要干了这么几件事:

  1. 导入了@RibbonClients注解,等会我们再看它。
  2. 自动创建SpringClientFactory,这是个Spring Cloud增加的功能,相当于一个Map,里面放的是client名到Application Context的映射。也就是说,对于Ribbon,一个client名就对应一组bean,这样方能实现配置隔离。
  3. 自动创建LoadBalancerClient Bean,这个类是对原生Ribbon的封装,提供负载均衡功能。
  4. 如果存在Spring Retry包,则自动创建某个Bean用来支持重试。嗯,Spring Cloud Ribbon也支持重试,但不是通过原生的RxJava了,而是通过Spring Retry框架。
  5. 对RestTemplate添加一个customizer,相当于拦截器,使其具有负载均衡功能。

彩蛋:代码末尾还有个TODO注释:配置RestTemlate支持http client 或 okhttp,可见目前并没有实现。经过断点调试我验证了这一点。

好,接下来我们看看RibbonClients注解是何方神圣。

//省略部分代码
@Configuration
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
	RibbonClient[] value() default {};
	Class<?>[] defaultConfiguration() default {};
}
复制代码
  1. 它导入了RibbonClientConfigurationRegistrar, 这显然是个ImportBeanDefinitionRegistrar的实现类。嗯,基本上所有的@EnableXYZ注解都是通过它实现的。
  2. 提供了一个defaultConfiguration字段,可以填入所有client的默认配置。

RibbonClientConfigurationRegistrar的代码不再贴了,它自动创建了所有声明的Ribbon client的配置bean。

到这里你应该提出疑问了:怎么没看到哪里创建Ribbon原生类的Bean?

很好,我们来看看starter包里还有什么。 很容易就找到了RibbonClientConfiguration这个Java配置类:

@Configuration
@EnableConfigurationProperties
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
		RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
    	DefaultClientConfigImpl config = new DefaultClientConfigImpl();
    	config.loadProperties(this.name);
    	config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
    	config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
    	config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
    	return config;
    }
    
    @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 IPing ribbonPing(IClientConfig config) {
    	if (this.propertiesFactory.isSet(IPing.class, name)) {
    		return this.propertiesFactory.get(IPing.class, config, name);
    	}
    	return new DummyPing();
    }
    
    @Bean
    @ConditionalOnMissingBean
    @SuppressWarnings("unchecked")
    public ServerList<Server> ribbonServerList(IClientConfig config) {
    	if (this.propertiesFactory.isSet(ServerList.class, name)) {
    		return this.propertiesFactory.get(ServerList.class, config, name);
    	}
    	ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
    	serverList.initWithNiwsConfig(config);
    	return serverList;
    }
    
    @Bean
    @ConditionalOnMissingBean
    public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
    	return new PollingServerListUpdater(config);
    }
    
    @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);
    }
    
    @Bean
    @ConditionalOnMissingBean
    @SuppressWarnings("unchecked")
    public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
    	if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
    		return this.propertiesFactory.get(ServerListFilter.class, config, name);
    	}
    	ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
    	filter.initWithNiwsConfig(config);
    	return filter;
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer,
    		IClientConfig config, RetryHandler retryHandler) {
    	return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
    }
}
复制代码

显然,这个类就是用来创建Ribbon原生类的Bean的。然而它是在哪触发的呢? 这个问题解释起来有点费劲,不如打个断点调试一下:

RibbonClientConfiguration

解释:

  1. RibbonLoadBalancerClient就是本节一开始那个LoadBalancerClient的实现类,上文说过,它是Spring Cloud对Ribbon的封装。其持有一个SpringClientFactory
  2. 每一个Ribbon client都有个配置,如果不指定,则默认为RibbonClientConfiguration
  3. RibbonLoadBalancerClient#execute()方法是从SpringClientFactory获得真正的Ribbon原生类,从而实现负载均衡功能。
  4. SpringClientFactory,前文说过,它是一个Application Context的map容器。也就是说,对于一个ribbon client,就有一组隔离的bean,包括IRule, IPing, ServerList这些。
  5. 第一次从SpringClientFactory获取原生Ribbon类的Bean时,前者需要创建新的Application。 Context,自然就需要传入Java配置类。创建后刷新Application Context,RibbonClientConfiguration就被导入了。
  6. 所以,可以理解@RibbonClient(configuration=XXX.class)这种方式自定义Ribbon的配置了原理吧,就是替换了默认的RibbonClientConfiguration

结合Eureka使用为何能服务发现

如果你看了spring-cloud-netflix-eureka-client-starter就明白了。Eureka client的自动配置会自动创建基于Eureka服务发现的Ribbon ServerList等一系列Ribbon组件bean。这样你不用做任何事就自动具有了Eureka服务发现功能。

@Configuration
public class EurekaRibbonClientConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IPing ribbonPing(IClientConfig config) {
    	if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
    		return this.propertiesFactory.get(IPing.class, config, serviceId);
    	}
    	NIWSDiscoveryPing ping = new NIWSDiscoveryPing();
    	ping.initWithNiwsConfig(config);
    	return ping;
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config,
    	Provider<EurekaClient> eurekaClientProvider) {
    if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
    	return this.propertiesFactory.get(ServerList.class, config, serviceId);
    }
    DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
    		config, eurekaClientProvider);
    DomainExtractingServerList serverList = new DomainExtractingServerList(
    		discoveryServerList, config, this.approximateZoneFromHostname);
    return serverList;
    }
    ...
}
复制代码

但是这也带来了另一个问题:如果你有一个服务在eureka之外,想通过CLIENT-NAME.ribbon.serverList=adress1,adress2这种方式就不能生效了。因为Eureka给你创建的ServerList实现是DiscoveryEnabledNIWSServerList,不支持配置方式。你需要再加上一条配置:CLIENT-NAME.ribbon.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList

配置文件原理

在原生Ribbon被开发的年代,Netflix并没有使用Spring Boot(那时当然还没有)和Spring,而是采用了自己的框架。在配置方面,他们有自己的动态配置框架Archaius。比如你可以从原生Ribbon的文档里看到一些配置示例。而在Spring Cloud中,我们也可以使用同样的配置定制Ribbon,这是为什么?

在Spring Cloud Netflix的文档中,有过解释:

Spring applications should generally not use Archaius directly, but the need to configure the Netflix tools natively remains. Spring Cloud has a Spring Environment Bridge so that Archaius can read properties from the Spring Environment. This bridge allows Spring Boot projects to use the normal configuration toolchain while letting them configure the Netflix tools as documented (for the most part).

也就是说Spring Cloud先用自己的Configuration Properties功能封装Archaius的配置,获取到配置后,再在必要时传递给Archaius,这样Netflix的原生组件就可以无缝使用。

小结

修改Ribbon配置有几种方式:

  1. 修改ribbon开头的少数几个全局配置,例如ribbon.okhttp.enabled等等。(但我还没看出这有什么用,虽然bean会创建,但是上层不会使用)
  2. 显式声明@RibbonClients(configuration=xxx.class),替换默认的全局配置。
  3. 直接在Java配置类中创建原生类的几种组件Bean,可以替换全局配置。
  4. 显式声明@RibbonClient(configuration=xxx.class),替换单个client的配置。
  5. 在配置文件中使用CLIENT-NAME.ribbon.的方式配置。

OpenFeign

Feign 最初也是Netflix的,只是后来他们自己不再使用了,开源出来后就改了个名字,叫OpenFeign。这个故事可以从此GitHub issue看到。

原生OpenFeign

OpenFeign的官方文档上声称他们是受了Retrofit的启发。所以这两个框架不论是使用还是设计都是很像的。

几个重要的组件:

  • Builder:根据一个接口创建一个类型安全的客户端封装类。
  • Encoder/Decoder:就是序列化/反序列化器。在Retrofit中叫Converter。
  • Client:即用什么组件去发网络请求。默认居然是UrlConnection。

Spring Cloud OpenFeign

自动配置原理

其实和Ribbon的配置很像了。也有@FeignClient@FeignClients注解。 @EnableFeignClients则导入了FeignClientsRegistrar,后者是一个ImportBeanDefinitionRegistrar的实现类。在其接口方法中,做两件事:

  1. 注册默认配置bean
  2. 扫描包,把所有声明了@FeignClient的接口都找出来,然后为它生成实现类的bean。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
	BeanDefinitionRegistry registry) {
	registerDefaultConfiguration(metadata, registry);
	registerFeignClients(metadata, registry);
}
复制代码

所以,如果Retrofit也要集成进Spring Boot,自然也需要在Starter中创建@EnableRetrofit这样的注解,然后做同样的事情。

一些值得注意的点

  • 原生OpenFeign的不少组件都是独立的包!比如各种Encoder/Decoder,以及Client。这点也是学习的Retrofit。这篇文章就提到了如何踩坑的。而官方文档对此完全没有提及!
  • 同样,Spring Cloud OpenFeign Starter的默认Encoder是封装的SpringEncoder。经过小伙伴的测试,完全不如Gson或Jackson给力。



原文地址:访问原文地址
快照地址: 访问文章快照