springcloud gateway实现灰度发布

灰度发布

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

更多介绍可以参考百度百科灰度发布

今天我将利用Spring Gateway 结合Nacos实现灰度发布

由于代码过多,这里只展示了关键信息,源代码地址以防在文末

创建项目

项目结构

  • api-server 需要灰度的app应用
  • gateway-server gateway 网管
  • nacos 注册中心

项目版本

    <properties>
        <youhuo.version>0.1-SNAPSHOT</youhuo.version>
        <spring.boot.version>2.7.8</spring.boot.version>
        <spring.cloud.version>2021.0.5</spring.cloud.version>
        <spring.cloud.alibaba.version>2021.0.4.0</spring.cloud.alibaba.version>
    </properties>

Gateway Server

灰度配置文件

@Data
// 动态刷新
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "server.gray")
public class ServerGrayProperty {

    /**
     * 生产的版本
     */
    private String proVersion;
    /**
     * 需要灰度的人员列表
     */
    private List<String> grayUsers;

    /**
     * 灰度的版本
     */
    private String grayVersion;

    /**
     * 权重
     */
    private Double weight;

    /**
     * 是否开启{@link GrayEnvLoadBalancer} 的方式进行灰度发布
     */
    private Boolean enable = true;
}

示例

server:
  gray:
    proVersion: 1
    enable: true
    grayUsers:  abc,ii,ss,kk,bb,pp
    grayVersion:  2

GrayEnvLoadBalancer

通过自定义的配置实现获取不同的路由选择

@RequiredArgsConstructor
@Slf4j
public class GrayEnvLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private ServerGrayProperty serverGrayProperty;
    /**
     * 用于获取 serviceId 对应的服务实例的列表
     */
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    /**
     * 需要获取的服务实例名
     * <p>
     * 暂时用于打印 logger 日志
     */
    private final String serviceId;

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 获得 HttpHeaders 属性,实现从 header 中获取 version
        HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
        // 选择实例
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map(list -> getInstanceResponse(list, headers));
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
        // 如果服务实例为空,则直接返回
        if (CollUtil.isEmpty(instances)) {
            log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
            return new EmptyResponse();
        }
        // todo 这里获取可以根据等候的信息获取
        String userId = Objects.requireNonNull(headers.get("userId")).stream().findFirst().orElse("");
        // 筛选满足 version 条件的实例列表
        String version = serverGrayProperty.getGrayVersion();
        // 筛选满足 灰度的用户实例列表
        List<String> grayUsers = serverGrayProperty.getGrayUsers();
        List<ServiceInstance> chooseInstances;
        if (StrUtil.isEmpty(version) && !grayUsers.contains(userId)) {
            chooseInstances = instances;
        } else {
            // 选择满足条件的实例
            chooseInstances = filterList(instances, instance -> version.equals(instance.getMetadata().get("version")) && grayUsers.contains(userId));
            if (CollUtil.isEmpty(chooseInstances)) {
                log.warn("[getInstanceResponse][serviceId({}) 没有满足版本({})的服务实例列表,直接使用所有服务实例列表]", serviceId, version);
                chooseInstances = instances;
            }
        }

        // 基于 tag 过滤实例列表
        chooseInstances = filterTagServiceInstances(chooseInstances, headers);

        // 随机 + 权重获取实例列表
        return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
    }


    /**
     * 基于 tag 请求头,过滤匹配 tag 的服务实例列表
     * <p>
     * copy from EnvLoadBalancerClient
     *
     * @param instances 服务实例列表
     * @param headers   请求头
     * @return 服务实例列表
     */
    private List<ServiceInstance> filterTagServiceInstances(List<ServiceInstance> instances, HttpHeaders headers) {
        // 情况一,没有 tag 时,直接返回
        String tag = EnvUtils.getTag(headers);
        if (StrUtil.isEmpty(tag)) {
            return instances;
        }

        // 情况二,有 tag 时,使用 tag 匹配服务实例
        List<ServiceInstance> chooseInstances = filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
        if (CollUtil.isEmpty(chooseInstances)) {
            log.warn("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
            chooseInstances = instances;
        }
        return chooseInstances;
    }

    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(predicate).collect(Collectors.toList());
    }


    public void setServerGrayProperty(ServerGrayProperty serverGrayProperty) {
        this.serverGrayProperty = serverGrayProperty;
    }
}

GrayReactiveLoadBalancerClientFilter

