Spring Cloud Alibaba-全链路灰度设计

news2024/11/16 3:25:43

文章目录

    • 灰度发布概念
    • 灰度发布架构
    • Spring Cloud Alibaba技术架构下的灰度发布实现
      • 基础设计
        • HttpHeader设计
      • Spring Cloud Gateway改造
        • Spring Cloud Gateway实现灰度发布过滤器
      • 自定义Loadbalancer
        • 微服务注册元信息修改
        • 自定义LoadBalancer使用
    • 测试
          • 代码放到了github上[地址]()

灰度发布概念

– 摘自百度百科

​ 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

​ 对比灰度发布,还有蓝绿发布,滚动发布,笔者在工作中用到最多的是滚动发布,滚动发布也并不能平滑的将一部分指定的流量切到新系统上,只能做到切部分流量这部分流量是无法指定的也是随机的,所以基于此笔者就着手做全链路灰度发布相关的研究。蓝绿发布,滚动发布的具体概念自行Google吧。

灰度发布架构

​ 这里我画了一个图,简单描述一下灰度发布,这里有微服务A集群和微服务B集群,每个集群中蓝颜色的是生产实例,绿色就代表发布的灰度版本实例;可以看到网络流量是通过微服务网关为入口流入微服务集群,所以这里微服务网关就负责过滤流量管理灰度规则等,流量经过微服务网关后,可以看到灰度流量就指向了灰度版本,微服务A通过RPC调用微服务B时也通过loadbalancer也将灰度流量发到了微服务B的灰度实例上,这里图中实际缺少了一个点,就是流量由微服务网关和微服务A集群中间也是有个loadbalancer的。

在这里插入图片描述

​ 通过上面的图和对于灰度发布架构的描述,我这里总结一下实现微服务全链路灰度发布的核心:

  1. 微服务过滤流量并管理灰度发布规则;
  2. loadbalancer负责进行灰度流量的重新路由;

Spring Cloud Alibaba技术架构下的灰度发布实现

​ 实现灰度发布的技术方案有很多,比如Nginx + Lua脚本方式,Nginx即可以作为微服务网关又可以做负载均衡;再比如Service Mesh技术Istio + Envoy,Istio作为控制平台管理下发灰度规则,Envoy作为网络调用组件,如果是Service Mesh技术架构可以完全采用这种架构。如果是传统的微服务架构就有可能需要自己研发一套灰度发布的组件,所以通过上面对灰度发布架构的研究,我们大致知道了如何实现灰度发布系统,我这里就基于Spring Cloud Alibaba传统微服务架构实现全链路灰度发布功能。

基础设计

实现灰度发布需要终端应用(客户端)和服务端做一些约定,这个约定就代表着是否是灰度发布的客户端的网络调用,当然如果不做约定也是可以实现灰度功能的,这就需要服务端的组件对流量做更细致的过滤,比如从网络调用的报文中过滤出灰度发布应用的网络调用,这对服务端来说显然更加麻烦,也不利于维护,所以我这里采用客户端和服务端约定的方式来设计。

HttpHeader设计

​ 客户端和服务端在Http请求头中约定一个固定字段来标识,此标识可以代表“是否走灰度”,也可以设置成一个“用户ID”,“客户端IP”等,如果是“用户ID”,“客户端IP”那就在网关层有个配置,网关匹配到对应的参数就走灰度。

​ HttpHeader增加gray字段作为灰度标记

{
  "gray":"123"
}

Spring Cloud Gateway改造

​ Spring Cloud Gateway在架构中是微服务网关,在灰度发布的作用就是管理灰度发布规则,设置灰度标记到HttpHeader并且传递下去。管理灰度发布规则需要一个配置,我这里选择放到配置文件当中,先实现一个Spring的自定义配置绑定,代码如下:

@Configuration
@RefreshScope
@ConfigurationProperties("spring.cloud.gateway.gray")
@Data
public class GrayProperties {

    /**
     * 灰度开关
     */
    private Boolean enabled;

