版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協定,轉載請附上原文出處連結和本聲明。
本文連結:https://blog.csdn.net/f641385712/article/details/100788040
每篇一句
你應該思考:為什麼往往完成比完美更重要?
前言
在
Spring Cloud
微服務應用體系中,遠端調用都應負載均衡。我們在使用
RestTemplate
作為遠端調用用戶端的時候,開啟負載均衡極其簡單:一個
@LoadBalanced
注解就搞定了。
相信大家大都使用過
Ribbon
做Client端的負載均衡,也許你有和我一樣的感受:Ribbon雖強大但不是特别的好用。我研究了一番,其實根源還是我們對它内部的原理不夠了解,導緻對一些現象無法給出合了解釋,同時也影響了我們對它的定制和擴充。本文就針對此做出梳理,希望大家通過本文也能夠對
Ribbon
有一個較為清晰的了解(本文隻解釋它
@LoadBalanced
這一小塊内容)。
開啟用戶端負載均衡隻需要一個注解即可,形如這樣:
@LoadBalanced // 标注此注解後,RestTemplate就具有了用戶端負載均衡能力
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
複制
說
Spring
是Java界最優秀、最傑出的重複發明輪子作品一點都不為過。本文就代領你一探究竟,為何開啟
RestTemplate
的負載均衡如此簡單。
說明:本文建立在你已經熟練使用,并且了解
RestTemplate
它相關元件的原理的基礎上分析。若對這部分還比較模糊,強行推薦你
RestTemplate
參看我前面這篇文章:RestTemplate的使用和原理你都爛熟于胸了嗎?【享學Spring MVC】
先
RibbonAutoConfiguration
這是
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
)
@LoadBalanced
注解本身及其簡單(一個屬性都木有):
// 所在包是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
LoadBalancerAutoConfiguration
// 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
,以及Spring容器内必須有RestTemplate
LoadBalancerClient
的實作Bean
1.
的唯一實作類是:LoadBalancerClient
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
-
是個LoadBalancerInterceptor
用戶端請求攔截器。它的作用是在用戶端發起請求之前攔截,進而實作用戶端的負載均衡ClientHttpRequestInterceptor
-
傳回的匿名定制器restTemplateCustomizer()
它用來給所有的RestTemplateCustomizer
加上負載均衡攔截器(需要注意它的RestTemplate
注解~)@ConditionalOnMissingBean
不難發現,負載均衡實作的核心就是一個攔截器,就是這個攔截器讓一個普通的
RestTemplate
逆襲成為了一個具有負載均衡功能的請求器
LoadBalancerInterceptor
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
請求被攔截後,最終都是委托給了
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
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
的使用,我總結如下細節供以參考:
- 傳入的
類型的url必須是絕對路徑(String
),否則抛出異常:http://...
java.lang.IllegalArgumentException: URI is not absolute
-
不區分大小寫(serviceId
)http://user/...效果同http://USER/...
-
後請不要跟port端口号了~~~serviceId
最後,需要特别指出的是:标注有
@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
體系的興趣,深入了解它~