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

举报
程序员-上善若水 发表于 2022/06/23 22:20:38 2022/06/23
【摘要】 一、灰度发布 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什...

一、灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行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 # 指定版本号

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

灰度环境配置:

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

在这里插入图片描述
在这里插入图片描述
下面为了模拟两个服务的差异性,创建相同的接口,不同的返回:

@RestController
public class TestController {

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
@RestController
public class TestController {

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

下面开始搭建 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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

再增加一个 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 # 去除请求地址中的前缀

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

下面搭建 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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

启动后,查看下是否已经注册到 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;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在创建一个 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();
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

下面创建 过滤器 对请求进行拦截,然后获取到用户的信息,这里就默认用户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));
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

上面的过滤器已经标识出当前请求属于灰度还是生产,下面就需要我们重写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;
        }
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

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

文章来源: blog.csdn.net,作者:小毕超,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/qq_43692950/article/details/125226460

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。