1. 程式人生 > >SpringCloud-Ribbon工作原理分析

SpringCloud-Ribbon工作原理分析

由於歷史原因,原有業務系統的大部分介面都是通過SpringBoot RESTFul暴露服務的且能過Nginx做閘道器。微服務架構近幾年也非常的火,各個公司開始將業務服務

 在使用Ribbon進行服務消費的時候,我們用到了RestTemplate,但是熟悉Spring的人是否產生過這樣的疑問:RestTemplate不是Spring自己就有的嗎?跟Ribbon的客戶端負載均衡又有什麼關係呢?帶著這些疑問,我們就來看RestTemplateRibbon是如何聯絡起來並實現客戶端負載均衡的。

首先,對於消費者示例:我們是如何實現客戶端負載均衡的?仔細觀察一下程式碼之前的程式碼,我們可以發現在消費者的例子中,可能就是這個註解@LoadBalanced

是之前沒有接觸過的,並且從命名上來看也與負載均衡相關。我們不妨根據這個線索來看看原始碼實現的機制。

@LoadBalanced註解原始碼的註釋中,我們可以知道該註解用來給RestTemplate標記,以使用負載均衡的客戶端(LoadBalancerClient)來配置它。

package org.springframework.cloud.client.loadbalancer;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

通過搜尋LoadBalancerClient,我們可以發現這是Spring Cloud中定義的一個介面:

package org.springframework.cloud.client.loadbalancer;

import org.springframework.cloud.client.ServiceInstance;

import java.io.IOException;
import java.net.URI;

/**
 * Represents a client side load balancer
 * @author Spencer Gibb
 */
public interface LoadBalancerClient extends ServiceInstanceChooser {

	/**
	 * execute request using a ServiceInstance from the LoadBalancer for the specified
	 * service
	 * @param serviceId the service id to look up the LoadBalancer
	 * @param request allows implementations to execute pre and post actions such as
	 * incrementing metrics
	 * @return the result of the LoadBalancerRequest callback on the selected
	 * ServiceInstance
	 */
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

	/**
	 * execute request using a ServiceInstance from the LoadBalancer for the specified
	 * service
	 * @param serviceId the service id to look up the LoadBalancer
	 * @param serviceInstance the service to execute the request to
	 * @param request allows implementations to execute pre and post actions such as
	 * incrementing metrics
	 * @return the result of the LoadBalancerRequest callback on the selected
	 * ServiceInstance
	 */
	<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;

	/**
	 * Create a proper URI with a real host and port for systems to utilize.
	 * Some systems use a URI with the logical serivce name as the host,
	 * such as http://myservice/path/to/service.  This will replace the
	 * service name with the host:port from the ServiceInstance.
	 * @param instance
	 * @param original a URI with the host as a logical service name
	 * @return a reconstructed URI
	 */
	URI reconstructURI(ServiceInstance instance, URI original);
}

 該介面中繼承了ServiceInstanceChooser介面,我們可以通過定義的抽象方法來了解到客戶端負載均衡器中應具備的幾種能力:

  • ServiceInstance choose(String serviceId):根據傳入的服務名serviceId,從負載均衡器中挑選一個對應服務的例項。
  • T execute(String serviceId, LoadBalancerRequest request) throws IOException:使用從負載均衡器中挑選出的服務例項來執行請求內容。
  • URI reconstructURI(ServiceInstance instance, URI original):為系統構建一個合適的“host:port”形式的URI。在分散式系統中,我們使用邏輯上的服務名稱作為host來構建URI(替代服務例項的“host:port”形式)進行請求,比如:http://myservice/path/to/service。在該操作的定義中,前者ServiceInstance物件是帶有host和port的具體服務例項,而後者URI物件則是使用邏輯服務名定義為host的URI,而返回的URI內容則是通過ServiceInstance的服務例項詳情拼接出的具體“host:post”形式的請求地址。

順著LoadBalancerClient介面的所屬包org.springframework.cloud.client.loadbalancer,我們對其內容進行整理,可以得出如下圖的關係:

 從類的命名上我們初步判斷LoadBalancerAutoConfiguration為實現客戶端負載均衡器的自動化配置類。通過檢視原始碼,我們可以驗證這一點假設:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}

	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(
			LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
	}

	@Configuration
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {
		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryAutoConfiguration {
		@Bean
		@ConditionalOnMissingBean
		public RetryTemplate retryTemplate() {
			RetryTemplate template =  new RetryTemplate();
			template.setThrowLastExceptionOnExhausted(true);
			return template;
		}

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
			return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
		}

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory() {
			return new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory();
		}

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedRetryListenerFactory loadBalancedRetryListenerFactory() {
			return new LoadBalancedRetryListenerFactory.DefaultRetryListenerFactory();
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryInterceptorAutoConfiguration {
		@Bean
		@ConditionalOnMissingBean
		public RetryLoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
				LoadBalancedRetryPolicyFactory lbRetryPolicyFactory,
				LoadBalancerRequestFactory requestFactory,
				LoadBalancedBackOffPolicyFactory backOffPolicyFactory,
				LoadBalancedRetryListenerFactory retryListenerFactory) {
			return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
					lbRetryPolicyFactory, requestFactory, backOffPolicyFactory, retryListenerFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}
	}
}

