微服务生产实战-Spring Cloud灰度发布和无损发布

大数据51

Spring Cloud灰度发布和无损发布

随着服务越来越多, 我们在实际的工作中我们会经常遇到如下问题:

1、几十上百的服务如何更好的在线上进行灰度发布, 使得部分用户能够使用最新的功能?

2、多人同环境开发或者共享部分服务的情况, 如何针对同一个服务分别进行不同逻辑的调试和验证?

3、服务在启动后突然收到大量请求会导致延时很大且业务大量失败, 如何做到服务级别的无损发布?

通过灰度发布, 我们希望做到任意链路的服务都能通过灰度规则请求到, 如下图所示:

微服务生产实战-Spring Cloud灰度发布和无损发布

灰度发布实现简述

spring cloud基于客户端的Ribbon做负载均衡, 要实现灰度发布, 首先需要知道整个Ribbon负载均衡的规则, 在负载均衡上进行规则定制, 使得不同的请求可以有自己特有的路由规则. 灰度发布实现主要有如下几个点设计:

1、服务能够标记自己支持的灰度版本
2、负载均衡器能获取到服务实例的版本信息
3、用户的灰度请求需要带上对应的灰度版本号
4、负载均衡器要识别请求里面对应的灰度版本, 并按照灰度版本路由到对应灰度版本的服务实例上
5、任意不存在的灰度请求或者异常请求均路由到正常服务上, 保证功能能够正常使用

1、服务标记自己支持的灰度版本

在spring cloud通过yaml或者properties我们能够很好设置配置项, 比如service.version=111. 但是为了让客户端能够拿到服务实例对应的版本信息, 我们需要讲此信息同步到注册中心, 这样客户端在拿到服务实例时, 不仅包含地址, 也包含支持的版本信息.

Eureka通过eureka.instance.metadata-map自定义元数据, 这里我们增加versions配置项, 用于表示服务实例支持的版本信息, 多个灰度版本用逗号分隔, 如:eureka.instance.metadata-map.versions=11,22,33

2、负载均衡器获取服务的版本信息

客户端可以通过ServerIntrospector.getMetadata()可以获取到注册中心某个服务实例的元数据信息, 具体代码如下:

public class ServiceInfoExtractor {

    private SpringClientFactory springClientFactory;

    public ServiceInfoExtractor(SpringClientFactory clientFactory) {
        this.springClientFactory = clientFactory;
    }

    /**
     * GetServerIntrospector
     * @param serviceId
     * @return
     */
    public ServerIntrospector getServerIntrospector(String serviceId) {
        ServerIntrospector serverIntrospector = this.springClientFactory.getInstance(serviceId,
                ServerIntrospector.class);
        if (serverIntrospector == null) {
            serverIntrospector = new DefaultServerIntrospector();
        }
        return serverIntrospector;
    }

    /**
     * Get service metainfo, config with "eureka.instance.metadata-map"
     * @param serviceId
     * @param server
     * @return
     */
    public Map<string, string> getMetadata(String serviceId, Server server) {
        return getServerIntrospector(serviceId).getMetadata(server);
    }

}</string,>

3、网关解析用户请求带上的灰度版本

为了方便内部服务识别, 我们定义一个固定的灰度KEY, 如X-VERSION, 用于在各个服务间进行传递. 前端用户请求在Header带上X-VERSION=111, 表示此请求是版本为111的灰度请求.

用户请求首先经过网关, 我们在网关将灰度版本拦截, 并保存在全链路上下文(实现方式详见springcloud生产实战-全链路上下文), 之后的每个服务都通过全链路上下文获取灰度版本信息.Zuul网关通过继承ZuulFilter实现一个自定义的灰度拦截器, 核心代码如下:

@Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        String version = ctx.getZuulRequestHeaders().get(ChainContextConstants.VERSION);
        if (StringUtils.isEmpty(version)) {
            version = Optional.ofNullable(ctx.getRequest().getHeader(ChainContextConstants.VERSION))
                    .orElse(ctx.getRequest().getParameter(ChainContextConstants.VERSION));
        }
        if (StringUtils.isNotEmpty(version)) {
            ChainRequestContext.getCurrentContext().put(ChainContextConstants.VERSION, version);
        }
        if (ctx.get(FilterConstants.SERVICE_ID_KEY) != null) {
            ChainRequestContext.getCurrentContext().put(ChainContextConstants.SERVICE_ID,
                    ctx.get(FilterConstants.SERVICE_ID_KEY));
        }
        return null;
    }

4、负载均衡器Ribbon进行灰度路由

Ribbon可以通过实现Predicate来自定义路由策略, 这里我们选择实现AbstractServerPredicate. 我们首先从链路上下文获取到请求灰度版本信息, 然后查看当前服务实例是否能够匹配版本.

public class GrayServicePredicate extends AbstractServerPredicate {

    public static final String META_VERSION = "versions";

    public GrayServicePredicate(GrayZoneAvoidanceRule rule) {
        super(rule);
    }

