SpringCloud Alibaba微服務實戰(zhàn)之實現(xiàn)網(wǎng)關的灰度發(fā)布
本文轉載自微信公眾號「JAVA日知錄」,作者飄渺Jam 。轉載本文請聯(lián)系JAVA日知錄公眾號。
前言
這篇文章來源于粉絲提出的一個問題:如何解決多環(huán)境統(tǒng)一注冊中心服務實例亂竄?
怎么理解呢?
假設現(xiàn)在開發(fā)環(huán)境的AccountService已經(jīng)在Nacos中注冊了,現(xiàn)在小張需要對它進行修改升級,本地啟動AccountService后也注冊到了Nacos,但是在調(diào)試的時候請求通過網(wǎng)關經(jīng)常直接跳轉到開發(fā)環(huán)境,這樣的話小張就沒辦法安心debug了。
其實這個問題歸根結底是如何基于SpringCloud Gateway實現(xiàn)灰度發(fā)布,通過指定的規(guī)則讓請求流量到達特定的實例。
在SpringCloud 2020 版本中官方推薦使用Spring Cloud LoadBalancer 來替換原Ribbon的負載均衡器。所以本篇文章我們直接基于Spring Cloud LoadBalancer來實現(xiàn)。
tips:何為灰度發(fā)布
灰度發(fā)布(又名金絲雀發(fā)布)是指在黑與白之間,能夠平滑過渡的一種發(fā)布方式。在其上可以進行A/B testing,即讓一部分用戶繼續(xù)用產(chǎn)品特性A,一部分用戶開始用產(chǎn)品特性B,如果用戶對B沒有什么反對意見,那么逐步擴大范圍,把所有用戶都遷移到B上面來。灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時候就可以發(fā)現(xiàn)、調(diào)整問題,以保證其影響度。
實現(xiàn)目標
目標很明確,小張希望在調(diào)試的時候發(fā)出的請求能直接到達自己的本地開發(fā)環(huán)境,方便調(diào)試。
實現(xiàn)思路
要實現(xiàn)此目標我們需要解決兩個關鍵的問題:
如何區(qū)分不同的實例
需要給小張本地啟動的AccountService服務實例一個特殊標識,讓它與開發(fā)環(huán)境的區(qū)分開。
這里我們可以使用注冊中心的元數(shù)據(jù)metadata來區(qū)分,可以通過spring.cloud.nacos.discovery.metadata.version = dev配置指定,也可以在nacos服務列表中直接添加元數(shù)據(jù)信息。
實現(xiàn)自定義的負載均衡規(guī)則,通過自定義規(guī)則讓負載均衡器能找到我們需要的服務實例
小張在請求服務的時候需要在請求頭上添加標簽,version=dev,自定義負載均衡器在獲取到請求頭信息后去服務實例中查找配置了mtadata.version=dev的服務實例。
Spring Cloud LoadBalancer(SCL)
SCL 負載均衡策略
在Spring Cloud LoadBalancer 官方文檔上有這樣一段說明:
Spring Cloud provides its own client-side load-balancer abstraction and implementation. For the load-balancing mechanism, ReactiveLoadBalancer interface has been added and a Round-Robin-based and Random implementations have been provided for it. In order to get instances to select from reactive ServiceInstanceListSupplier is used. Currently we support a service-discovery-based implementation of ServiceInstanceListSupplier that retrieves available instances from Service Discovery using a Discovery Client available in the classpath.
結合文檔中的其他內(nèi)容,提取出幾條關鍵信息:
Spring Cloud LoadBalancer提供了兩種負載均衡算法:Round-Robin-based 和 Random,默認使用Round-Robin-based
可以通過實現(xiàn)ServiceInstanceListSupplier來篩選符合要求的服務實例
需要通過 LoadBalancerClient 注解,指定服務級別的負載均衡策略以及實例選擇策略
提示:如果大家需要探究SCL的實現(xiàn)原理,可以通過GatewayReactiveLoadBalancerClientAutoConfiguration入手。
自定義灰度發(fā)布
結合上文,利用Spring Cloud LoadBalancer實現(xiàn)灰度我們有兩種實現(xiàn)方式:
簡單粗暴,直接實現(xiàn)一個新的負載均衡策略,然后通過LoadBalancerClient注解指定服務實例使用此策略。
自定義服務實例篩選邏輯,在返回給前端實例時篩選出符合要求的服務實例,當然也需要通過LoadBalancerClient注解指定服務實例使用此選擇器。
代碼實現(xiàn)
版本說明
SpringCloud 項目使用的版本是SpringCloud alibaba推薦的畢業(yè)版本
- <spring-boot.version>2.4.2</spring-boot.version>
- <alibaba-cloud.version>2021.1</alibaba-cloud.version>
- <springcloud.version>2020.0.0</springcloud.version>
自定義負載均衡策略
首先我們來看第一種實現(xiàn)方式,通過自定義負載均衡策略來實現(xiàn)。
在網(wǎng)關模塊引入 SCL ,同時需要剔除nacos注冊中心自帶的Ribbon負載均衡器。
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
- <exclusions>
- <exclusion>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-loadbalancer</artifactId>
- </dependency>
自定義負載均衡策略 VersionGrayLoadBalancer
- /**
- * Description:
- * 自定義灰度
- * 通過給請求頭添加Version 與 Service Instance 元數(shù)據(jù)屬性進行對比
- * @author Jam
- * @date 2021/6/1 17:26
- */
- @Log4j2
- public class VersionGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
- private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
- private final String serviceId;
- private final AtomicInteger position;
- public VersionGrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
- this(serviceInstanceListSupplierProvider,serviceId,new Random().nextInt(1000));
- }
- public VersionGrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
- String serviceId, int seedPosition) {
- this.serviceId = serviceId;
- this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
- this.position = new AtomicInteger(seedPosition);
- }
- @Override
- public Mono<Response<ServiceInstance>> choose(Request request) {
- ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
- return supplier.get(request).next()
- .map(serviceInstances -> processInstanceResponse(serviceInstances,request));
- }
- private Response<ServiceInstance> processInstanceResponse(List<ServiceInstance> instances, Request request) {
- if (instances.isEmpty()) {
- log.warn("No servers available for service: " + this.serviceId);
- return new EmptyResponse();
- } else {
- DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();
- RequestData clientRequest = (RequestData) requestContext.getClientRequest();
- HttpHeaders headers = clientRequest.getHeaders();
- // get Request Header
- String reqVersion = headers.getFirst("version");
- if(StringUtils.isEmpty(reqVersion)){
- return processRibbonInstanceResponse(instances);
- }
- log.info("request header version : {}",reqVersion );
- // filter service instances
- List<ServiceInstance> serviceInstances = instances.stream()
- .filter(instance -> reqVersion.equals(instance.getMetadata().get("version")))
- .collect(Collectors.toList());
- if(serviceInstances.size() > 0){
- return processRibbonInstanceResponse(serviceInstances);
- }else{
- return processRibbonInstanceResponse(instances);
- }
- }
- }
- /**
- * 負載均衡器
- * 參考 org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#getInstanceResponse
- * @author javadaily
- */
- private Response<ServiceInstance> processRibbonInstanceResponse(List<ServiceInstance> instances) {
- int pos = Math.abs(this.position.incrementAndGet());
- ServiceInstance instance = instances.get(pos % instances.size());
- return new DefaultResponse(instance);
- }
- }
獲取請求頭中的version屬性,然后根據(jù)服務實例元數(shù)據(jù)中的version屬性進行匹配,對于符合條件的實例參考Round-Robin-based實現(xiàn)方法。
編寫配置類VersionLoadBalancerConfiguration,用于替換默認的負載均衡算法
- /**
- * Description:
- * 自定義負載均衡器配置實現(xiàn)類
- * @author javadaily
- * @date 2021/6/3 16:02
- */
- public class VersionLoadBalancerConfiguration {
- @Bean
- ReactorLoadBalancer<ServiceInstance> versionGrayLoadBalancer(Environment environment,
- LoadBalancerClientFactory loadBalancerClientFactory) {
- String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
- return new VersionGrayLoadBalancer(
- loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
- }
- }
VersionLoadBalancerConfiguration配置類不能添加@Configuration注解。
在網(wǎng)關啟動類使用注解@LoadBalancerClient指定哪些服務使用自定義負載均衡算法
通過@LoadBalancerClient(value = "auth-service", configuration = VersionLoadBalancerConfiguration.class),對于auth-service啟用自定義負載均衡算法;
或通過@LoadBalancerClients(defaultConfiguration = VersionLoadBalancerConfiguration.class)為所有服務啟用自定義負載均衡算法。
自定義服務實例篩選邏輯
接下來我們看第二種實現(xiàn)方法,通過實現(xiàn)ServiceInstanceListSupplier來自定義服務篩選邏輯,我們可以直接繼承DelegatingServiceInstanceListSupplier來實現(xiàn)。
在網(wǎng)關模塊引入Spring Cloud LoadBalancer(同上)
自定義服務實例篩選邏輯VersionServiceInstanceListSupplier
- /**
- * 自定義服務實例篩選邏輯
- * @author javadaily
- * 參考:org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier
- */
- @Log4j2
- public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
- public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
- super(delegate);
- }
- @Override
- public Flux<List<ServiceInstance>> get() {
- return delegate.get();
- }
- @Override
- public Flux<List<ServiceInstance>> get(Request request) {
- return delegate.get(request).map(instances -> filteredByVersion(instances,getVersion(request.getContext())));
- }
- /**
- * filter instance by requestVersion
- * @author javadaily
- */
- private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String requestVersion) {
- log.info("request version is {}",requestVersion);
- if(StringUtils.isEmpty(requestVersion)){
- return instances;
- }
- List<ServiceInstance> filteredInstances = instances.stream()
- .filter(instance -> requestVersion.equalsIgnoreCase(instance.getMetadata().getOrDefault("version","")))
- .collect(Collectors.toList());
- if (filteredInstances.size() > 0) {
- return filteredInstances;
- }
- return instances;
- }
- private String getVersion(Object requestContext) {
- if (requestContext == null) {
- return null;
- }
- String version = null;
- if (requestContext instanceof RequestDataContext) {
- version = getVersionFromHeader((RequestDataContext) requestContext);
- }
- return version;
- }
- /**
- * get version from header
- * @author javadaily
- */
- private String getVersionFromHeader(RequestDataContext context) {
- if (context.getClientRequest() != null) {
- HttpHeaders headers = context.getClientRequest().getHeaders();
- if (headers != null) {
- //could extract to the properties
- return headers.getFirst("version");
- }
- }
- return null;
- }
- }
實現(xiàn)原理跟自定義負載均衡策略一樣,根據(jù)version匹配符合要求的服務實例。
編寫配置類VersionServiceInstanceListSupplierConfiguration,用于替換默認服務實例篩選邏輯
- public class VersionServiceInstanceListSupplierConfiguration {
- @Bean
- ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) {
- ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
- .withDiscoveryClient()
- .withCaching()
- .build(context);
- return new VersionServiceInstanceListSupplier(delegate);
- }
- }
在網(wǎng)關啟動類使用注解@LoadBalancerClient指定哪些服務使用自定義負載均衡算法
通過@LoadBalancerClient(value = "auth-service", configuration = VersionServiceInstanceListSupplierConfiguration.class),對于auth-service啟用自定義負載均衡算法;
或通過@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)為所有服務啟用自定義負載均衡算法。
測試
啟動多個AccountService實例,對于58302端口的實例配置元數(shù)據(jù)version = dev
postman 調(diào)用接口時指定請求頭
通過debug模式觀察兩種實現(xiàn)邏輯,觀察結果是否符合預期。
小結
本篇文章咱們基于SCL通過擴展負載均衡算法以及修改服務實例篩選邏輯兩種方式實現(xiàn)了簡單的灰度發(fā)布功能,大家可以參考此實現(xiàn)擴展SCL的負載均衡算法或者定制自己的服務篩選邏輯。