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. 垃圾回收之CMS、G1、ZGC对比

    ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括: 停顿时间不超过10ms: 停顿时间不会随着堆的大小,或者活跃对象的大小而增加: ...

  2. Perceptron, Support Vector Machine and Dual Optimization Problem (3)

    Support Vector Machines Perceptron and Linear Separability 假设存在一个 linear decision boundary,它可以完美地对 t ...

  3. 白嫖一个月的ES,完成了与MySQL的联动

    前言 <腾讯云 x Elasticsearch三周年>活动来了.文章写之前的思路是:在腾讯云服务器使用docker搭建ES.但是理想很丰满,显示很骨感,在操作过程中一波三折,最后还是含着泪 ...

  4. 一个 OpenTiny,Vue2 Vue3 都支持!

    大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 今天给大家介绍如何同时在 Vue2 和 Vue3 项目中使 ...

  5. 【Spring专题】「技术原理」从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理

    ExceptionHandler的作用 ExceptionHandler是Spring框架提供的一个注解,用于处理应用程序中的异常.当应用程序中发生异常时,ExceptionHandler将优先地拦截 ...

  6. day104:MoFang:个人中心页面&flask-admin&基于faker生成仿真数据

    目录 BUG:登陆跳转并解决页面卡顿现象 1.前端显示个人中心页面 2.flask-Admin构建和配置后台运营站点管理用户信息 3.基于Faker生成仿真测试数据 BUG:登陆跳转并解决页面卡顿现象 ...

  7. Vite-Admin后台管理系统|vite4+vue3+pinia前端后台框架实例

    基于vite4.x+vue3+pinia前端后台管理系统解决方案ViteAdmin. 前段时间分享了一篇vue3自研pc端UI组件库VEPlus.这次带来最新开发的基于vite4+vue3+pinia ...

  8. .NET敏捷开发框架-RDIFramework.NET V5.1发布(跨平台)

    RDIFramework.NET,基于全新.NET Framework与.NET Core的快速信息化系统敏捷开发.整合框架,给用户和开发者最佳的.Net框架部署方案.为企业快速构建跨平台.企业级的应 ...

  9. 自定义Mybatis-plus插件(限制最大查询数量)

    自定义Mybatis-plus插件(限制最大查询数量) 需求背景 ​ 一次查询如果结果返回太多(1万或更多),往往会导致系统性能下降,有时更会内存不足,影响系统稳定性,故需要做限制. 解决思路 1.经 ...

  10. Swift CustomStringConvertible 协议的使用

    目录 一.前言 二.使用场景 1. 整型类型的枚举使用 2. Class类型的使用 一.前言 先看一下Swift标准库中对CustomStringConvertible协议的定义 public pro ...