Spring Cloud 升級之路 - 2020.0.x - 6. 使用 Spring Cloud LoadBalancer (1)

乾貨滿滿張雜湊發表於2021-05-28

本專案程式碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

我們使用 Spring Cloud 官方推薦的 Spring Cloud LoadBalancer 作為我們的客戶端負載均衡器。

Spring Cloud LoadBalancer背景

Spring Cloud LoadBalancer是一個客戶端負載均衡器,類似於Ribbon,但是由於Ribbon已經進入維護模式,並且Ribbon 2並不與Ribbon 1相互相容,所以Spring Cloud全家桶在Spring Cloud Commons專案中,新增了Spring cloud Loadbalancer作為新的負載均衡器,並且做了向前相容,就算你的專案中繼續用 Spring Cloud Netflix 套裝(包括Ribbon,Eureka,Zuul,Hystrix等等)讓你的專案中有這些依賴,你也可以通過簡單的配置,把ribbon替換成Spring Cloud LoadBalancer。

負載均衡器在哪裡使用?

Spring Cloud 中內部微服務呼叫預設是 http 請求,主要通過下面三種 API:

  • RestTemplate:同步 http API
  • WebClient:非同步響應式 http API
  • 三方客戶端封裝,例如 openfeign

如果專案中加入了 spring-cloud-loadbalancer 的依賴並且配置啟用了,那麼會自動在相關的 Bean 中加入負載均衡器的特性。

  • 對於 RestTemplate,會自動對所有 @LoadBalanced 註解修飾的 RestTemplate Bean 增加 Interceptor 從而加上了負載均衡器的特性。
  • 對於 WebClient,會自動建立 ReactorLoadBalancerExchangeFilterFunction,我們可以通過加入ReactorLoadBalancerExchangeFilterFunction會加入負載均衡器的特性。
  • 對於三方客戶端,一般不需要我們額外配置什麼。

這些使用的示例,會在我們系列升級完最後的測試部分看到。

Spring Cloud LoadBalancer 結構簡介

上一節我們提到了 NamedContextFactory,Spring Cloud LoadBalancer 這裡也是使用了這個機制實現了不同微服務使用不同的 Spring Cloud LoadBalancer 配置。相關核心實現是 @LoadBalancerClient@LoadBalancerClients 這兩個註解,以及 NamedContextFactory.Specification 的實現 LoadBalancerClientSpecificationNamedContextFactory 的實現 LoadBalancerClientFactory

經過上一節的詳細分析,我們知道可以通過 LoadBalancerClientFactory 知道預設配置類LoadBalancerClientConfiguration. 並且獲取微服務名稱可以通過 environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

LoadBalancerClientFactory

public static final String NAMESPACE = "loadbalancer";
public static final String PROPERTY_NAME = NAMESPACE + ".client.name";
public LoadBalancerClientFactory() {
	super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
}

檢視配置類 LoadBalancerClientConfiguration,我們可以發現這個類主要定義兩種 Bean,分別是 ReactorLoadBalancer<ServiceInstance>ServiceInstanceListSupplier

ReactorLoadBalancer 是負載均衡器,主要提供根據服務名稱獲取服務例項列表並從從中選擇的功能。

ReactorLoadBalancer

Mono<Response<T>> choose(Request request);

在預設配置中的實現是:

LoadBalancerClientConfiguration

@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
		Environment environment,
		LoadBalancerClientFactory loadBalancerClientFactory) {
	//獲取微服務名稱
	String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
	//建立 RoundRobinLoadBalancer 
	//注意這裡注入的是 LazyProvider,這主要因為在註冊這個 Bean 的時候相關的 Bean 可能還沒有被載入註冊,利用 LazyProvider 而不是直接注入所需的 Bean 防止報找不到 Bean 注入的錯誤。
	return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
			ServiceInstanceListSupplier.class), name);
}

