為何一個@LoadBalanced註解就能讓RestTemplate擁有負載均衡的能力?【享學Spring Cloud】

  • 2019 年 10 月 5 日
  • 筆記

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/f641385712/article/details/100788040

每篇一句

你應該思考:為什麼往往完成比完美更重要?

前言

Spring Cloud微服務應用體系中,遠程調用都應負載均衡。我們在使用RestTemplate作為遠程調用客戶端的時候,開啟負載均衡極其簡單:一個@LoadBalanced註解就搞定了。 相信大家大都使用過RibbonClient端的負載均衡,也許你有和我一樣的感受: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);  			};  		}  	}  	...  }

這段配置程式碼稍微有點長,我把流程總結為如下幾步:

  1. LoadBalancerAutoConfiguration要想生效類路徑必須有RestTemplate,以及Spring容器內必須有LoadBalancerClient的實現Bean 1. LoadBalancerClient的唯一實現類是:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient
  2. LoadBalancerInterceptor是個ClientHttpRequestInterceptor客戶端請求攔截器。它的作用是在客戶端發起請求之前攔截,進而實現客戶端的負載均衡
  3. 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

請求被攔截後,最終都是委託給了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);  }

它只有一個實現類RibbonLoadBalancerClientServiceInstanceChooser是有多個實現類的~)。

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的使用,我總結如下細節供以參考:

  1. 傳入的String類型的url必須是絕對路徑(http://...),否則拋出異常:java.lang.IllegalArgumentException: URI is not absolute
  2. serviceId不區分大小寫(http://user/...效果同http://USER/...
  3. serviceId後請不要跟port埠號了~~~

最後,需要特別指出的是:標註有@LoadBalancedRestTemplate只能書寫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】

總結

本文以大家熟悉的@LoadBalancedRestTemplate為切入點介紹了Ribbon實現負載均衡的執行流程,當然此部分對Ribbon整個的核心負載體系知識來說知識冰山一角,但它作為敲門磚還是很有意義的,希望本文能勾起你對Ribbon體系的興趣,深入了解它~