Spring Cloud灰度部署

news2024/11/20 3:35:43

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);
    }

    @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"}%
➜  ~

请求头中携带 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.coloradmin.cn/o/669620.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第五届双态IT北京用户大会回顾 | 基于运维数据治理的数智化转型

专题演讲人&#xff1a;擎创科技CTO 葛晓波 文末附有本场专题演讲视频 ●前言 各行业的云原生发展程度各有不同&#xff0c;并不是所有业务应用都适合云原生的形态&#xff0c;如若过度追求云原生化反而会使得企业运维压力骤增&#xff0c;运维成本激增。 从数字化转型的角度…

软考:软件工程:软件定义,特点,软件生命周期,软件危机,软件开发模型

软考&#xff1a;软件工程: 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &#xff08;1&#…

C++——vector容器模拟实现

目录 1. 基本成员函数 2. 默认成员函数 2.1 构造函数 2.2 析构函数 2.3 拷贝构造函数 2.4 赋值运算符重载函数 3. 容器访问相关函数 3.1 operator[ ]运算符重载 3.2 迭代器 3.3 范围for 4. vector空间增长问题 4.1 vector 容量和大小 4.2 vector扩容 4.3 重新定义…

均匀B样条采样从LiDAR数据中快速且鲁棒地估计地平面

文章&#xff1a;Fast and Robust Ground Surface Estimation from LiDAR Measurements using Uniform B-Splines 作者&#xff1a;Sascha Wirges, Kevin Rsch, Frank Bieder and Christoph Stiller 编辑&#xff1a;点云PCL 代码&#xff1a; https://github.com/KIT-MRT/poin…

全志V3S嵌入式驱动开发(编译器升级到7.5)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 看过我们文章的朋友都知道&#xff0c;前面为了做v3s的驱动&#xff0c;对linux kernel进行了两次升级。第一次升级是从4.10.y升级到4.14.y&#x…

【Python】open打开文件出现的错误解决

一、Python中关于打开open打开文件出现的错误解决 &#xff08;第一种&#xff09;UnicodeDecodeError: ‘utf-8’.......... &#xff08;第二种&#xff09;UnicodeDecodeError: ‘gbk’......... 二、问题解决 两种解决方式针对不同错误&#xff0c;实际应用中可以都试试…

PCB设计实验|第五周|LED显示电路PCB库设计|3月27日

目录 实验四 LED显示电路PCB库设计 一、实验原理 二、实验环境 三、实验结果 四、实验总结 实验四 LED显示电路PCB库设计 一、实验原理 LED(Light- Emitting-Diode中文意思为发光二极管)是一种能够将电能转化为可见光的半导体&#xff0c;它改变了白炽灯钨丝发光与节能…

裁剪图片软件有哪些?这些图片裁剪工具很好用

有哪些好用的图片裁剪软件呢&#xff1f;有时候&#xff0c;将一张大图缩小到更小的尺寸可以改善图片的质量&#xff0c;因为它可以减少像素和噪点。这对于那些需要在网上展示高质量图片的人来说尤其重要。裁剪后的图片可能更清晰、更锐利&#xff0c;并且更适合在各种设备上观…

Alex-Net 与 VGG-16

Alex-Net 由加拿大多伦多大学的 Alex Krizhevsky、Ilya Sutskever(G. E. Hinton 的两位博士生)和 Geoffrey E. Hinton 提出&#xff0c;网络名“Alex-Net”即 取自第一作者名。 下图所示是 Alex-Net 的网络结构&#xff0c;共含五层卷积层和三层全连接层。其中&#xff0c;Ale…

03.SELF-INSTRUCT+Alpaca

文章目录 前言泛读储备知识提示学习提示工程Promt Engineering答案工程 背景介绍研究SELF-INSTRUCT的动机研究意义&贡献 精读Overview种子任务步骤1&#xff1a;定义指令数据步骤2&#xff1a;自动指令数据生成步骤2.1指令生成步骤2.2指令分类步骤2.3实例生成步骤2.4筛选和…

19.组件之间传递数据

不同组件传递数据的时候&#xff0c;最好不要直接传递复杂数据类型(比如对象&#xff0c;数组) 前端需要处理的数据层级一般不会很多&#xff0c;需要在多处使用的数据一般会被放到数据库中 目录 1 组件的关系 2 父向子传递数据-props 3 子向父传递数据-自定义事件 4 …

分布式任务调度平台 XXL-JOB 实战

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

Scrum敏捷估算

无论是团队研发一款产品或者开发某一个项目&#xff0c;我们都需要回答“我们大概什么时间能够完成&#xff1f;”&#xff0c; 或者到某一个时间点&#xff0c;我们能够做到什么程度&#xff0c; 因此和传统的开发模式一样&#xff0c;我们在工作开始之前需要对我们需要做的事…

Linux Vim基本操作(文件的打开和编辑)完全攻略(有图有真相)

首先学习如何使用 Vim 打开文件。 Vim 打开文件 使用 Vim 打开文件很简单&#xff0c;例如在命令行模式下打开一个自己编写的文件 /test/vi.test&#xff0c;打开方法如下&#xff1a; [rootitxdl ~]# vim /test/vi.test 刚打开文件时 Vim 处于命令模式&#xff0c;此时文件…

CTFshow-pwn入门-前置基础pwn26-pwn28

什么是ASLR 大多数的攻击都基于这样一个前提&#xff0c;即攻击者知道程序的内存布局&#xff0c;需要提前知道shellcode或者其他一些数据的位置。因此&#xff0c;引入内存布局的随机化能够有效增加漏洞利用的难度&#xff0c;其中一种技术就是ASLR&#xff08;Address Space…

无线wifi视频传输方案|基于qca9531方案SKW99的无线视频流云端推送方案

为满足物联网智慧校园&#xff0c;智能家居&#xff0c;智慧工厂&#xff0c;智能交通、智慧博物馆、培训机构等不同行业实时直播的需求。本篇以集成200万高清摄像头功能的高通方案qca9531 wifi模块SKW99为为例&#xff0c;简单介绍基于WiFi技术的无线视频流云端推送方案。 1、…

上位机与两台PLC之间无线PPI通信

在实际系统中&#xff0c;人机界面与PLC通常不在一起&#xff0c;中心计算机一般放置在控制室&#xff0c;而PLC安装在现场车间&#xff0c;二者之间距离往往从几十米到几千米。如果布线的话&#xff0c;需要挖沟施工&#xff0c;比较麻烦&#xff0c;这种情况下比较适合采用无…

0基础学习VR全景平台篇第47篇:底部菜单-场景/分组复制功能

大家好&#xff0c;欢迎观看蛙色VR官方系列——后台使用课程&#xff01; 本期为大家带来蛙色VR平台&#xff0c;底部菜单—场景/分组复制功能操作。 功能位置示意 一、本功能将用在哪里&#xff1f; 平台用户在编辑作品时可以使用本功能将作品中的某一分组或者某一场景进行复…

岩土工程监测案例:完整链条的振弦传感器、采集仪和在线监测系统

岩土工程监测案例&#xff1a;完整链条的振弦传感器、采集仪和在线监测系统 在岩土工程监测中&#xff0c;振弦传感器被广泛应用于测量土体或岩体的振动情况&#xff0c;以了解地震或其他振动事件对结构物或地基的影响。振弦传感器具有高精度、快速响应、易于安装和低成本等优…

django校园宿舍管理系统-计算机毕设 附源码84831

django校园宿舍管理系统 摘 要 本论文主要论述了如何使用Django开发一个校园宿舍管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述校园宿舍管理系统的当…