灰度发布的方案有很多,这里我结合公司使用的框架给出一套灰度发布方案。我们公司使用的是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在执行负载均衡策略时只会去找版本号匹配的节点转发
- 上面的步骤准备完毕后测试发现并没有实现我想要的功能,通过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,代码中引用的部分外部函数大家可以网上百度