1、背景(灰度部署)

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

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

2、需求

3、实现思路



通过翻阅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);
} @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())));
} 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 { @Override
public 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-8001
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
group: DEFAULT_GROUP
config:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true server:
port: 8001 logging:
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 { @Resource
private NacosDiscoveryProperties nacosDiscoveryProperties; /**
* 获取服务信息
*
* @return ip:port
*/
@GetMapping("serverInfo")
public String serverInfo() {
return nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort();
}
}

5.3.2 提供者端口8005配置信息

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

注意 metadata中version的值

5.3.2 提供者端口8006配置信息

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

注意 metadata中version的值

5.3.3 提供者端口8007配置信息

spring:
application:
name: provider
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
# 配置元数据
metadata:
version: default
config:
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 { @Resource
private FeignProvider feignProvider;
@Resource
private 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: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: v1
config:
server-addr: localhost:8848
server:
port: 8002

注意 metadata中version的值

消费者端口 8003 配置信息

spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: v2
config:
server-addr: localhost:8848
server:
port: 8003

注意 metadata中version的值

消费者端口 8004 配置信息

spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: default
config:
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"}%
➜ ~

可以看到,消费者返回的端口是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

Spring Cloud灰度部署的更多相关文章

  1. Spring Cloud灰度发布之Nepxion Discovery

    <蓝绿部署.红黑部署.AB测试.灰度发布.金丝雀发布.滚动发布的概念与区别> 最近公司项目在做架构升级,升级为 Spring Cloud,我们希望能够做到服务的灰度发布,根据访问量逐渐切换 ...

  2. 【架构】Kubernetes和Spring Cloud哪个部署微服务更好?

    Spring Cloud 和Kubernetes都自称自己是部署和运行微服务的最好环境,但是它们在本质上和解决不同问题上是有很大差异的.在本文中,我们将看到每个平台如何帮助交付基于微服务的架构(MSA ...

  3. 微服务开发平台 Spring Cloud Blade 部署实践

    本文介绍使用 Rainbond 快速部署 Spring Cloud Blade 微服务平台.Spring Cloud Blade 是一个由商业级项目升级优化而来的微服务架构,采用Spring Boot ...

  4. 一句话概括下spring框架及spring cloud框架主要组件

    作为java的屌丝,基本上跟上spring屌丝的步伐,也就跟上了主流技术.spring 顶级项目:Spring IO platform:用于系统部署,是可集成的,构建现代化应用的版本平台,具体来说当你 ...

  5. Spring顶级项目以及Spring cloud组件

    作为java的屌丝,基本上跟上spring屌丝的步伐,也就跟上了主流技术. spring 顶级项目: Spring IO platform:用于系统部署,是可集成的,构建现代化应用的版本平台,具体来说 ...

  6. 【译文】用Spring Cloud和Docker搭建微服务平台

    by Kenny Bastani Sunday, July 12, 2015 转自:http://www.kennybastani.com/2015/07/spring-cloud-docker-mi ...

  7. Spring cloud定义学习

    今天讲到的最重要的内容: Spring cloud是什么? Spring cloud项目 spring cloud版本     什么事springcloud? spring cloud 为开发人员提供 ...

  8. 简述 Spring Cloud 是什么2

    一.概念定义       Spring Cloud是一个微服务框架,相比Dubbo等RPC框架, Spring Cloud提供的全套的分布式系统解决方案. Spring Cloud对微服务基础框架Ne ...

  9. spring 和spring cloud 组成

    spring 顶级项目:Spring IO platform:用于系统部署,是可集成的,构建现代化应用的版本平台,具体来说当你使用maven dependency引入spring jar包时它就在工作 ...

  10. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(一):Kitty 系统介绍

    在线演示 演示地址:http://139.196.87.48:9002/kitty 用户名:admin 密码:admin 温馨提示: 有在演示环境删除数据的童鞋们,如果可以的话,麻烦动动小指,右键头像 ...

随机推荐

  1. Semantic Kernel 知多少 | 开启面向AI编程新篇章

    引言 在ChatGPT 火热的当下, 即使没有上手亲自体验,想必也对ChatGPT的强大略有耳闻.当一些人在对ChatGPT犹犹豫豫之时,一些敏锐的企业主和开发者们已经急不可耐的开展基于ChatGPT ...

  2. kali linux 基本渗透测试流程

    渗透测试流程 1. 信息收集阶段 网络拓扑结构分析 使用nmap扫描目标网络,获取目标主机IP地址和开放端口信息 使用whois查询目标域名的注册信息和DNS服务器信息 使用nslookup查询目标域 ...

  3. SprintBoot2报错汇总

    报错1:SpringBoot找不到bean Unable to start ServletWebServerApplicationContext due to missing ServletWebSe ...

  4. day03-Redis的客户端

    Redis的Java客户端 在Redis官网中提供了各种语言的客户端,地址:Get started using Redis clients | Redis Redis的Java客户端: 1.Jedis ...

  5. 零样本文本分类应用:基于UTC的医疗意图多分类,打通数据标注-模型训练-模型调优-预测部署全流程。

    零样本文本分类应用:基于UTC的医疗意图多分类,打通数据标注-模型训练-模型调优-预测部署全流程. 1.通用文本分类技术UTC介绍 本项目提供基于通用文本分类 UTC(Universal Text C ...

  6. Gradio入门到进阶全网最详细教程[一]:快速搭建AI算法可视化部署演示(侧重项目搭建和案例分享)

    Gradio入门到进阶全网最详细教程[一]:快速搭建AI算法可视化部署演示(侧重项目搭建和案例分享) 常用的两款AI可视化交互应用比较: Gradio Gradio的优势在于易用性,代码结构相比Str ...

  7. Typora用法:

    Typora用法: 一:标题 模板: #+空格+标题名+回车 一级标题 二级标题 三级标题 四级标题 五级标题 二:字体 加粗 斜体 斜体加粗 删除线(esc键下面的那个键) 上标:我是上标 下表:我 ...

  8. HTML中meta标签的那些属性

    <meta> 标签是 HTML 中用于描述网页元信息的元素.它位于 <head> 部分,不会显示在页面内容中,但对于浏览器.搜索引擎等具有重要作用.主要作用有:定义文档的字符编 ...

  9. 解决PaddlePaddle飞桨在迁移学习使用预训练模型时更改num_classes参数出现警告

    当我们使用 PaddlePaddle 进行迁移学习的时候,直接导入模型虽然是可以的,但是总是会有个警告 如直接用官方的 resnet101 并加载预训练模型的话 model = paddle.visi ...

  10. 在docker容器里,ffmpeg给视频文件内嵌字幕文件,不生效,如何解决?

    用ffmpeg命令,发现执行成功,但视频文件就是没有字幕.看不出问题出现在什么地方.后来直接用ffmpeg添加水印命令测试,发现是缺少字体文件,如下图所示: 报Fontconfig error: Ca ...