Spring Cloud灰度部署

news/2024/9/18 0:54:44/

1、背景(灰度部署)

在我们系统发布生产环境时,有时为了确保新的服务逻辑没有问题,会让一小部分特定的用户来使用新的版本(比如客户端的内测版本),而其余的用户使用旧的版本,那么这个在Spring Cloud中该如何来实现呢?

负载均衡组件使用:Spring Cloud LoadBalancer

2、需求

需求

3、实现思路

Spring Cloud Loadbalancer
通过翻阅Spring Cloud的官方文档,我们知道,大概可以通过2种方式来达到我们的目的。

  1. 实现 ReactiveLoadBalancer接口,重写负载均衡算法。
  2. 实现ServiceInstanceListSupplier接口,重写get方法,返回自定义的服务列表

ServiceInstanceListSupplier: 可以实现如下功能,比如我们的 user-service在注册中心上存在5个,此处我可以只返回3个。

4、Spring Cloud中是否有我上方类似需求的例子

查阅Spring Cloud官方文档,发现org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier 类可以实现类似的功能。

那可能有人会说,既然Spring Cloud已经提供了这个功能,为什么你还要重写一个? 此处只是为了一个记录,因为工作中的需求可能各种各样,万一后期有类似的需求,此处记录了,后期知道怎么实现。

5、核心代码实现

5.1 灰度核心代码

5.1.1 灰度服务实例选择器实现

package com.huan.loadbalancer;import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Flux;import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;/*** 自定义 根据服务名 获取服务实例 列表* <p>* 需求: 用户通过请求访问 网关<br />* 1、如果请求头中的 version 值和 下游服务元数据的 version 值一致,则选择该 服务。<br />* 2、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 不存在 version 的值 为 default 则直接报错。<br />* 3、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 存在 version 的值 为 default,则选择该服务。<br />* <p>* 参考: {@link org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier} 实现** @author huan.fu* @date 2023/6/19 - 21:14*/
@Slf4j
public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {/*** 请求头的名字, 通过这个 version 字段和 服务中的元数据来version字段进行比较,* 得到最终的实例数据*/private static final String VERSION_HEADER_NAME = "version";public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {super(delegate);}@Overridepublic Flux<List<ServiceInstance>> get() {return delegate.get();}@Overridepublic Flux<List<ServiceInstance>> get(Request request) {return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext())));}private String getVersion(Object requestContext) {if (requestContext == null) {return null;}String version = null;if (requestContext instanceof RequestDataContext) {version = getVersionFromHeader((RequestDataContext) requestContext);}log.info("获取到需要请求服务[{}]的version:[{}]", getServiceId(), version);return version;}/*** 从请求中获取version*/private String getVersionFromHeader(RequestDataContext context) {if (context.getClientRequest() != null) {HttpHeaders headers = context.getClientRequest().getHeaders();if (headers != null) {return headers.getFirst(VERSION_HEADER_NAME);}}return null;}private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {// 1、获取 请求头中的 version 和 ServiceInstance 中 元数据中 version 一致的服务List<ServiceInstance> selectServiceInstances = instances.stream().filter(instance -> instance.getMetadata().get(VERSION_HEADER_NAME) != null&& Objects.equals(version, instance.getMetadata().get(VERSION_HEADER_NAME))).collect(Collectors.toList());if (!selectServiceInstances.isEmpty()) {log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size());return selectServiceInstances;}// 2、返回 version=default 的实例selectServiceInstances = instances.stream().filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default")).collect(Collectors.toList());log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), "default", selectServiceInstances.size());return selectServiceInstances;}
}

5.1.2 灰度feign请求头传递拦截器

package com.huan.loadbalancer;import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;/*** 将version请求头通过feign传递到下游** @author huan.fu* @date 2023/6/20 - 08:27*/
@Component
@Slf4j
public class VersionRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {String version = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("version");log.info("feign 中传递的 version 请求头的值为:[{}]", version);requestTemplate.header("version", version);}
}

注意: 此处全局配置了,配置了一个feign的全局拦截器,进行请求头version的传递。

5.1.3 灰度服务实例选择器配置

