Spring Cloud Netflix Ribbon及Spring Cloud OpenFeign探祕

使用Spring Boot及Spring Cloud全家桶,Eureka,Feign,Ribbon通常是必選套餐。在咱們無腦使用了一段時間後,發現有的配置方式和預期不符,因而便進行了一番研究。本文將介紹Ribbon和Feign一些重要而不常據說的細節。java

在閱讀本文以前,你須要瞭解Spring Boot自動配置的原理。能夠參考我前面一篇文章:Spring Boot Starter自動配置的加載原理react

Ribbon

Ribbon是Netflix微服務體系中的一個核心組件。甚至是Java領域中很少見的客戶端負載均衡組件,恕我孤陋寡聞。關於Ribbon的原理,其實不復雜。Github的文檔倒也還算完整,只是咱們通常不會直接使用Ribbon,而是使用Spring Cloud提供的Netflix Ribbon Starter,所以文檔會有很多對不上的地方。git

原生Ribbon簡介

Ribbon五大組件:github

  • ServerList:定義獲取服務器列表
  • ServerListFilter:對ServerList服務器列表進行二次過濾
  • ServerListUpdater:定義服務更新策略
  • IPing:檢查服務列表是否存活
  • IRule:根據算法中從服務列表中選取一個要訪問的服務

Ribbon的主要接口:算法

  • ILoadBalancer:軟件負載平衡器入口,整合以上全部的組件實現負載功能

代碼簡析

Ribbon原生代碼有兩個包特別重要,com.netflix.loadbalancer包和com.netflix.clientspring

loadbalancer包核心類圖: apache

loadbalancer

client包核心類圖:安全

client

總結:bash

  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的原生組件就能夠無縫使用。

小結

修改Spring-Cloud-Netflix-Ribbon配置有幾種方式:

  1. 修改ribbon開頭的少數幾個全局配置,例如ribbon.okhttp.enabled等等。(但我還沒看出這有什麼用,雖然bean會建立,可是上層不會使用)
  2. 顯式聲明@RibbonClients(defaultConfiguration=xxx.class),替換默認的全局配置。
  3. 直接在Java配置類中建立原生類的幾種組件Bean,能夠替換全局配置。
  4. 顯式聲明@RibbonClient(configuration=xxx.class),替換單個client的配置。
  5. 在配置文件中使用CLIENT-NAME.ribbon.的方式配置。
  6. 能夠不帶client-name前綴,直接使用ribbon.xyz指定全部client的默認配置。這個並無在官方文檔中介紹。而它起做用的原理在這個方法中:com.netflix.client.config.DefaultClientConfigImpl#getProperty(java.lang.String)。當找不到ClientName開頭的配置時,會直接使用ribbon前綴的配置。

OpenFeign

Feign 最初也是Netflix的,只是後來他們本身再也不使用了,開源出來後就改了個名字,叫OpenFeign。這個故事能夠今後GitHub issue看到。

原生OpenFeign

OpenFeign的官方文檔上聲稱他們是受了Retrofit的啓發。因此這兩個框架不管是使用仍是設計都是很像的。

原理

用腳趾頭想一下,原理就是在運行時根據聲明的Api接口,生成動態代理。代碼可見feign.ReflectiveFeign#newInstance

幾個重要的組件:

  • Builder:根據一個接口建立一個類型安全的客戶端封裝類。
  • Encoder/Decoder:就是序列化/反序列化器。在Retrofit中叫Converter。
  • Client:即用什麼組件去髮網絡請求。默認竟然是UrlConnection。
  • RequestTemplate和RequestInterceptor:分別表明一個請求的描述,以及攔截器。

這些組件能夠在下面這個Spring Cloud OpenFeign的配置裏頭看出個大概

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這樣的註解,而後作一樣的事情。

client配置獨立的原理

另外,Feign要作到分client配置獨立,也會使用到相似Ribbon的SpringClientFactory類型,而在Feign這邊叫FeignContext,二者都是繼承自NamedContextFactory的。原理可見org.springframework.cloud.context.named.NamedContextFactory#createContext

NamedContextFactory只有兩個子類

