草庐IT

基于 GateWay 和 Nacos 实现微服务架构灰度发布方案

小毕超 2023-10-26 原文

一、灰度发布

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

灰度发布开始到结束期间的这一段时间,称为灰度期。灰度发布能及早获得用户的意见反馈,完善产品功能,提升产品质量,让用户参与产品测试,加强与用户互动,降低产品升级所影响的用户范围。

下面基于 GateWayNacos 实现微服务架构灰度发布方案,首先对生产的服务和灰度环境的服务统一注册到 Nacos 中,但是版本不同,比如生产环境版本为 1.0 ,灰度环境版本为 2.0 ,请求经过网关后,判断携带的用户是否为灰度用户,如果是将请求转发至 2.0 的服务中,否则转发到 1.0 的服务中。

二、开始实施

首先搭建两个web服务模拟生产和灰度环境,分别注册到nacos 中,注意这里服务ID 要一致:

生产环境配置:

spring:
  application:
    name: web
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        metadata:
          version: 1.0 # 指定版本号

灰度环境配置:

spring:
  application:
    name: web
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        metadata:
          version: 2.0 # 指定版本号

启动两个服务后,可以在nacos 中查看详情:



下面为了模拟两个服务的差异性,创建相同的接口,不同的返回:

@RestController
public class TestController {

    @GetMapping("/getTest")
    public String getTest(){
        return "当前处于-生产环境!";
    }
}
@RestController
public class TestController {

    @GetMapping("/getTest")
    public String getTest(){
        return "当前处于-灰度环境!";
    }
}

下面开始搭建 GateWay 网关,同样需要注册到 nacos 中,但是和以前不同的是,这里我们要实现一个负载均衡器,在负载均衡器中判断是否使用哪个版本的服务,这里为了演示效果,在nacos 中新建一个配置文件,将灰度用户配置在这个配置文件中,在项目中应该从 db 或 noSQL 中进行获取。

Data ID: env-config.yaml
Group: DEFAULT_GROUP

env:
  gray:
    version: 2.0
    users: abc,ii,ss,kk,bb,pp
  pro:
    version: 1.0

再增加一个 GateWay 路由的配置:

Data ID:gateway.yaml
Group: DEFAULT_GROUP

spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 2000
        response-timeout: 10s
      routes:
        - id: web
          uri: lb://web/
          order: 0
          predicates:
            - Path=/web/**
          filters:
            - StripPrefix=1 # 去除请求地址中的前缀

下面搭建 gateway 网关服务,注册到 nacos 中,并加载上面创建的配置文件:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        refresh-enabled: true
        extension-configs[0]:
          data-id: env-config.yaml
          group: DEFAULT_GROUP
          refresh: true

启动后,查看下是否已经注册到 nacos 中了:

测试下是否可以负载转发:


已经实现了负载效果,但是还没有达到我们想要的效果,下面开始对 gateway 网关进行修改。

首先我们新建一个 EnvProperties 来接收 env-config.yaml 中的配置,注意一定要加 @RefreshScope 注解,这样才能修改配置后通知到相应的服务:

@Data
@Configuration
@RefreshScope
public class EnvProperties {
    @Value("${env.pro.version}")
    private String proVersion;

    @Value("${env.gray.users}")
    private List<String> grayUsers;

    @Value("${env.gray.version}")
    private String grayVersion;
}

在创建一个 ThreadLocal ,存储当前的版本信息,这里先记下来,后面就知道什么作用了:

public class GrayscaleThreadLocalEnvironment {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
    public static void setCurrentEnvironment(String currentEnvironmentVsersion) {
        threadLocal.set(currentEnvironmentVsersion);
    }
    
    public static String getCurrentEnvironment() {
        return threadLocal.get();
    }
}

下面创建 过滤器 对请求进行拦截,然后获取到用户的信息,这里就默认用户ID 在 header 中,key 为 userId,取到之后判断是否在 灰度用户列表中,如果存在就把当前的 ThreadLocal(就是上面声明的ThreadLocal ) 中存储灰度的版本号,,否则就为生产的版本号:

@Component
@RefreshScope
public class GrayscaleGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    EnvProperties envProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders header = response.getHeaders();
        header.add("Content-Type", "application/json; charset=UTF-8");

        List<String> list = request.getHeaders().get("userId");
        if (Objects.isNull(list) || list.isEmpty()) {
            return resultErrorMsg(response," 缺少userId!");
        }
        String userId = list.get(0);
        if (StringUtils.isBlank(userId)) {
            return resultErrorMsg(response," 缺少userId!");
        }
        if (envProperties.getGrayUsers().contains(userId)) {
            //指定灰度版本
            GrayscaleThreadLocalEnvironment.setCurrentEnvironment(envProperties.getGrayVersion());
        } else {
            //指定生产版本
            GrayscaleThreadLocalEnvironment.setCurrentEnvironment(envProperties.getProVersion());
        }
        return chain.filter(exchange.mutate().request(request).build());
    }

    public int getOrder() {
        return -1;
    }

    private Mono<Void> resultErrorMsg(ServerHttpResponse response, String msg) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", "403");
        jsonObject.put("message", msg);
        DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
        return response.writeWith(Mono.just(buffer));
    }
}

上面的过滤器已经标识出当前请求属于灰度还是生产,下面就需要我们重写Ribbon 负载均衡器,这里重写的 RoundRobinRule ,在 choose 方法中,根据当前 ThreadLocal 中的版本,便利服务中版本与之相等的服务,作为转发服务,为了防止服务获取失败,这里曾加了重试策略,重试 10 次还是失败,即放弃重试:

@Component
@Slf4j
public class EnvRoundRobinRule extends RoundRobinRule {

    private AtomicInteger nextServerCyclicCounter;

    public EnvRoundRobinRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }
        Server server = null;
        int count = 0;
        // 如果失败,重试 10 次
        while (Objects.isNull(server) && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
            List<NacosServer> filterServers = new ArrayList<>();
            String currentEnvironmentVersion = GrayscaleThreadLocalEnvironment.getCurrentEnvironment();

            for (Server serverInfo : reachableServers) {
                NacosServer nacosServer = (NacosServer) serverInfo;
                String version = nacosServer.getMetadata().get("version");
                if (version.equals(currentEnvironmentVersion)) {
                    filterServers.add(nacosServer);
                }
            }

            int filterServerCount = filterServers.size();
            int nextServerIndex = incrementAndGetModulo(filterServerCount);
            server = filterServers.get(nextServerIndex);

            if (server == null) {
                Thread.yield();
                continue;
            }
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    private int incrementAndGetModulo(int modulo) {
        for (; ; ) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
}

到这 流程基本就已经结束了,下面在header 中增加 userId 为 abc,然后多访问几次,可以看到都被转发到了 灰度环境:

下面在header 中增加 userId 为 110,然后多访问几次,可以看到都被转发到了 生产环境:

三、服务中互相调用问题

上面可以通过 userId 来控制是否转发到灰度环境,但是随之而来还有一个问题就是,服务都注册到了同一个 nacos 中,那服务间互相调用的时候不还是没有控制环境,生产的服务通过 feign 客户端调用,通过轮训就会调用到灰度环境的服务,对此就需要对每个服务的 Ribbon 负载规则进行上面的配置,我们再使用feign 客户端的时候,将 userId 放入请求的 header 中,然后每个服务在请求拦截器中从header 中获取 userId,然后放入当前的 ThreadLoad 中。

这里补充下 feignheader 中传递数据有几种实现方式,可以通过 @RequestHeader 注解进行添加,也可以在请求拦截器中添加。

@RequestHeader 中添加:

@Component
@FeignClient(value = "PROVIDER",contextId = "ProviderFeignService",path = "/test")
public interface ProviderFeignService {
    @GetMapping("/getData")
    JSONObject getData(@RequestParam(name = "data") String data, @RequestHeader(name = "userId") String userId);

    @PostMapping("/postData")
    JSONObject postData(@RequestBody Map<String, Object> data);
}

本文的场景需要都所有的 feign 调用都要添加请球头,因此下面在拦截器中添加比较合适:

@Configuration
public class FeignConfiguration implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        requestTemplate.header("userId", request.getHeader("userId"));
    }
}

在 feign 接口中指定配制:

@Component
@FeignClient(value = "PROVIDER",contextId = "ProviderFeignService",path = "/test",configuration = FeignConfiguration.class)
public interface ProviderFeignService {
    @GetMapping("/getData")
    JSONObject getData(@RequestParam(name = "data") String data);

    @PostMapping("/postData")
    JSONObject postData(@RequestBody Map<String, Object> data);
}

有关基于 GateWay 和 Nacos 实现微服务架构灰度发布方案的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  4. ruby-on-rails - 如何在发布新的 Ruby 或 Rails 版本时收到通知? - 2

    有人知道在发布新版本的Ruby和Rails时收到电子邮件的方法吗?他们有邮件列表,RubyonRails有一个推特,但我不想听到那些随之而来的喧嚣,我只想知道什么时候发布新版本,尤其是那些有安全修复的版本。 最佳答案 从therailsblog获取提要.http://weblog.rubyonrails.org/feed/atom.xml 关于ruby-on-rails-如何在发布新的Ruby或Rails版本时收到通知?,我们在StackOverflow上找到一个类似的问题:

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  7. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  8. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  9. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  10. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

随机推荐