package com.huan.loadbalancer;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 此处选择全局配置** @author huan.fu* @date 2023/6/19 - 22:16*/
@Configuration
@Slf4j
@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)
public class VersionServiceInstanceListSupplierConfiguration {@Bean@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV1(ConfigurableApplicationContext context) {log.error("===========> versionServiceInstanceListSupplierV1");ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);return new VersionServiceInstanceListSupplier(delegate);}@Bean@ConditionalOnClass(name = "org.springframework.web.reactive.DispatcherHandler")public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV2(ConfigurableApplicationContext context) {log.error("===========> versionServiceInstanceListSupplierV2");ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder().withDiscoveryClient().withCaching().build(context);return new VersionServiceInstanceListSupplier(delegate);}
}

此处偷懒全局配置了
@Configuration @Slf4j @LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)

5.2 网关核心代码

5.2.1 网关配置文件

spring:application:name: lobalancer-gateway-8001cloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848group: DEFAULT_GROUPconfig:server-addr: localhost:8848gateway:discovery:locator:enabled: trueserver:port: 8001logging:level:root: info

5.3 服务提供者核心代码

5.3.1 向外提供一个方法

package com.huan.loadbalancer.controller;import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** 提供者控制器** @author huan.fu* @date 2023/3/6 - 21:58*/
@RestController
public class ProviderController {@Resourceprivate NacosDiscoveryProperties nacosDiscoveryProperties;/*** 获取服务信息** @return ip:port*/@GetMapping("serverInfo")public String serverInfo() {return nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort();}
}

5.3.2 提供者端口8005配置信息

spring:application:name: providercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848# 配置元数据metadata:version: v1config:server-addr: localhost:8848
server:port: 8005

注意 metadata中version的值

5.3.2 提供者端口8006配置信息

spring:application:name: providercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848# 配置元数据metadata:version: v1config:server-addr: localhost:8848
server:port: 8006

注意 metadata中version的值

5.3.3 提供者端口8007配置信息

spring:application:name: providercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848# 配置元数据metadata:version: defaultconfig:server-addr: localhost:8848
server:port: 8007

注意 metadata中version的值

5.4 服务消费者代码

5.4.1 通过 feign 调用提供者方法

/*** @author huan.fu* @date 2023/6/19 - 22:21*/
@FeignClient(value = "provider")
public interface FeignProvider {/*** 获取服务信息** @return ip:port*/@GetMapping("serverInfo")String fetchServerInfo();}

5.4.2 向外提供一个方法

package com.huan.loadbalancer.controller;import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.huan.loadbalancer.feign.FeignProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;/*** 消费者控制器** @author huan.fu* @date 2023/6/19 - 22:21*/
@RestController
public class ConsumerController {@Resourceprivate FeignProvider feignProvider;@Resourceprivate NacosDiscoveryProperties nacosDiscoveryProperties;@GetMapping("fetchProviderServerInfo")public Map<String, String> fetchProviderServerInfo() {Map<String, String> ret = new HashMap<>(4);ret.put("consumer信息", nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort());ret.put("provider信息", feignProvider.fetchServerInfo());return ret;}
}

消费者端口 8002 配置信息

spring:application:name: consumercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848register-enabled: trueservice: nacos-feign-consumergroup: DEFAULT_GROUPmetadata:version: v1config:server-addr: localhost:8848
server:port: 8002

注意 metadata中version的值

消费者端口 8003 配置信息

spring:application:name: consumercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848register-enabled: trueservice: nacos-feign-consumergroup: DEFAULT_GROUPmetadata:version: v2config:server-addr: localhost:8848
server:port: 8003

注意 metadata中version的值

消费者端口 8004 配置信息

spring:application:name: consumercloud:nacos:discovery:# 配置 nacos 的服务地址server-addr: localhost:8848register-enabled: trueservice: nacos-feign-consumergroup: DEFAULT_GROUPmetadata:version: defaultconfig:server-addr: localhost:8848
server:port: 8003

注意 metadata中version的值

6、测试

代码与图的对应关系

6.1 请求头中携带 version=v1

从上图中可以看到,当version=v1时,服务消费者为consumer-8002, 提供者为provider-8005provider-8006

➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
➜  ~

请求头中携带 version=v1

可以看到,消费者返回的端口是8002,提供者返回的端口是8005|8006是符合预期的。

6.2 不传递version

从上图中可以看到,当不携带时,服务消费者为consumer-8004, 提供者为provider-8007

➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
➜  ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
➜  ~

可以看到,消费者返回的端口是8004,提供者返回的端口是8007是符合预期的。

7、完整代码

https://gitee.com/huan1993/spring-cloud-alibaba-parent/tree/master/loadbalancer-supply-service-instance

8、参考文档

1、https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer


http://www.ppmy.cn/news/496474.html

相关文章

通过CMA、CNAS双认证的第三方软件测试机构安利

1、什么是CMA认证? CMA是中国计量认证的简称&#xff0c;由省级以上人民政府计量行政部门对检测机构的检测能力及可靠性进行的一种全面的认证及评价&#xff0c;认证对象是所有对社会出具公正数据的产品质量监督检验机构及其它各类实验室&#xff0c;是需要强制性认证的资质。…

笔记app有好用的吗?

通常情况下&#xff0c;每个人的脑容量都是有限的&#xff0c;所以为了保障工作经验、数据资料的完整性&#xff0c;很多人选择用文字的方式来代替大脑记忆。而且&#xff0c;还会使用一些智能化的笔记应用进行记录&#xff0c;以此来提升记事的效率。那么&#xff0c;当下手机…

如何评判软件测试培训机构的好坏?

想要学习软件测试技术&#xff0c;那么找一家软件测试培训机构无疑是最好的选择&#xff0c;那么如今市面上的软件测试培训机构比较多&#xff0c;如何评判软件测试培训机构的好坏呢?来看看下面的详细介绍。 如何评判软件测试培训机构的好坏?现在国内的软件行业的市场日益增大…

孩子作业教不来?用下面四种软件不用烦

现在有很多家长是不是觉得小孩子的作业越来越难&#xff0c;越来越不会教孩子了&#xff1f;连个小学题我们家长都要思考很久&#xff0c;这道题其实不是我们家长不会&#xff0c;而是把这道题复杂化了。现在手机软件有很多解答题目的APP&#xff0c;下面四种都是非常实用的软件…

平板手写笔有必要买吗?开学季便宜又好用电容笔推荐

随着平板电脑在学校和工作场所的普及&#xff0c;许多人都想要一款优秀的电容笔。尽管原装的苹果电容笔性能很好&#xff0c;但是由于的价格很高&#xff0c;如果花1000元去购买用于一些简单的学习记录&#xff0c;那就是大材小用了。那么&#xff0c;究竟哪一个品牌的平替电容…

大学生入学该准备哪些东西?Ipad好用电容笔测评

马上即将进入开学季&#xff0c;所有的准大学生都准备东西好了吗&#xff1f;每一种商品都有其独特的内涵&#xff0c;每一种商品都有其存在的意义&#xff0c;每一种商品都有其独特的功能。ipad的兴起&#xff0c;让ipad的用户数量大增&#xff0c;想要更好的驾驭ipad&#xf…

开学季好物怎么选,学生党必备的几款好物分享

​如今电子产品琳琅满目&#xff0c;有时不知道如何选择时我们可以根据自己的需求进行判断。而且对于不同用户来说&#xff0c;他们对数码产品所需功能和需求也是不一样的&#xff0c;有的人需要游戏性能强、有的人需要高性能、有的人则需要低功耗、有的人需要便携性等等。下面…

手把手教你写保研简历|计算机保研|保研夏令营文书写作|简历模板

手把手教你写保研简历 2022年保研夏令营在即&#xff0c;很多同学都开始制作保研需要的文书&#xff0c;比如说简历、个人陈述、研究计划和导师推荐信等… 可别小看文书的制作&#xff0c;有的时候一份好的保研文书能在老师那里给我们增加很多的印象分&#xff01; 本系列将…

在校大学生如何申请软件著作权(超级详细)

文章目录 一、前言二、网上申请步骤&#xff1a;(1)打开中国版权保护中心网站(2)点击网站右上方注册/登录按钮(3)进行网上申请登记软件著作权 三、材料准备&#xff08;1&#xff09;申请表&#xff08;2&#xff09;完整文档一份&#xff08;3&#xff09;合作开发协议书&…

保研院校、导师对比以及其方法论-V1

