SpringCloud Alibaba微服務(wù)實(shí)戰(zhàn)之實(shí)現(xiàn)網(wǎng)關(guān)的灰度發(fā)布
本文轉(zhuǎn)載自微信公眾號「JAVA日知錄」,作者飄渺Jam 。轉(zhuǎn)載本文請聯(lián)系JAVA日知錄公眾號。
前言
這篇文章來源于粉絲提出的一個問題:如何解決多環(huán)境統(tǒng)一注冊中心服務(wù)實(shí)例亂竄?
怎么理解呢?
假設(shè)現(xiàn)在開發(fā)環(huán)境的AccountService已經(jīng)在Nacos中注冊了,現(xiàn)在小張需要對它進(jìn)行修改升級,本地啟動AccountService后也注冊到了Nacos,但是在調(diào)試的時候請求通過網(wǎng)關(guān)經(jīng)常直接跳轉(zhuǎn)到開發(fā)環(huán)境,這樣的話小張就沒辦法安心debug了。
其實(shí)這個問題歸根結(jié)底是如何基于SpringCloud Gateway實(shí)現(xiàn)灰度發(fā)布,通過指定的規(guī)則讓請求流量到達(dá)特定的實(shí)例。
在SpringCloud 2020 版本中官方推薦使用Spring Cloud LoadBalancer 來替換原Ribbon的負(fù)載均衡器。所以本篇文章我們直接基于Spring Cloud LoadBalancer來實(shí)現(xiàn)。
tips:何為灰度發(fā)布
灰度發(fā)布(又名金絲雀發(fā)布)是指在黑與白之間,能夠平滑過渡的一種發(fā)布方式。在其上可以進(jìn)行A/B testing,即讓一部分用戶繼續(xù)用產(chǎn)品特性A,一部分用戶開始用產(chǎn)品特性B,如果用戶對B沒有什么反對意見,那么逐步擴(kuò)大范圍,把所有用戶都遷移到B上面來?;叶劝l(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時候就可以發(fā)現(xiàn)、調(diào)整問題,以保證其影響度。
實(shí)現(xiàn)目標(biāo)
目標(biāo)很明確,小張希望在調(diào)試的時候發(fā)出的請求能直接到達(dá)自己的本地開發(fā)環(huán)境,方便調(diào)試。
實(shí)現(xiàn)思路
要實(shí)現(xiàn)此目標(biāo)我們需要解決兩個關(guān)鍵的問題:
如何區(qū)分不同的實(shí)例
需要給小張本地啟動的AccountService服務(wù)實(shí)例一個特殊標(biāo)識,讓它與開發(fā)環(huán)境的區(qū)分開。
這里我們可以使用注冊中心的元數(shù)據(jù)metadata來區(qū)分,可以通過spring.cloud.nacos.discovery.metadata.version = dev配置指定,也可以在nacos服務(wù)列表中直接添加元數(shù)據(jù)信息。
實(shí)現(xiàn)自定義的負(fù)載均衡規(guī)則,通過自定義規(guī)則讓負(fù)載均衡器能找到我們需要的服務(wù)實(shí)例
小張在請求服務(wù)的時候需要在請求頭上添加標(biāo)簽,version=dev,自定義負(fù)載均衡器在獲取到請求頭信息后去服務(wù)實(shí)例中查找配置了mtadata.version=dev的服務(wù)實(shí)例。
Spring Cloud LoadBalancer(SCL)
SCL 負(fù)載均衡策略
在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.
結(jié)合文檔中的其他內(nèi)容,提取出幾條關(guān)鍵信息:
Spring Cloud LoadBalancer提供了兩種負(fù)載均衡算法:Round-Robin-based 和 Random,默認(rèn)使用Round-Robin-based
可以通過實(shí)現(xiàn)ServiceInstanceListSupplier來篩選符合要求的服務(wù)實(shí)例
需要通過 LoadBalancerClient 注解,指定服務(wù)級別的負(fù)載均衡策略以及實(shí)例選擇策略
提示:如果大家需要探究SCL的實(shí)現(xiàn)原理,可以通過GatewayReactiveLoadBalancerClientAutoConfiguration入手。
自定義灰度發(fā)布
結(jié)合上文,利用Spring Cloud LoadBalancer實(shí)現(xiàn)灰度我們有兩種實(shí)現(xiàn)方式:
簡單粗暴,直接實(shí)現(xiàn)一個新的負(fù)載均衡策略,然后通過LoadBalancerClient注解指定服務(wù)實(shí)例使用此策略。
自定義服務(wù)實(shí)例篩選邏輯,在返回給前端實(shí)例時篩選出符合要求的服務(wù)實(shí)例,當(dāng)然也需要通過LoadBalancerClient注解指定服務(wù)實(shí)例使用此選擇器。
代碼實(shí)現(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>
 
自定義負(fù)載均衡策略
首先我們來看第一種實(shí)現(xiàn)方式,通過自定義負(fù)載均衡策略來實(shí)現(xiàn)。
在網(wǎng)關(guān)模塊引入 SCL ,同時需要剔除nacos注冊中心自帶的Ribbon負(fù)載均衡器。
- <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>
 
自定義負(fù)載均衡策略 VersionGrayLoadBalancer
- /**
 - * Description:
 - * 自定義灰度
 - * 通過給請求頭添加Version 與 Service Instance 元數(shù)據(jù)屬性進(jìn)行對比
 - * @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);
 - }
 - }
 - }
 - /**
 - * 負(fù)載均衡器
 - * 參考 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ù)服務(wù)實(shí)例元數(shù)據(jù)中的version屬性進(jìn)行匹配,對于符合條件的實(shí)例參考Round-Robin-based實(shí)現(xiàn)方法。
編寫配置類VersionLoadBalancerConfiguration,用于替換默認(rèn)的負(fù)載均衡算法
- /**
 - * Description:
 - * 自定義負(fù)載均衡器配置實(shí)現(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)關(guān)啟動類使用注解@LoadBalancerClient指定哪些服務(wù)使用自定義負(fù)載均衡算法
通過@LoadBalancerClient(value = "auth-service", configuration = VersionLoadBalancerConfiguration.class),對于auth-service啟用自定義負(fù)載均衡算法;
或通過@LoadBalancerClients(defaultConfiguration = VersionLoadBalancerConfiguration.class)為所有服務(wù)啟用自定義負(fù)載均衡算法。
自定義服務(wù)實(shí)例篩選邏輯
接下來我們看第二種實(shí)現(xiàn)方法,通過實(shí)現(xiàn)ServiceInstanceListSupplier來自定義服務(wù)篩選邏輯,我們可以直接繼承DelegatingServiceInstanceListSupplier來實(shí)現(xiàn)。
在網(wǎng)關(guān)模塊引入Spring Cloud LoadBalancer(同上)
自定義服務(wù)實(shí)例篩選邏輯VersionServiceInstanceListSupplier
- /**
 - * 自定義服務(wù)實(shí)例篩選邏輯
 - * @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;
 - }
 - }
 
實(shí)現(xiàn)原理跟自定義負(fù)載均衡策略一樣,根據(jù)version匹配符合要求的服務(wù)實(shí)例。
編寫配置類VersionServiceInstanceListSupplierConfiguration,用于替換默認(rèn)服務(wù)實(shí)例篩選邏輯
- public class VersionServiceInstanceListSupplierConfiguration {
 - @Bean
 - ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) {
 - ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
 - .withDiscoveryClient()
 - .withCaching()
 - .build(context);
 - return new VersionServiceInstanceListSupplier(delegate);
 - }
 - }
 
在網(wǎng)關(guān)啟動類使用注解@LoadBalancerClient指定哪些服務(wù)使用自定義負(fù)載均衡算法
通過@LoadBalancerClient(value = "auth-service", configuration = VersionServiceInstanceListSupplierConfiguration.class),對于auth-service啟用自定義負(fù)載均衡算法;
或通過@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)為所有服務(wù)啟用自定義負(fù)載均衡算法。
測試
啟動多個AccountService實(shí)例,對于58302端口的實(shí)例配置元數(shù)據(jù)version = dev
postman 調(diào)用接口時指定請求頭
通過debug模式觀察兩種實(shí)現(xiàn)邏輯,觀察結(jié)果是否符合預(yù)期。
小結(jié)
本篇文章咱們基于SCL通過擴(kuò)展負(fù)載均衡算法以及修改服務(wù)實(shí)例篩選邏輯兩種方式實(shí)現(xiàn)了簡單的灰度發(fā)布功能,大家可以參考此實(shí)現(xiàn)擴(kuò)展SCL的負(fù)載均衡算法或者定制自己的服務(wù)篩選邏輯。





















 
 
 











 
 
 
 