你應該思考:爲何每每完成比完美更重要?html
在Spring Cloud
微服務應用體系中,遠程調用都應負載均衡。咱們在使用RestTemplate
做爲遠程調用客戶端的時候,開啓負載均衡極其簡單:一個@LoadBalanced
註解就搞定了。
相信你們大都使用過Ribbon
作Client端的負載均衡,也許你有和我同樣的感覺:Ribbon雖強大但不是特別的好用。我研究了一番,其實根源仍是咱們對它內部的原理不夠了解,致使對一些現象沒法給出合理解釋,同時也影響了咱們對它的定製和擴展。本文就針對此作出梳理,但願你們經過本文也可以對Ribbon
有一個較爲清晰的理解(本文只解釋它@LoadBalanced
這一小塊內容)。java
開啓客戶端負載均衡只須要一個註解便可,形如這樣:算法
@LoadBalanced // 標註此註解後,RestTemplate就具備了客戶端負載均衡能力 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
說Spring
是Java界最優秀、最傑出的重複發明輪子做品一點都不爲過。本文就代領你一探究竟,爲什麼開啓RestTemplate
的負載均衡如此簡單。spring
說明:本文創建在你已經熟練使用
RestTemplate
,而且瞭解RestTemplate
它相關組件的原理的基礎上分析。若對這部分還比較模糊,強行推薦你先
參看我前面這篇文章:RestTemplate的使用和原理你都爛熟於胸了嗎?【享學Spring MVC】安全
這是Spring Boot/Cloud
啓動Ribbon
的入口自動配置類,須要先有個大概的瞭解:服務器
@Configuration // 類路徑存在com.netflix.client.IClient、RestTemplate等時生效 @Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) // // 容許在單個類中使用多個@RibbonClient @RibbonClients // 如有Eureka,那就在Eureka配置好後再配置它~~~(若是是別的註冊中心呢,ribbon還能玩嗎?) @AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration") @AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class }) // 加載配置:ribbon.eager-load --> true的話,那麼項目啓動的時候就會把Client初始化好,避免第一次懲罰 @EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class }) public class RibbonAutoConfiguration { @Autowired private RibbonEagerLoadProperties ribbonEagerLoadProperties; // Ribbon的配置文件們~~~~~~~(複雜且重要) @Autowired(required = false) private List<RibbonClientSpecification> configurations = new ArrayList<>(); // 特徵,FeaturesEndpoint這個端點(`/actuator/features`)會使用它org.springframework.cloud.client.actuator.HasFeatures @Bean public HasFeatures ribbonFeature() { return HasFeatures.namedFeature("Ribbon", Ribbon.class); } // 它是最爲重要的,是一個org.springframework.cloud.context.named.NamedContextFactory 此工廠用於建立命名的Spring容器 // 這裏傳入配置文件,每一個不一樣命名空間就會建立一個新的容器(和Feign特別像) 設置當前容器爲父容器 @Bean public SpringClientFactory springClientFactory() { SpringClientFactory factory = new SpringClientFactory(); factory.setConfigurations(this.configurations); return factory; } // 這個Bean是關鍵,若你沒定義,就用系統默認提供的Client了~~~ // 內部使用和持有了SpringClientFactory。。。 @Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); } ... }
這個配置類最重要的是完成了Ribbon
相關組件的自動配置,有了LoadBalancerClient
才能作負載均衡(這裏使用的是它的惟一實現類RibbonLoadBalancerClient
)app
註解自己及其簡單(一個屬性都木有):負載均衡
// 所在包是org.springframework.cloud.client.loadbalancer // 能標註在字段、方法參數、方法上 // JavaDoc上說得很清楚:它只能標註在RestTemplate上纔有效 @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
它最大的特色:頭上標註有@Qualifier
註解,這是它生效的最重要因素之一,本文後半啦我花了大篇幅介紹它的生效時機。
關於@LoadBalanced
自動生效的配置,咱們須要來到這個自動配置類:LoadBalancerAutoConfiguration
ide
// Auto-configuration for Ribbon (client-side load balancing). // 它的負載均衡技術依賴於的是Ribbon組件~ // 它所在的包是:org.springframework.cloud.client.loadbalancer @Configuration @ConditionalOnClass(RestTemplate.class) //可見它只對RestTemplate生效 @ConditionalOnBean(LoadBalancerClient.class) // Spring容器內必須存在這個接口的Bean纔會生效(參見:RibbonAutoConfiguration) @EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry的配置文件 public class LoadBalancerAutoConfiguration { // 拿到容器內全部的標註有@LoadBalanced註解的Bean們 // 注意:必須標註有@LoadBalanced註解的才行 @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); // LoadBalancerRequestTransformer接口:容許使用者把request + ServiceInstance --> 改造一下 // Spring內部默認是沒有提供任何實現類的(匿名的都木有) @Autowired(required = false) private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList(); // 配置一個匿名的SmartInitializingSingleton 此接口咱們應該是熟悉的 // 它的afterSingletonsInstantiated()方法會在全部的單例Bean初始化完成以後,再調用一個一個的處理BeanName~ // 本處:使用配置好的全部的RestTemplateCustomizer定製器們,對全部的`RestTemplate`定製處理 // RestTemplateCustomizer下面有個lambda的實現。若調用者有須要能夠書寫而後扔進容器裏既生效 // 這種定製器:若你項目中有多個RestTempalte,須要統一處理的話。寫一個定製器是個不錯的選擇 // (好比統一要放置一個請求攔截器:輸出日誌之類的) @Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { return () -> restTemplateCustomizers.ifAvailable(customizers -> { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } }); } // 這個工廠用於createRequest()建立出一個LoadBalancerRequest // 這個請求裏面是包含LoadBalancerClient以及HttpRequest request的 @Bean @ConditionalOnMissingBean public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) { return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers); } // =========到目前爲止還和負載均衡沒啥關係========== // =========接下來的配置才和負載均衡有關(固然上面是基礎項)========== // 如有Retry的包,就是另一份配置,和這差很少~~ @Configuration @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig {、 // 這個Bean的名稱叫`loadBalancerClient`,我我的以爲叫`loadBalancerInterceptor`更合適吧(雖然ribbon是惟一實現) // 這裏直接使用的是requestFactory和Client構建一個攔截器對象 // LoadBalancerInterceptor但是`ClientHttpRequestInterceptor`,它會介入到http.client裏面去 // LoadBalancerInterceptor也是實現負載均衡的入口,下面詳解 // Tips:這裏可沒有@ConditionalOnMissingBean哦~~~~ @Bean public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } // 向容器內放入一個RestTemplateCustomizer 定製器 // 這個定製器的做用上面已經說了:在RestTemplate初始化完成後,應用此定製化器在**全部的實例上** // 這個匿名實現的邏輯超級簡單:向全部的RestTemplate都塞入一個loadBalancerInterceptor 讓其具有有負載均衡的能力 // Tips:此處有註解@ConditionalOnMissingBean。也就是說若是調用者本身定義過RestTemplateCustomizer類型的Bean,此處是不會執行的 // 請務必注意這點:容易讓你的負載均衡不生效哦~~~~ @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; } } ... }
這段配置代碼稍微有點長,我把流程總結爲以下幾步:函數
LoadBalancerAutoConfiguration
要想生效類路徑必須有RestTemplate
,以及Spring容器內必須有LoadBalancerClient
的實現BeanLoadBalancerClient
的惟一實現類是:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
LoadBalancerInterceptor
是個ClientHttpRequestInterceptor
客戶端請求攔截器。它的做用是在客戶端發起請求以前攔截,進而實現客戶端的負載均衡restTemplateCustomizer()
返回的匿名定製器RestTemplateCustomizer
它用來給全部的RestTemplate
加上負載均衡攔截器(須要注意它的@ConditionalOnMissingBean
註解~)不難發現,負載均衡實現的核心就是一個攔截器,就是這個攔截器讓一個普通的RestTemplate
逆襲成爲了一個具備負載均衡功能的請求器
LoadBalancerInterceptor
該類惟一被使用的地方就是LoadBalancerAutoConfiguration
裏配置上去~
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { // 這個命名都不叫Client了,而叫loadBalancer~~~ private LoadBalancerClient loadBalancer; // 用於構建出一個Request private LoadBalancerRequestFactory requestFactory; ... // 省略構造函數(給這兩個屬性賦值) @Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); } }
此攔截器攔截請求後把它的serviceName
委託給了LoadBalancerClient
去執行,根據ServiceName
可能對應N多個實際的Server
,所以就能夠從衆多的Server中運用均衡算法,挑選出一個最爲合適的Server
作最終的請求(它持有真正的請求執行器ClientHttpRequestExecution
)。
請求被攔截後,最終都是委託給了LoadBalancerClient
處理。
// 由使用負載平衡器選擇要向其發送請求的服務器的類實現 public interface ServiceInstanceChooser { // 從負載平衡器中爲指定的服務選擇Service服務實例。 // 也就是根據調用者傳入的serviceId,負載均衡的選擇出一個具體的實例出來 ServiceInstance choose(String serviceId); } // 它本身定義了三個方法 public interface LoadBalancerClient extends ServiceInstanceChooser { // 執行請求 <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException; <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException; // 從新構造url:把url中原來寫的服務名 換掉 換成實際的 URI reconstructURI(ServiceInstance instance, URI original); }
它只有一個實現類RibbonLoadBalancerClient
(ServiceInstanceChooser
是有多個實現類的~)。
RibbonLoadBalancerClient
首先咱們應當關注它的choose()
方法:
public class RibbonLoadBalancerClient implements LoadBalancerClient { @Override public ServiceInstance choose(String serviceId) { return choose(serviceId, null); } // hint:你能夠理解成分組。若指定了,只會在這個偏好的分組裏面去均衡選擇 // 獲得一個Server後,使用RibbonServer把server適配起來~~~ // 這樣一個實例就選好了~~~真正請求會落在這個實例上~ 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)); } // 根據ServiceId去找到一個屬於它的負載均衡器 protected ILoadBalancer getLoadBalancer(String serviceId) { return this.clientFactory.getLoadBalancer(serviceId); } }
choose方法
:傳入serviceId,而後經過SpringClientFactory
獲取負載均衡器com.netflix.loadbalancer.ILoadBalancer
,最終委託給它的chooseServer()
方法選取到一個com.netflix.loadbalancer.Server
實例,也就是說真正完成Server
選取的是ILoadBalancer
。
ILoadBalancer
以及它相關的類是一個較爲龐大的體系,本文不作更多的展開,而是隻聚焦在咱們的流程上
LoadBalancerInterceptor
執行的時候是直接委託執行的loadBalancer.execute()
這個方法:
RibbonLoadBalancerClient: // hint此處傳值爲null:一視同仁 // 說明:LoadBalancerRequest是經過LoadBalancerRequestFactory.createRequest(request, body, execution)建立出來的 // 它實現LoadBalancerRequest接口是用的一個匿名內部類,泛型類型是ClientHttpResponse // 由於最終執行的顯然仍是執行器:ClientHttpRequestExecution.execute() @Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { return execute(serviceId, request, null); } // public方法(非接口方法) public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException { // 同上:拿到負載均衡器,而後拿到一個serverInstance實例 ILoadBalancer loadBalancer = getLoadBalancer(serviceId); Server server = getServer(loadBalancer, hint); if (server == null) { // 若沒找到就直接拋出異常。這裏使用的是IllegalStateException這個異常 throw new IllegalStateException("No instances available for " + serviceId); } // 把Server適配爲RibbonServer isSecure:客戶端是否安全 // serverIntrospector內省 參考配置文件:ServerIntrospectorProperties RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); //調用本類的重載接口方法~~~~~ return execute(serviceId, ribbonServer, request); } // 接口方法:它的參數是ServiceInstance --> 已經肯定了惟一的Server實例~~~ @Override public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException { // 拿到Server)(說白了,RibbonServer是execute時的惟一實現) Server server = null; if (serviceInstance instanceof RibbonServer) { server = ((RibbonServer) serviceInstance).getServer(); } if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } // 說明:執行的上下文是和serviceId綁定的 RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId); ... // 真正的向server發送請求,獲得返回值 // 由於有攔截器,因此這裏確定說執行的是InterceptingRequestExecution.execute()方法 // so會調用ServiceRequestWrapper.getURI(),從而就會調用reconstructURI()方法 T returnVal = request.apply(serviceInstance); return returnVal; ... // 異常處理 }
returnVal
是一個ClientHttpResponse
,最後交給handleResponse()
方法來處理異常狀況(若存在的話),若無異常就交給提取器提值:responseExtractor.extractData(response)
,這樣整個請求就算所有完成了。
針對@LoadBalanced
下的RestTemplate
的使用,我總結以下細節供以參考:
String
類型的url必須是絕對路徑(http://...
),不然拋出異常:java.lang.IllegalArgumentException: URI is not absolute
serviceId
不區分大小寫(http://user/...效果同http://USER/...
)serviceId
後請不要跟port端口號了~~~最後,須要特別指出的是:標註有@LoadBalanced
的RestTemplate
只能書寫serviceId
而不能再寫IP地址/域名
去發送請求了。若你的項目中兩種case都有須要,請定義多個RestTemplate
分別應對不一樣的使用場景~
瞭解了它的執行流程後,若須要本地測試(不依賴於註冊中心),能夠這麼來作:
// 由於自動配置頭上有@ConditionalOnMissingBean註解,因此自定義一個覆蓋它的行爲便可 // 此處複寫它的getServer()方法,返回一個固定的(訪問百度首頁)便可,方便測試 @Bean public LoadBalancerClient loadBalancerClient(SpringClientFactory factory) { return new RibbonLoadBalancerClient(factory) { @Override protected Server getServer(ILoadBalancer loadBalancer, Object hint) { return new Server("www.baidu.com", 80); } }; }
這麼一來,下面這個訪問結果就是百度首頁的html內容嘍。
@Test public void contextLoads() { String obj = restTemplate.getForObject("http://my-serviceId", String.class); System.out.println(obj); }
此處
my-serviceId
確定是不存在的,但得益於我上面自定義配置的LoadBalancerClient
什麼,寫死return
一個Server
實例不優雅?確實,總不能每次上線前還把這部分代碼給註釋掉吧,如有多個實例呢?還得本身寫負載均衡算法嗎?很顯然Spring Cloud
早早就爲咱們考慮到了這一點:脫離Eureka使用配置listOfServers進行客戶端負載均衡調度(<clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>
)
對於上例我只須要在主配置文件裏這麼配置一下:
# ribbon.eureka.enabled=false # 若沒用euraka,此配置可省略。不然不能夠 my-serviceId.ribbon.listOfServers=www.baidu.com # 如有多個實例請用逗號分隔
效果徹底同上。
Tips:這種配置法不須要是完整的絕對路徑,
http://
是能夠省略的(new Server()
方式亦可)
顯然是可行的,我給出示例以下:
@LoadBalanced @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); List<ClientHttpRequestInterceptor> list = new ArrayList<>(); list.add((request, body, execution) -> { System.out.println("當前請求的URL是:" + request.getURI().toString()); return execution.execute(request, body); }); restTemplate.setInterceptors(list); return restTemplate; }
這樣每次客戶端的請求都會打印這句話:當前請求的URI是:http://my-serviceId
,通常狀況(缺省狀況)自定義的攔截器都會在負載均衡攔截器前面執行(由於它要執行最終的請求)。若你有必要定義多個攔截器且要控制順序,可經過Ordered
系列接口來實現~
最後的最後,我拋出一個很是很是重要的問題:
@LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired
+ @LoadBalanced
能把你配置的RestTemplate
自動注入進來拿來定製呢???核心原理是什麼?
提示:本原理內容屬於
Spring Framwork
核心技術,建議深刻思考而不囫圇吞棗。有疑問的能夠給我留言,我也將會在下篇文章給出詳細解答(建議先思考)
RestTemplate的使用和原理你都爛熟於胸了嗎?【享學Spring MVC】
@Qualifier高級應用---按類別批量依賴注入【享學Spring】
本文以你們熟悉的@LoadBalanced
和RestTemplate
爲切入點介紹了Ribbon
實現負載均衡的執行流程,固然此部分對Ribbon
整個的核心負載體系知識來講知識冰山一角,但它做爲敲門磚仍是頗有意義的,但願本文能勾起你對Ribbon
體系的興趣,深刻了解它~
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入羣一塊兒飛 ==
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入羣一塊兒飛 ==