    /**
     * 灰度匹配内容
     */
    private List<String> matches = new ArrayList<>();
}

对应的在Spring Cloud Gateway的application.yml中的配置示例如下:

spring:
  cloud:
    gateway:
      gray:
        enabled: true
        matches: 
          - 123
          - 456
          - 10.1.1.10

解释下配置文件,spring.cloud.gateway.gray.enabled控制开启关闭灰度发布,spring.cloud.gateway.gray.matches配置的是灰度发布规则匹配,该值是一个list,也就是说只要匹配到HttpHeaders中的gray的值就走灰度发布逻辑。

Spring Cloud Gateway实现灰度发布过滤器

这个过滤器是灰度发布流量过滤的一个核心,大致逻辑是通过处理HttpHeaders中的gray值决定是够要走灰度发布,如果走灰度发布将HttpHeaders中的gray值设置为true即可,这里也可以和客户端约定两个字段,一个是匹配规则,一个灰度控制,我这里就把它放在一个字段中了。

spring.cloud.gateway.gray.enabled为false是不会走到这个过滤器,还有一点要注意,必须要实现Ordered接口,并且设置其顺序为Ordered.HIGHEST_PRECEDENCE,因为灰度发布过滤这属于一个最高等级的过滤器,要先执行。

​ 为了保证线程隔离,通过GrayRequestContextHolder存取灰度标记。

@Component
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.gray.enabled", havingValue = "true")
@AllArgsConstructor
public class GrayscalePublishFilter implements GlobalFilter, Ordered {

    private final GrayProperties grayProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            GrayRequestContextHolder.setGrayTag(false);
            if (grayProperties.getEnabled()) {
                var headers = exchange.getRequest().getHeaders();
                if (headers.containsKey("gray")) {
                    List<String> grayValues = headers.get("gray");
                    if (!Objects.isNull(grayValues) && grayValues.size() > 0) {
                        // 灰度标记为true,直接走灰度
                        String grayValue = grayValues.get(0);
                        // 配置中的值匹配到header中的灰度值,走灰度(可是用户ID,IP,APP版本号等等,只要匹配到就走灰度)
                        if (grayProperties.getMatches().stream().anyMatch(grayValue::equals)) {
                            GrayRequestContextHolder.setGrayTag(true);
                        }
                    }
                }
                var newRequest = exchange.getRequest().mutate()
                        .header("gray", GrayRequestContextHolder.getGrayTag().toString())
                        .build();
                var newExchange = exchange.mutate()
                        .request(newRequest)
                        .build();
                return chain.filter(newExchange);
            }
            return chain.filter(exchange);
        } finally {
            GrayRequestContextHolder.remove();
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
public class GrayRequestContextHolder {

    private static final ThreadLocal<Boolean> GARY_TAG = new ThreadLocal<>();

    public static void setGrayTag(final Boolean tag) {
        GARY_TAG.set(tag);
    }

    public static Boolean getGrayTag() {
        return GARY_TAG.get();
    }

    public static void remove() {
        GARY_TAG.remove();
    }
}

自定义Loadbalancer

​ 这里基础的LoadBalancer框架使用的是Spring Cloud LoadBalancer,所以需要引入LoadBalancer,代码如下:

				<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

​ 我这里的是将负载均衡作为一个单独模块开发的,如果有需要灰度发布的微服务引用该模块并且配置自定义Loadbalancer即可,Loadbalancer的核心逻辑是根据HttpHeaders中的灰度发布标记,从服务发现的服务列表中筛选出灰度发布的机器实例,然后再通过loadbalancer算法就行负载均衡返回一个服务实例,RPC调用不用管,我这里使用的是OpenFeign作为RPC框架。

​ Spring Cloud LoadBalancer自定义Loadbalancer实现ReactorServiceInstanceLoadBalancer接口,代码如下:

getInstances方法包含了筛选服务列表逻辑,如果从HttpHeaders中获取到gary字段并且该字段值是true就走灰度发布;至于负载均衡逻辑完全拷贝了spring cloud gatewayRoundRobinLoadBalancer的负载均衡逻辑。

@Slf4j
public class GrayscaleLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    final AtomicInteger position;

    final String serviceId;

    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public GrayscaleLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this(new Random().nextInt(1000), serviceId, serviceInstanceListSupplierProvider);
    }

    public GrayscaleLoadBalancer(int seedPosition, String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.position = new AtomicInteger(seedPosition);
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
                .getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next()
                .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request));
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
                                                              List<ServiceInstance> serviceInstances,
                                                              Request request) {
        Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }
        // 获取ServiceInstance列表

        instances = getInstances(instances, request);
        // Do not move position when there is only 1 instance, especially some suppliers
        // have already filtered instances
        if (instances.size() == 1) {
            return new DefaultResponse(instances.get(0));
        }

        // Ignore the sign bit, this allows pos to loop sequentially from 0 to
        // Integer.MAX_VALUE
        int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;

        ServiceInstance instance = instances.get(pos % instances.size());

        return new DefaultResponse(instance);
    }

    private List<ServiceInstance> getInstances(List<ServiceInstance> instances, Request request) {
        DefaultRequest<RequestDataContext> defaultRequest = Convert
                .convert(new TypeReference<DefaultRequest<RequestDataContext>>() {
                }, request);
        RequestDataContext dataContext = defaultRequest.getContext();
        RequestData requestData = dataContext.getClientRequest();
        HttpHeaders headers = requestData.getHeaders();
        // 获取灰度标记
        String gray = CollectionUtil.get(headers.get("gray"), 0);
        // 灰度标记不为空并且标记为true, 筛选ServiceInstance
        if (StringUtils.isNotBlank(gray) && StringUtils.equals("true", gray)) {
            return instances.stream()
                    .filter(instance -> StringUtils.isNotBlank(instance.getMetadata().get(GrayConstant.HEADER_GRAY_TAG))
                            && gray.equals(instance.getMetadata().get(GrayConstant.HEADER_GRAY_TAG)))
                    .collect(Collectors.toList());
        } else {
            return instances;
        }
    }
}