保研院校、导师对比以及其方法论-V1 蒋晨阳 \text{蒋晨阳} 蒋晨阳 文章目录 保研院校、导师对比以及其方法论-V1根据感兴趣的领域进行对比关于导师研控网导师评价网Web Of Science 学术水平检索Google Scholar 方法百度学术方法 根据感兴趣的领域进行对比 以操作系统为例&…

学校报修管理系统怎么用的?

为了方便广大师生日常设备的报修&#xff0c;精简维修申报流程&#xff0c;提高维修服务质量和效率&#xff0c;分享一款主要针对学校的报修系统&#xff0c;此系统操作简单&#xff0c;功能十分的强大。那么&#xff0c;学校报修管理系统怎么用的? 一、学校报修管理系统使用…

吐血整理各大高校保研官网

同济 同济全学科汇总&#xff1a;https://yz.tongji.edu.cn/info/1010/1871.htm同济软件学院官网&#xff1a;https://sse.tongji.edu.cn同济设创官网&#xff1a;https://tjdi.tongji.edu.cn同济上海国际设计创新学院夏令营通知&#xff1a; https://mp.weixin.qq.com/s/6zGL…

18.Linux切换用户

在 Linux 系统中&#xff0c;可以使用 su 命令来切换用户&#xff0c;具体步骤如下&#xff1a; 打开终端或登录到 Linux 系统中的命令行界面。 使用下面的命令来切换到目标用户。其中 <username> 是目标用户的用户名。 su <username>系统提示输入目标用户的密码…

开学季,这款学校收费管理软件轻松搞定收费、缴费问题!

伴随着开学季&#xff0c;一年一度缴纳学费的时候又到了。每到开学季&#xff0c;学校和家长就开始犯愁。 学校&#xff1a;收费是个大工程&#xff0c;头疼&#xff01; 家长&#xff1a;排队缴费时间长&#xff0c;头疼&#xff01; 传统的收费模式&#xff0c;让财务老师…

在校大学生如何申请软著,手把手教会你(内有免费模板)

目录 一.前言 二.以学校为单位全流程申请&#xff08;以我的学校为例&#xff09; 1.问问导员谁负责管软著申请这块的&#xff0c;联系他&#xff0c;问需要什么。 2.为了防止学生买软著转头申请 3.按以下要求准备材料 4.没问题就发给老师&#xff0c;一般要破费一下 5.…

发现孩子做作业用计算机,孩子写作业要用手机完成?家庭作业电子化,到底靠谱不靠谱...

原标题&#xff1a;孩子写作业要用手机完成&#xff1f;家庭作业电子化&#xff0c;到底靠谱不靠谱 “妈妈&#xff0c;拿手机给我&#xff0c;我要开始做作业啦&#xff01;”最近&#xff0c;有不少家长吐槽&#xff0c;说从开学到现在&#xff0c;孩子几乎每天都有手机上的作…

北大信科直博保研经历

零、前言 经过一学期的准备&#xff0c;也是顺利的拿到了心仪的offer&#xff0c;北大信科直博。 由于我在准备的过程当中&#xff0c;看了许多学长学姐留下来的经验&#xff0c;所以我也写一下我的经验以及教训来帮助后来的学弟学妹们。这里只说北大信科的夏令营经历。文中涉…

家校协同小程序实战教程

目录 前言1 设计数据源2 创建模型应用3 开发小程序4 学生注册5 学生修改总结前言 现在学校已经有了初步的信息化意识,老师日常在和家长互动时,通过微信群发布各类信息,通过在线采集表来收集各类信息。 采集表收集信息有如下弊端 1、家长及学生的个人隐私信息在采集表中暴露…

这些年,我“端”掉的软件测试培训机构

大家好&#xff0c;我是小谭。 作为一名技术博主&#xff0c;我有多个软件测试交流群&#xff08;今天⑧群新开&#xff0c;想加群的朋友们call我&#xff09;。 我不是培训机构&#xff0c;也不是营销号&#xff0c;没有团队&#xff0c;只我一人&#xff0c;其实拉起这么多…

全豹校园帮2.0版本上线

生活节奏越来越快的当下&#xff0c;越来越多消费者都习惯了同城配送、外卖订餐等生活服务&#xff0c;以节省自己时间的商业形式。与其说同城配送是在“共享劳动力”&#xff0c;不如说是“共享时间”。 而在各个年龄层的用户中&#xff0c;大学生用户无疑是使用配送跑腿、外…