【收藏】基于spring cloud灰度发版方案

简介

敏捷开发迭代周期短发布快,每周都可能面临版本发版上线,为最大可能的降低对用户的影响提高服务可用率,大部分团队都需要等到半夜做发布和支持。本文就如何基于spring cloud体系做灰度发版改造提供了方案,让我们终于白天也能偷偷摸摸的无感知发版,验证,上线等动作,从此再也不用因为发版要熬夜了。

本文阐述的方案是灰度发版方案的一种实现(各种部署方案可参考文档最后的附录),属于一种比较节约资源的部署方案,通过精准的导流和开关控制实现用户无感知的热部署,比较适合中小企业采纳应用。整体技术架构基于nepxion discovery插件结合项目中各个实践场景做了方案说明和源代码展示,如需要做权重,分组等策略可自行扩展。

术语与配置

名称 说明
灰度节点 被标记为灰度的节点
灰度入口 前端部署的节点被标记为灰度的节点
灰度用户 账号被标记位灰度的用户
灰度流量 路由是需要优先选择灰度节点的请求链
灰度开关 是否开启灰度路由,值为:开启/关闭
灰度流量开关 是否所有流量都是灰度流量,值为开启/关闭

开关与流量关系

灰度流量开关\灰度总开关 适用场景 正常用户
正常入口
灰度用户
灰度入口
(灰度总开关)开
(灰度流量开关)关
灰度节点发版,新版本验证阶段 旧版本体验 新版本体验
(灰度总开关)开
(灰度流量开关)开
正常节点发版,新版本批量部署阶段 新版本体验 新版本体验
(灰度总开关)关
(灰度流量开关)开/关
新版本完成上线 新版本体验 新版本体验

灰度配置 Gray Properties

![1591942259553](d:\user\01388368\Application Data\Typora\typora-user-images\1591942259553.png)

用户白名单:

  • 节点清单加载可以从eureka获取

public ResultMessage getServices() {
        //本地配置的服务map
        Map<String, Service> servicesLocalMap = getServicesLocalMap();

        //要返回的服务清单
        List<Service> services = new ArrayList();
        discoveryClient.getServices().forEach(service -> {
            final List<Instance> instances = new ArrayList();
            discoveryClient.getInstances(service).forEach(instanceInfo -> {
                instances.add(toInstance(instanceInfo));
            });

            //优先使用本地实例
            if (null != servicesLocalMap.get(service)) {
                final List<Instance> serviceLocalInstances = servicesLocalMap.get(service).getData();
                //更新状态
                List<Instance> serviceLocalInstancesHasLatestStatus = serviceLocalInstances.stream()
                        .map(instanceLocal -> instances.stream()
                                        .filter(instance -> StringUtils.join(instance.getHost(), instance.getPort().toString()).equals(StringUtils.join(instanceLocal.getHost(), instanceLocal.getPort().toString())))
                                        .findFirst().map(m -> {
                                            instanceLocal.setStatus(m.getStatus());
                                            return instanceLocal;
                                        })
//                                        .orElse(null)
                                        .orElseGet(() -> {
                                            instanceLocal.setStatus("OFFLINE");
                                            return instanceLocal;
                                        })
                        ).filter(Objects::nonNull).collect(Collectors.toList());

                //去除eureka中本地已配置的实例
                List<Instance> instancesRemoveLocal = instances.stream().filter(instance -> !serviceLocalInstancesHasLatestStatus.stream()
                        .anyMatch(instanceLocal -> StringUtils.join(instance.getHost(), instance.getPort().toString()).equals(StringUtils.join(instanceLocal.getHost(), instanceLocal.getPort().toString()))))
                        .collect(Collectors.toList());

                //清空并重新添加处理过的本地和远程实例
                instances.clear();
                instances.addAll(instancesRemoveLocal);
                instances.addAll(serviceLocalInstancesHasLatestStatus);

                //本地服务实例排除已经添加的服务
                servicesLocalMap.remove(service);
            }

            //排序
            Collections.sort(instances);

            //单个服务和节点添加
            Service serviceResp = new Service();
            serviceResp.setService(service);
            serviceResp.setData(instances);
            services.add(serviceResp);
        });

        //添加eureka中不存在,本地存在的服务
        servicesLocalMap.values().forEach(service->{
            Collections.sort(service.getData());
            services.add(service);
        });

        Collections.sort(services);
        return ResultCode.SUCCESS.withData(services);
    }

依赖 Gray Dependency

<!-- 服务的灰度依赖 -->
<dependency>
    <groupId>com.sf</groupId>
    <artifactId>cloud-discovery-service-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- 网关的灰度依赖,注意不能与服务的灰度依赖一起配置 -->
<dependency>
    <groupId>com.sf</groupId>
    <artifactId>cloud-discovery-gateway-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>


<!-- 灰度发版插件 -->
<plugin>
    <groupId>com.sf</groupId>
    <artifactId>maven-plugin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>gray-plugin</goal>
            </goals>
            <configuration>
                <grayBuildLocationExclude></grayBuildLocationExclude>
            </configuration>
        </execution>
    </executions>
</plugin>

灰度头部 GrayHeader

灰度头部信息信息,编码支持的类有 < GrayHeader,GrayHeaderConstant,GrayUtil, ServiceGrayUtil >

灰度对象存放的上下文有:自定义实现类 < GrayHeaderHolder >,Request内置实现类 < RequestContextHolder >

参数key 参数value 说明
h-gray-is true/false 是否为灰度流量
h-gray-domain 域名 用户请求的域名
h-gray-userid xxx 用户请求的账户

技术改造点

灰度改造分三大类:网关改造,服务改造,场景改造。主要目的是实现灰度头部计算、复用、续传,负载均衡的改造。

网关改造 cloud-discovery-gateway-starter

修改pom.xml依赖
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-starter-eureka</artifactId>
    <version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-gateway</artifactId>
    <version>0.0.2-SNAPSHOT</version>
</dependency>

configure加载项
@Configuration
public class DiscoveryGatewayAutoConfiguration {
    //负载均衡改造注入
    @Bean
    public DiscoveryEnabledAdapter discoveryEnabledAdapter() {
        return new GatewayGrayDiscoveryEnabledAdapter();
    }
    //灰度路由计算注入
    @Bean
    public GrayRouteFlagFilter grayRouteFilter() {
        return new GrayRouteFlagFilter();
    }
    //灰度配置获取注入
    @Bean
    public GrayPropertiesLoader grayPropertiesLoader() {
        return new GrayPropertiesLoader(gatewayRedisson);
    }
}
[路由场景]增加header

在网关新增Filter,将request上下文和灰度配置匹配,算出灰度路由标记

public class GrayRouteFlagFilter implements GlobalFilter, Ordered {
    @Autowired
    private GrayPropertiesLoader grayPropertiesLoader;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            ServerHttpRequest request = exchange.getRequest();
            Boolean isGray = grayPropertiesLoader.calculateGrayFlag(request.getHeaders().getFirst(GrayHeaderConstant.GRAY_IS),
                    request.getHeaders().getFirst(GrayHeaderConstant.GRAY_NGINX_IP),
                    request.getHeaders().getFirst(GrayHeaderConstant.GRAY_USER_ACCOUNT));
            request = request.mutate().header(GrayHeaderConstant.GRAY_IS, isGray.toString()).build();
            exchange = exchange.mutate().request(request).build();
        }catch (Throwable e){
            log.error("未知错误",e);
        }finally {
            return chain.filter(exchange);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值