配置自定义LoadBalancer,代码如下:

@Configuration
public class LoadBalancerGrayAutoConfiguration {

    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.gray.enabled", havingValue = "true", matchIfMissing = true)
    @ConditionalOnBean(LoadBalancerClientFactory.class)
    public ReactorLoadBalancer<ServiceInstance> grayReactorLoadBalancer(Environment environment,
                                                                        LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayscaleLoadBalancer(name, loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
    }
}

微服务注册元信息修改

微服务注册增加灰度服务标记配置,微服务注册到服务注册中心(Nacos)时通过附加元数据的方式来标记该服务是一个灰度发布的微服务

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          gray: true

自定义LoadBalancer使用

通过如下配置开启自定义LoadBalancer。

spring:
  cloud:
    loadbalancer:
      gray:
        enabled: true

代码中配置LoadBalancer,在微服务启动类上通过注解开启使用自定义LoadBalancer。

@SpringBootApplication
@EnableDiscoveryClient
@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
public class GateWayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class, args);
    }
}

测试

三个微服务ruuby-gateway,order-svc,account-svc,调用的关系式通过ruuby-gateway调用order-svc,order-svc内部通过OpenFeign调用

account-svc,现在order-svc,account-svc服务都是灰度版本,测试自定义LoadBalancer效果,下面是服务元数据中的灰度标记

在这里插入图片描述

我们在Postman中设置HttpHeaders的灰度标记gray,设置其值为123,因为我们在网关中配置的matches中有123。

在这里插入图片描述

代码放到了github上地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/671219.html

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

相关文章

STM32F4_CAN详解

目录 1. CAN简介 2. CAN协议的物理层 2.1 CAN协议中的差分信号 3. CAN协议的协议层 3.1 CAN的报文种类及结构 4. STM32的CAN外设 4.1 CAN框图 5. CAN相关结构体 6. CAN相关寄存器 7. 硬件设计 7.1 TJA1050芯片 8. 库函数配置CAN初始化 9. 实验代码 9.1 main.c 9…