LoadBalancerAutoConfiguration類頭上的註解可以知道Ribbon實現的負載均衡自動化配置需要滿足下面兩個條件:

  • @ConditionalOnClass(RestTemplate.class)RestTemplate類必須存在於當前工程的環境中。
  • @ConditionalOnBean(LoadBalancerClient.class):在Spring的Bean工程中有必須有LoadBalancerClient的實現Bean。

在該自動化配置類中,主要做了下面三件事:

  • 建立了一個LoadBalancerInterceptor的Bean,用於實現對客戶端發起請求時進行攔截,以實現客戶端負載均衡。
  • 建立了一個RestTemplateCustomizer的Bean,用於給RestTemplate增加LoadBalancerInterceptor攔截器。
  • 維護了一個被@LoadBalanced註解修飾的RestTemplate物件列表,並在這裡進行初始化,通過呼叫RestTemplateCustomizer的例項來給需要客戶端負載均衡的RestTemplate增加LoadBalancerInterceptor攔截器。

接下來,我們看看LoadBalancerInterceptor攔截器是如何將一個普通的RestTemplate變成客戶端負載均衡的:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
		this.loadBalancer = loadBalancer;
		this.requestFactory = requestFactory;
	}

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		// for backwards compatibility
		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
	}

	@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, requestFactory.createRequest(request, body, execution));
	}
}

通過原始碼以及之前的自動化配置類,我們可以看到在攔截器中注入了LoadBalancerClient的實現。當一個被@LoadBalanced註解修飾的RestTemplate物件向外發起HTTP請求時,會被LoadBalancerInterceptor類的intercept函式所攔截。由於我們在使用RestTemplate時候採用了服務名作為host,所以直接從HttpRequest的URI物件中通過getHost()就可以拿到服務名,然後呼叫execute函式去根據服務名來選擇例項併發起實際的請求。

分析到這裡,LoadBalancerClient還只是一個抽象的負載均衡器介面,所以我們還需要找到它的具體實現類來進一步分析。通過檢視ribbon的原始碼,我們可以很容易的在org.springframework.cloud.netflix.ribbon包下找到對應的實現類:RibbonLoadBalancerClient

	@Override
	public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
		Server server = null;
		if(serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer)serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);
		RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

		try {
			T returnVal = request.apply(serviceInstance);
			statsRecorder.recordStats(returnVal);
			return returnVal;
		}
		// catch IOException and rethrow so RestTemplate behaves correctly
		catch (IOException ex) {
			statsRecorder.recordStats(ex);
			throw ex;
		}
		catch (Exception ex) {
			statsRecorder.recordStats(ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}

可以看到,在execute函式的實現中,首先從服務列表中獲取RibbonServer,然後根據服務ID獲取RibbonLoadBalanceContext上下文物件,最後建立一個Ribbon的統計記錄物件,程式碼如下:

	public static class RibbonServer implements ServiceInstance {
		private final String serviceId;
		private final Server server;
		private final boolean secure;
		private Map<String, String> metadata;

		public RibbonServer(String serviceId, Server server) {
			this(serviceId, server, false, Collections.<String, String> emptyMap());
		}

		public RibbonServer(String serviceId, Server server, boolean secure,
				Map<String, String> metadata) {
			this.serviceId = serviceId;
			this.server = server;
			this.secure = secure;
			this.metadata = metadata;
		}

		@Override
		public String getServiceId() {
			return this.serviceId;
		}

		@Override
		public String getHost() {
			return this.server.getHost();
		}

		@Override
		public int getPort() {
			return this.server.getPort();
		}

		@Override
		public boolean isSecure() {
			return this.secure;
		}

		@Override
		public URI getUri() {
			return DefaultServiceInstance.getUri(this);
		}

		@Override
		public Map<String, String> getMetadata() {
			return this.metadata;
		}

		public Server getServer() {
			return this.server;
		}

		@Override
		public String toString() {
			final StringBuffer sb = new StringBuffer("RibbonServer{");
			sb.append("serviceId='").append(serviceId).append('\'');
			sb.append(", server=").append(server);
			sb.append(", secure=").append(secure);
			sb.append(", metadata=").append(metadata);
			sb.append('}');
			return sb.toString();
		}
	}