前后端灰度发布解决方案

        灰度发布的方案有很多,这里我结合公司使用的框架给出一套灰度发布方案。我们公司使用的是spring cloud框架,zuul做网关,ribbon做负载均衡,eureka做注册中心。
        我自己对灰度发布的理解就是能够做到根据不同策略去区分用户,根据用户标签去展示不同的效果。比如当我们在线上发布一个新的功能时,由于我们无法预测这个功能带来的影响,所以我们想只让老用户能够访问这个新功能,然后收集用户反馈。如果用户反响不好,就把新功能关闭,把老用户切换回原来的逻辑;如果用户反馈满意,我们就把新功能对所有用户开放,但是这个切换的过程又不想去重启服务。
        上面这个需求相信很多人在生产环境都会碰到,这里我们就可以使用灰度发布去解决这个需求。我上面说过我们采用eureka做注册中心,所有的服务都会向eureka中注册,然后ribbon从eureka的注册列表去读取可用的服务列表,根据一定的策略去分发请求。只要理解了这一点,灰度发布的实现就很容易了。

  • 添加jar包依赖
		<dependency>
            <groupId>io.jmnarloch</groupId>
            <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
  • 首先我们在往eureka注册服务时,对于服务名相同的服务,我们可以指定每个服务的版本,例如当前有一个服务ADMIN,然后我们通过eureka.instance.metadata-map.version为两个服务指定不同版本1.0/2.0,启动两个服务,此时eureka中就能看到ADMIN中有两个服务同时存在。
    设置服务版本号
    服务列表
  • 我们使用zuul做网关,所有的请求都经过网关转发,所以这里新建一个filter,在filter中设置想要转发请求的版本号(这个需要结合自己的业务去判断版本号,可能根据用户session,可能根据ip等等),这样ribbon在执行负载均衡策略时只会去找版本号匹配的节点转发
    自定义filter
  • 上面的步骤准备完毕后测试发现并没有实现我想要的功能,通过debug我发现在MetadataAwarePredicate.apply()方法中有一段代码
		//内部通过ThreadLocal实现
		final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
		//获取在第二步存放的“version”    (RibbonFilterContextHolder.getCurrentContext().add("version", version.toString())
		可以在这里获取
        final Set<Map.Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
        //查询eureka中服务的元数据,也就是第一步中设置的metadata
        final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
        //匹配versoin
        return metadata.entrySet().containsAll(attributes);

可以发现,这里的实现借助了ThreadLocal,就需要保证线程上线文不能丢失。所以我感觉应该是由于我采用线程隔离策略导致的上下文丢失。于是我分别在zuul filter和apply中打印了线程,发现确实存在线程切换,所有ThreadLocal无法生效,从而导致获取的version与存储的version不一致,进而无法通过version去转发请求到指定的服务。
因此我们需要自定义一套线程隔离策略,保证线程切换时上下文不会丢失

public class CustomConcurrencyStrategy extends HystrixConcurrencyStrategy {

    private HystrixConcurrencyStrategy delegate;

    public CustomConcurrencyStrategy() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof CustomConcurrencyStrategy) {
                // Welcome to singleton hell...
                return;
            }
            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();
            this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);
            HystrixPlugins.reset();
            HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
            HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
            HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        } catch (Exception e) {
            log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
        }
    }

    private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
                                                 HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) {
        if (log.isDebugEnabled()) {
            log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy ["
                    + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher ["
                    + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
            log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
        }
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        Map<String, String> ribbonAttributes = RibbonFilterContextHolder.getCurrentContext().getAttributes();
        return new WrappedCallable<>(callable, requestAttributes, ribbonAttributes);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize,
                                            HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixThreadPoolProperties threadPoolProperties) {
        return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return this.delegate.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
        return this.delegate.getRequestVariable(rv);
    }

    static class WrappedCallable<T> implements Callable<T> {
        private final Callable<T> target;
        private final RequestAttributes requestAttributes;
        private final Map<String, String> ribbonAttributes;

        public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes, Map<String, String> ribbonAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
            this.ribbonAttributes = ribbonAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                if(!CollectionUtils.isEmpty(ribbonAttributes)){
                    for(String key : ribbonAttributes.keySet()){
                        RibbonFilterContextHolder.getCurrentContext().add(key, ribbonAttributes.get(key));
                    }
                }
                return target.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}

所有步骤完成后再测试灰度发布,发现达到了预期的效果。

上述的后端的灰度发布解决方案,前端的灰度发布可以借助nginx+lua去实现。下面是我实验的代码,使用的环境是openresty-1.15.8.2-win64,代码中引用的部分外部函数大家可以网上百度
nginx+lua

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值