SpringBoot问题-----------可以同时处理多少个请求

SpringBoot之可以同时处理多少个请求 SpringBoot的运行方式springBoot中tomcat的位置最小、最多工作线程数最大连接数等待数 SpringBoot的运行方式 SpringBoot项目中内嵌了tomcat 程序的运行就是通过tomcat来实现的&#xff1b;springBoot可以同时处理多少个请求其实就是在说 …

图像生成—使用GANs给出代码示例

文章目录 图像生成简单介绍—使用GANs给出代码示例1. 什么是生成对抗网络&#xff08;GANs&#xff09;2. 准备数据集3. 构建生成器和判别器4. 训练GAN模型5. 生成新图像6. 总结 图像生成简单介绍—使用GANs给出代码示例 图像生成是指使用计算机算法生成图像的过程。这些图像可…

C++ | 多线程使用vector

多线程使用vector 文章目录 多线程使用vector场景描述原因分析解决代码测试不扩容和提前扩容 size 与 capacity 变化欢迎关注公众号【三戒纪元】 场景描述 最近在看代码优化&#xff0c;看到有这样的代码&#xff1a; std::vector<int> valid_indices;void SimbaSegmen…

APP测试面试题快问快答(五)

21. App自动化你用的什么工具&#xff1f; 框架&#xff1a;Appium 编译环境和工具&#xff1a;python3.7和PyCharm 环境&#xff1a;Android sdk 第三方模拟器&#xff1a;夜神、蓝叠等模拟器 定位工具&#xff1a;uiautomatorviewer 实时日志查看&#xff1a;ddms 22.…

Tapdata 重磅更新已就绪!全托管云服务上线,应用场景再扩展

继 5 月举办的 「连接 1 次孤岛&#xff0c;服务 N 个场景」主题产品发布会后&#xff0c;Tapdata Live Data Platform 现已实现功能特性的全面升级&#xff0c;并基于自身产品能力积极探索在应用场景层面的落地实践及无限可能。 在去年 6 月的 Tapdata 2.0 发布会上&#xff…

Threejs实现数字人3D粽子

个人主页&#xff1a; 左本Web3D&#xff0c;更多案例预览请点击》 在线案例 个人简介&#xff1a;专注Web3D使用ThreeJS实现3D效果技巧和学习案例 &#x1f495; &#x1f495;积跬步以至千里&#xff0c;致敬每个爱学习的你。喜欢的话请三连&#xff0c;有问题请私信或者加微…

美国访问学者的父母如何申请探亲签证?

对于美国访问学者的父母来说&#xff0c;申请探亲签证是能够让他们在美国与子女团聚的重要途径。下面是知识人网小编整理的一些关于如何申请探亲签证的基本步骤和要点&#xff0c;希望对您有所帮助。 第一步&#xff1a;了解签证类型 在开始申请探亲签证之前&#xff0c;父母需…

【增值税发票识别 OCR】如何实现自动化发票管理

导言 在现代商业环境中&#xff0c;管理和处理大量的增值税发票数据是一项繁琐而重要的任务。传统的手动处理方法既费时又容易出错&#xff0c;而使用增值税发票识别OCR API可以实现自动化的发票管理&#xff0c;大大减少人工处理的工作量。本文将介绍如何利用增值税发票识别O…

Hadoop --- HDFS介绍

HDFS 全称是Hadoop Distributed File System hadoop分布式&#xff08;cluser&#xff09;文件存储系统。适合一次写入&#xff0c;多次读出的场景。 HDFS不需要单独安装&#xff0c;安装Hadoop的时候带了HDFS系统。 Hadoop安装可以参考&#xff1a; 有基础的&#xff0c;已…

轻松了解OPC:实时数据通信领域的必备神器!

OPC简介 OPC&#xff08;OLE for Process Control&#xff0c;进程控制对象连接&#xff09;是一种在工业自动化领域中被广泛使用的技术&#xff0c;它允许不同厂商的自动化设备之间进行通信和数据交换。 OPC技术最早是由美国的软件公司OPC Foundation推出的&#xff0c;它通…

