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. Windows Powershell无法切换anaconda的问题

    前言 近期做大创发现power shell启动以后activate环境之后没有反应,遂进行如下操作 启用默认配置 使用管理员模式打开Powershell 输入conda init powershell ...

  2. 重磅!Apache Hudi联合传智教育推出免费中文视频教程

    基础介绍 Apache Hudi(简称:Hudi)使得您能在hadoop兼容的存储之上存储大量数据,同时它还提供两种原语,使得除了经典的批处理之外,还可以在数据湖上进行流处理.这两种原语分别是: Up ...

  3. vue中实现video的动态src绑定

    Vue中实现video的动态src 试了网上的$refs方法发现并没有用 解决方案: 通过require方法  <div>     <video :src='url' @click= ...

  4. 最新版本 Stable Diffusion 开源 AI 绘画工具之图生图进阶篇

    目录 图生图基本参数 图生图(img2img) 涂鸦绘制(Sketch) 局部绘制(Inpaint) 涂鸦蒙版(Inpaint sketch) 上传蒙版(Inpaint upload) 图生图基本参数 ...

  5. layUI之DataTable组件V1.0(父子表管理传值/数据表格与select&laydate结合等)

    layUI之DataTable数据表格组件V1.0 目录 layUI之DataTable数据表格组件V1.0 概述 一.下载与引用 二.组件功能介绍 三.父表格渲染 1. HTML中声明空table一 ...

  6. Kubesphere中DevOps流水线无法部署/部署失败

    摘要 总算能让devops运行以后,流水线却卡在了deploy这一步.碰到了两个比较大的问题,一个是无法使用k8sp自带的kubeconfig认证去部署:一个是部署好了以后但是没有办法解析镜像名. 版 ...

  7. Sentinel为什么这么强,我扒了扒背后的实现原理

    大家好,我是三友~~ 最近我在整理代码仓库的时候突然发现了被尘封了接近两年之久的Sentinel源码库 两年前我出于好奇心扒了一下Sentinel的源码,但是由于Sentinel本身源码并不复杂,在简 ...

  8. JS中的Map、Set、WeakMap和WeakSet

    在JavaScript中,Map.Set.WeakMap和WeakSet是四个不同的数据结构,它们都有不同的特点和用途: 1. Map :Map是一种键值对的集合,其中的键和值可以是任意类型的.与对象 ...

  9. Wolai 使用教程:嵌入小组件库,打造精美、强大的知识库主页

    Wolai /我来云笔记在 2022.7.11 日的更新中,支持嵌入包括 NotionPet.芦笋.Replit 等在内的第三方应用.感谢 Wolai 云笔记官方对于 NotionPet 的支持. 趁 ...

  10. 2020-10-23:go中channel的创建流程是什么?

    福哥答案2020-10-23:1.元素大小是否小于2的16次方,否则throw.2.对齐检查,否则throw.3.元素大小和容量的乘积不能超出范围,否则panic.4.生成*hchan,设置buf.4 ...