    @Override
    public boolean apply(@Nullable PredicateKey input) {
        ChainRequestContext chainCtx = ChainRequestContext.getCurrentContext();
        if (chainCtx != null &&  chainCtx.get(ChainContextConstants.SERVICE_ID) != null) {
            String requestVersion = (String) chainCtx.get(ChainContextConstants.VERSION);
            if (StringUtils.isEmpty(requestVersion)) {
                requestVersion = "default";
            }
            String serviceId = chainCtx.getString(ChainContextConstants.SERVICE_ID);
            Map<string, string> serviceMeta = ApplicationContextHelper.getApplicationContext()
                    .getBean(ServiceInfoExtractor.class).getMetadata(serviceId, input.getServer());
            return matchVersion(requestVersion, serviceMeta.get(META_VERSION));
        }
        return true;
    }

    /**
     * Version match
     *
     * @param grayVersion
     * @param serviceVersion
     * @return
     */
    private boolean matchVersion(String grayVersion, String serviceVersion) {
        if (StringUtils.isEmpty(serviceVersion)) {
            return false;
        }
        return ArrayUtils.contains(StringUtils.split(serviceVersion, ","), grayVersion);
    }

}</string,>

我们需要把GrayServicePredicate添加到负载均很的predicate集合中去, 通过自定义ZoneAvoidanceRule进行实现, 核心代码如下:

public class GrayZoneAvoidanceRule extends ZoneAvoidanceRule {

    protected CompositePredicate compositePredicate;

    public GrayZoneAvoidanceRule() {
        this(null, null);
    }

    public GrayZoneAvoidanceRule(CustomServicePredicate customServerPredicate, GrayProperties grayProperties) {
        super();
        // &#x8FFD;&#x52A0;&#x7070;&#x5EA6;&#x8DEF;&#x7531;
        Builder builder = CompositePredicate.withPredicates(super.getPredicate(), new GrayServicePredicate(this));
        compositePredicate = builder.build();
    }

    @Override
    public AbstractServerPredicate getPredicate() {
        return compositePredicate;
    }

}

Spring Cloud进行灰度发布的核心就实现了.

无损发布

无损发布涉及到很多其他的部分, 我们这里主要从spring cloud微服务层面如何做到无损发布. 无损发布一般分为如下几个步骤:

1、从注册中心移除服务实例
2、等服务实例处理完所有处理中的请求后停掉服务
3、服务升级发布并启动实例
4、预热脚本并延迟注册到注册中心
5、负载均衡流控处理.

前面1-4步骤更多是利用智能脚本进行执行流程规范化, 我们这里主要简单表述下流控处理.

首先要问问为什么需要流控处理?

因为Spring服务在启动后会有很多懒加载或者未缓存化的情形, 服务在一开始不能接受大量的请求, 不然很容易出现5XX. 尤其对于高并发请求的服务, 在实例刚启动不久就有大量请求进来, 会导致请求5XX且服务实例出现拥挤情况, 甚至造成连锁反应.

有人会说那启动后我们延迟更长时间再注册到注册中心是不是就会好了? 其实不然, 因为很多业务涉及到缓存和初始化等逻辑, 没有真实的请求进来, 整个服务很难进入一个高效状态, 所以我们需要服务在一开始的时候有较少的请求来进行服务预热.

实现方式我们依然在负载均衡的时候对服务实例的注册时间进行判断处理, 当服务启动前期一段时间, 我们通过少量的请求路由到新实例. Eureka注册中心里面记录了服务的注册时间, 核心代码逻辑如下所示:

public static Optional<server> choose(List<server> serverlist, long startupDelayTimestamp) {
        long currentTimestamp = System.currentTimeMillis();
        List<server> finalServerList = Lists.newArrayList();
        for (Server server : serverlist) {
            try {
                DiscoveryEnabledServer discoveryServer = (DiscoveryEnabledServer) server;
                long startupSoFar = currentTimestamp
                        - discoveryServer.getInstanceInfo().getLeaseInfo().getRegistrationTimestamp();
                if ((startupSoFar >= startupDelayTimestamp) || RandomUtils.nextLong(0, startupDelayTimestamp) < startupSoFar) {
                    // &#x8D85;&#x8FC7;deplay&#x65F6;&#x95F4;&#x6216;&#x8005;&#x968F;&#x673A;&#x5230;&#x65F6;&#x95F4;&#x8303;&#x56F4;&#x5185;
                    finalServerList.add(server);
                }
            } catch (Exception e) {
                log.error("fail to choose server with server:{}", server, e);
            }
        }
        if (finalServerList.isEmpty()) {
            finalServerList = serverlist;
        }
        return Optional.ofNullable(finalServerList.get(RandomUtils.nextInt(0, finalServerList.size())));
    }</server></server></server>

服务注册到注册中心时间越短, 请求路由的概率就会越小.

Original: https://blog.csdn.net/einarzhang/article/details/118853063
Author: EinarZhang
Title: 微服务生产实战-Spring Cloud灰度发布和无损发布