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