网站建设定制开发基于 GateWay 和 Nacos 实现微服务架构灰度发布方案

一、

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

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

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

二、开始实施

首先搭建两个web服务模拟生产和灰度环境,分别注册到 中,注意这里服务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 中查看详情:



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

@RestControllerpublic class TestController {    @GetMapping("/getTest")    public String getTest(){        return "当前处于-生产环境!";    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
@RestControllerpublic 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@RefreshScopepublic 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@RefreshScopepublic 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@Slf4jpublic 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,然后多访问几次,可以看到都被转发到了 生产环境:

三、服务中互相调用问题

上面可以通过 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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

@Configurationpublic 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"));    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在 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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发