【jsDelivr】jsDelivr - 一个免费、快速、可靠的为JS和开源项目服务的CDN

文章目录 jsDelivr 简介jsDelivr 工作原理 jsDelivr加速域名如下cdn.jsdelivr.net 2023/06/21 域名解析结果fastly.jsdelivr.net 2023/06/21 域名解析结果gcore.jsdelivr.net 2023/06/21 域名解析结果test1.jsdelivr.net 2023/06/21 域名解析结果 仓库拓展 - 其他CDNnpmESMGitH…

nginx nginx-module-vts 监控模块

nginx nginx-module-vts 监控模块 大纲 nginx-module-vts 安装nginx-module-vts 配置监控字段总结配置参数总结vhost_traffic_status_filter_by_host 使用vhost_traffic_status_filter_by_set_key 使用 nginx-module-vts 安装 nginx-module-vts 可以实现对nginx 各个虚拟主…

接口测试是什么?如何测试?

扫盲内容&#xff1a; 1.什么是接口&#xff1f; 2.接口都有哪些类型&#xff1f; 3.接口的本质是什么&#xff1f; 4.什么是接口测试&#xff1f; 5.问什么要做接口测试&#xff1f; 6.怎样做接口测试&#xff1f; 7.接口测测试点是什么&#xff1f; 8.接口测试都要掌…

在Linux系统实现服务器端和客户端的多线程并发通信

先导知识&#xff1a; 在Linux系统实现服务器端和客户端的套接字通信_小梁今天敲代码了吗的博客-CSDN博客 线程同步&#xff08;一&#xff09;_小梁今天敲代码了吗的博客-CSDN博客 线程同步&#xff08;二&#xff09;_小梁今天敲代码了吗的博客-CSDN博客 线程同步&#x…

通付盾入围《2023年度中国数字安全能力图谱(行业版)》

近日&#xff0c;数世咨询发布《2023年度中国数字安全能力图谱&#xff08;行业版&#xff09;》。通付盾作为以分布式数字身份和大数据决策智能技术为核心的数字化高端软件与服务提供商&#xff0c;凭借在数字安全领域的实力和影响力&#xff0c;入选政府、互联网两大行业细分…

实战react+ts+antd遇见的问题之自定义树形结构

目录 自定义编辑树搜索树形结构搜索算法原理 实时更改数据界面不随之发生变化 自定义编辑树 需求要求在每个节点的后面加上新增&#xff0c;编辑&#xff0c;删除按钮&#xff0c;并且能够点击编辑title的显示变成input输入框&#xff0c;antd的案例中没有这种情况&#xff0c…

逍遥自在学C语言 | 指针函数与函数指针

前言 在C语言中&#xff0c;指针函数和函数指针是强大且常用的工具。它们允许我们以更灵活的方式处理函数和数据&#xff0c;进而扩展程序的功能。 本文将介绍指针函数和函数指针的概念&#xff0c;并讲解一些常见的应用示例。 一、人物简介 第一位闪亮登场&#xff0c;有请…

金士顿U盘无法识别的修复软件,方便好用

一、PD V1.16 先打开“PDx16.exe”这个软件&#xff0c;插入U盘。就会在“DEVICE 1”那里检测到U盘&#xff08;如果没有&#xff0c;就用另外的软件&#xff09;。然后按“全部开始”。当完成好&#xff0c;再重新插入U盘。 二、2090&2090E_V1.6.9_普通版070628 1、插入…

工业机器人运动学与Matlab正逆解算法学习笔记(用心总结一文全会)(二)

文章目录 机器人逆运动学※ 代数解、几何解&#xff0c;解析解&#xff08;封闭解&#xff09;、数值解的含义与联系○ 代数解求 θ 1 \theta_1 θ1​、 θ 2 \theta_2 θ2​、 θ 3 \theta_3 θ3​※参考资料 求解 θ 1 \theta_1 θ1​ 求解 θ 3 \theta_3 θ3​ 求解 θ 2 \t…