可以看出,預設配置的 ReactorLoadBalancer 實現是 RoundRobinLoadBalancer。這個負載均衡器實現很簡單,有一個原子型別的 AtomicInteger position,從 ServiceInstanceListSupplier 中讀取所有的服務例項列表,然後對於 position 原子加1,對列表大小取模,返回列表中這個位置的服務例項 ServiceInstance

RoundRobinLoadBalancer

public Mono<Response<ServiceInstance>> choose(Request request) {
    //注入的時候注入的是 Lazy Provider,這裡取出真正的 Bean,也就是 ServiceInstanceListSupplier
	ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
			.getIfAvailable(NoopServiceInstanceListSupplier::new);
			//獲取例項列表
	return supplier.get(request)
	        .next()
	        //從列表中選擇一個例項
			.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}

private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
		List<ServiceInstance> serviceInstances) {
	Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
	// 如果 ServiceInstanceListSupplier 也實現了 SelectedInstanceCallback,則執行下面的邏輯進行回撥。SelectedInstanceCallback 就是每次負載均衡器選擇例項之後進行的回撥
	if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
		((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
	}
	return serviceInstanceResponse;
}

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
	if (instances.isEmpty()) {
		return new EmptyResponse();
	}
	//postion 原子 +1 並取絕對值
	int pos = Math.abs(this.position.incrementAndGet());
    //返回對應下標的例項
	ServiceInstance instance = instances.get(pos % instances.size());
	return new DefaultResponse(instance);
}

ServiceInstanceListSupplier 是服務列表提供者介面:

ServiceInstanceListSupplier

public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> {
	String getServiceId();
	default Flux<List<ServiceInstance>> get(Request request) {
		return get();
	}
	static ServiceInstanceListSupplierBuilder builder() {
		return new ServiceInstanceListSupplierBuilder();
	}
}

spring-cloud-loadbalancer 中有很多 ServiceInstanceListSupplier 的實現,在預設配置中是通過屬性配置指定實現的,這個配置項是spring.cloud.loadbalancer.configurations。例如:

LoadBalancerClientConfiguration

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
//spring.cloud.loadbalancer.configurations 未指定或者為 default
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default",
		matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
		ConfigurableApplicationContext context) {
	return ServiceInstanceListSupplier.builder()
	//通過 DiscoveryClient 提供例項
	.withDiscoveryClient()
	//開啟快取
	.withCaching()
	.build(context);
}

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
//如果 spring.cloud.loadbalancer.configurations 指定為 zone-preference
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "zone-preference")
public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceListSupplier(
		ConfigurableApplicationContext context) {
	return ServiceInstanceListSupplier.builder()
	//通過 DiscoveryClient 提供例項
	.withDiscoveryClient()
	//啟用更傾向於同一個 zone 下例項的特性
	.withZonePreference()
	//開啟快取
	.withCaching()
			.build(context);
}

可以看到,可以通過 ServiceInstanceListSupplier.builder() 生成官方封裝好各種特性的 ServiceInstanceListSupplier。其實從底層實現可以看出,所有的 ServiceInstanceListSupplier 實現都是代理模式,例如對於預設配置,底層程式碼近似於:

return  //開啟服務例項快取
        new CachingServiceInstanceListSupplier(
                        //啟用通過 discoveryClient 的服務發現
                        new DiscoveryClientServiceInstanceListSupplier(
                                discoveryClient, env
                        )
                , cacheManagerProvider.getIfAvailable()
        );

除了預設配置 LoadBalancerClientConfiguration,使用者配置自定義配置則是通過 @LoadBalancerClients@LoadBalancerClient.這個原理是通過 LoadBalancerClientConfigurationRegistrar 實現的。首先,我們來看一下 LoadBalancerClientFactory 這個 NamedContextFactory 是如何建立的:

[LoadBalancerAutoConfiguration]

private final ObjectProvider<List<LoadBalancerClientSpecification>> configurations;