下面仍是說下原理吧,仍是挺費解的。

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> implements DisposableBean, ApplicationContextAware {
	// 保存局部應用上下文的map,好比定義了App1, App2兩個client,就保存兩個entry
    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
    // 保存配置的map,每一個client能夠單獨有一個
    private Map<String, C> configurations = new ConcurrentHashMap<>();
    // 此NamedContextFactory所要建立的具體client的默認Java配置類,構造時傳入
    private Class<?> defaultConfigType;
    
    protected AnnotationConfigApplicationContext createContext(String name) {
        // 新建一個局部的ApplicationContext,由註解驅動。
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 把client獨有的配置註冊進去
        if (this.configurations.containsKey(name)) {
        	for (Class<?> configuration : this.configurations.get(name)
        			.getConfiguration()) {
        		context.register(configuration);
        	}
        }
        // 把default.開頭的配置註冊進去
        for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        	if (entry.getKey().startsWith("default.")) {
        		for (Class<?> configuration : entry.getValue().getConfiguration()) {
        			context.register(configuration);
        		}
        	}
        }
        // 把默認的Java配置類註冊進去,就是Ribbon
        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;
    }
}
複製代碼

重點:

  1. NamedContextFactory會在構造時接受三個參數,第一個參數指定默認配置。而這個類目前也只有兩個實現:
    ClientSpecification
  2. 這個類的子類使用者會使用NamedContextFactory#getInstance(String name, ResolvableType type)這個方法來得到這個局部上下文內部的Bean,其中第一個方法是client名,第二個方法是Bean的類型名。而此方法首先會檢查對應的client名在不在contexts這個map裏面,若是沒有,就要調用createContext(String name)建立。
  3. 接下來的代碼就一目瞭然了。

支持properties配置

OpenFeign比Ribbon更好地支持了Spring的Properties外置化配置,緣由是Ribbon使用了Archaius,和Spring兼容不夠好,而OpenFeign沒有。Spring Cloud OpenFeign的外置化配置可見FeignClientProperties,其使用之處則在FeignClientFactoryBean#configureFeign

使用這種方式,咱們能夠方便地在配置文件中使用feign.client.config.CLIENT-NAME.xyz=blabla來指定某個Feign Client的具體配置,甚至能夠用feign.client.config.default.xyz=blabla來指定全部client的默認配置,好評!

在後面的 Spring Cloud Netflix Hystrix中,咱們能夠看到相似的配置設計。

Retry功能

OpenFeign的retry功能是利用的Spring Retry框架。而要使用retry基本上須要的也就是個Retry Policy配置。OpenFeign並無默認配置,而是利用了Ribbon的配置。具體配置參見com.netflix.client.config.CommonClientConfigKey

一些值得注意的點

  • 原生OpenFeign的很多組件都是獨立的包!好比各類Encoder/Decoder,以及Client。這點也是學習的Retrofit。這篇文章就提到了如何踩坑的。而官方文檔對此徹底沒有說起!使用時切記引入
  • 一樣,Spring Cloud OpenFeign Starter的默認Encoder是封裝的SpringEncoder。通過小夥伴的測試,不如Gson或Jackson給力。

總結

修改OpenFeign配置有幾種方式:

  1. feign的全局配置,好比啓用okhttp,http客戶端線程池大小等等。
  2. @EnableFeignClients(defaultConfiguration=xxx.class),替換默認的全局配置。
  3. 直接在Java配置類中建立原生類的幾種組件Bean,能夠替換全局配置。
  4. 顯式聲明@RibbonClient(configuration=xxx.class),替換單個client的配置。
  5. 在配置文件中使用feign.client.config.default.的方式配置client默認設置。
  6. 在配置文件中使用feign.client.config.CLIENT-NAME.的方式配置單個client。

補充

  1. 超時:不要認爲http client建立出來超時就指定好了哦!實際上超時是按請求設置來的,在請求前會應用request specific 設置。Request-specific配置參見feign.Request.Options。若是配置了feign的超時,就會按feign的超時;若沒配feign的超時,則會按ribbon的超時,此時若ribbon也沒配置,則會默認Connect-timeout和Read-timeout都是1秒;若配置了ribbon的超時,則會按ribbon的超時。
  2. 重試:前面說過,feign重試使用的是Spring Retry框架,而Ribbon自帶重試功能,使用的RxJava的重試。在Spring-Cloud-OpenFeign場景下,這兩種重試均可以用到。在不引入Spring Retry包,配置Ribbon Retry設置時,也能開啓重試。

擴展

關注下異步的Feign Client:github.com/kptfh/feign…

相關文章
相關標籤/搜索