@Component
@AllArgsConstructor
@Slf4j
@SuppressWarnings({"JavadocReference", "rawtypes", "unchecked", "ConstantConditions"})
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {

    private final LoadBalancerClientFactory clientFactory;

    private final GatewayLoadBalancerProperties properties;

    private final ServerGrayProperty serverGrayProperty;

    @Override
    public int getOrder() {
        return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        // 将 lb 替换成 grayLb,表示灰度负载均衡
        if (url == null || (!"grayLb".equals(url.getScheme()) && !"grayLb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        // preserve the original url
        addOriginalRequestUrl(exchange, url);

        if (log.isTraceEnabled()) {
            log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
        }

        URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String serviceId = requestUri.getHost();
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
                .getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
                        RequestDataContext.class, ResponseData.class, ServiceInstance.class);
        DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
                new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));
        return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {

            if (!response.hasServer()) {
                supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));
                throw NotFoundException.create(properties.isUse404(), "Unable to find instance for " + url.getHost());
            }

            ServiceInstance retrievedInstance = response.getServer();

            URI uri = exchange.getRequest().getURI();

            // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
            // if the loadbalancer doesn't provide one.
            String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
            if (schemePrefix != null) {
                overrideScheme = url.getScheme();
            }

            DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
                    overrideScheme);

            URI requestUrl = reconstructURI(serviceInstance, uri);

            if (log.isTraceEnabled()) {
                log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
            }
            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
            exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));
        }).then(chain.filter(exchange))
                .doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
                                CompletionContext.Status.FAILED, throwable, lbRequest,
                                exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR)))))
                .doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
                                CompletionContext.Status.SUCCESS, lbRequest,
                                exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR),
                                new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest()))))));
    }

    protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
        return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
    }

    private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
                                                   Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
        if (serverGrayProperty.getEnable()) {
            // 直接创建 GrayEnvLoadBalancer 对象
            GrayEnvLoadBalancer loadBalancer = new GrayEnvLoadBalancer(
                    clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
            loadBalancer.setServerGrayProperty(serverGrayProperty);
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
            return loadBalancer.choose(lbRequest);
        } else {
            // 直接创建 GrayLoadBalancer 对象
            GrayLoadBalancer loadBalancer = new GrayLoadBalancer(
                    clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
            return loadBalancer.choose(lbRequest);
        }
    }

    private String getHint(String serviceId) {
        LoadBalancerProperties loadBalancerProperties = clientFactory.getProperties(serviceId);
        Map<String, String> hints = loadBalancerProperties.getHint();
        String defaultHint = hints.getOrDefault("default", "default");
        String hintPropertyValue = hints.get(serviceId);
        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
    }

}

这里我们就可以根据ServerGrayProperty配置的映射进行不灰度策略的实现

Api Server

一个简单的Springboot 项目,每次请求返回自己服务的端口号

@RestController
@RequestMapping(value = "/system/api/")
public class GraLbController {

    @Value("${server.port}")
    private Integer port;

    @GetMapping(value = "test")
    public String test() {
        return String.format("port=%s", port);
    }
}

需要启动两个项目,设置不同的端口号即可
在这里插入图片描述

Nacos配置

确定我们的服务已经注册到Nacos
在这里插入图片描述
修改api的元数据
在这里插入图片描述

{
	"preserved.register.source": "SPRING_CLOUD",
	"version": "2",
	"nacos.weight":10,
	"nacos.healthy":true,
	"name":"gray-api-01"
}

效果展示

不在访问列表里的用户持续访问原有生产版本

GET http://localhost:9080/system/api/test
userId: abc2

返回效果如下:

HTTP/1.1 200 OK
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: text/plain;charset=UTF-8
Content-Length: 9
Date: Wed, 29 Mar 2023 13:03:47 GMT

port=9081

当用户出现在访问列表后将灰度到升级版本中

GET http://localhost:9080/system/api/test
userId: abc

返回效果如下:

HTTP/1.1 200 OK
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: text/plain;charset=UTF-8
Content-Length: 9
Date: Wed, 29 Mar 2023 13:05:28 GMT

port=8092

如果有任何疑问,可以联系小编 372787553,小遍
大家如果想进入程序交流群,可以直接备注进群即可

源代码地址 https://github.com/yanghaiji/grayscale-publishing-demo 如果对您有所帮助记得 star哦

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小杨同学~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值