public LoadBalancerAutoConfiguration(ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
    //注入 LoadBalancerClientSpecification List 的 provider
    //在 Bean 建立的時候,進行載入,而不是註冊的時候
	this.configurations = configurations;
}

@ConditionalOnMissingBean
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
    //建立 LoadBalancerClientFactory
	LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory();
    //讀取所有的 LoadBalancerClientSpecification,設定為 LoadBalancerClientFactory 的配置
	clientFactory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList));
	return clientFactory;
}

那麼,LoadBalancerClientSpecification 這些 Bean 是怎麼建立的呢?在 @LoadBalancerClients@LoadBalancerClient 註解中,都包含 @Import(LoadBalancerClientConfigurationRegistrar.class)。這個 @Import 載入一個 ImportBeanDefinitionRegistrar,這裡是 LoadBalancerClientConfigurationRegistrar. ImportBeanDefinitionRegistrar裡面的方法引數包含註解後設資料,以及註冊 Bean 的 BeanDefinitionRegistry。一般通過註解後設資料,動態通過 BeanDefinitionRegistry 註冊 Bean,在這裡的實現是:

[LoadBalancerClients]

@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(LoadBalancerClientConfigurationRegistrar.class)
public @interface LoadBalancerClients {
    //可以指定多個 LoadBalancerClient
	LoadBalancerClient[] value() default {};
	//指定所有的負載均衡配置的預設配置
	Class<?>[] defaultConfiguration() default {};
}

[LoadBalancerClient]

@Configuration(proxyBeanMethods = false)
@Import(LoadBalancerClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoadBalancerClient {
    //name 和 value 都是微服務名稱
	@AliasFor("name")
	String value() default "";
	@AliasFor("value")
	String name() default "";
	//這個微服務的配置
	Class<?>[] configuration() default {};
}

[LoadBalancerClientConfigurationRegistrar]

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    //獲取 LoadBalancerClients 註解的後設資料
	Map<String, Object> attrs = metadata.getAnnotationAttributes(LoadBalancerClients.class.getName(), true);
	if (attrs != null && attrs.containsKey("value")) {
		AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
		//對於 value 屬性,其實就是一個 LoadBalancerClient 列表,對於每個生成一個特定微服務名字的  LoadBalancerClientSpecification
		for (AnnotationAttributes client : clients) {
			registerClientConfiguration(registry, getClientName(client), client.get("configuration"));
		}
	}
	//如果指定了 defaultConfiguration,則註冊為 default 的配置
	if (attrs != null && attrs.containsKey("defaultConfiguration")) {
		String name;
		if (metadata.hasEnclosingClass()) {
			name = "default." + metadata.getEnclosingClassName();
		}
		else {
			name = "default." + metadata.getClassName();
		}
		registerClientConfiguration(registry, name, attrs.get("defaultConfiguration"));
	}
	//獲取 LoadBalancerClient 註解的後設資料
	Map<String, Object> client = metadata.getAnnotationAttributes(LoadBalancerClient.class.getName(), true);
	String name = getClientName(client);
	if (name != null) {
		registerClientConfiguration(registry, name, client.get("configuration"));
	}
}

private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
    //初始化 LoadBalancerClientSpecification 的 BeanDefinition,用於註冊一個 LoadBalancerClientSpecification Bean
	BeanDefinitionBuilder builder = BeanDefinitionBuilder
			.genericBeanDefinition(LoadBalancerClientSpecification.class);
	//構造器引數
	builder.addConstructorArgValue(name);
	builder.addConstructorArgValue(configuration);
	//註冊 Bean
	registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification", builder.getBeanDefinition());
}

從程式碼中我們可以看出,通過使用 @LoadBalancerClients@LoadBalancerClient 註解可以自動生成對應的 LoadBalancerClientSpecification 進而實現公共負載均衡配置或者特定某個微服務的負載均衡配置。

微信搜尋“我的程式設計喵”關注公眾號,加作者微信,每日一刷,輕鬆提升技術,斬獲各種offer

image

相關文章