什么是灰度发布?
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
本文以springcloud gateway + nacos来演示如何实现灰度发布,如果对springcloud gateway和nacos还不熟悉的朋友,可以先阅读如下文章,然后再阅读本文。
springcloud gateway官方介绍
nacos官方介绍
实现的整体思路:
编写带权重的灰度路由
编写自定义filter
nacos服务配置需要灰度发布的服务的元数据信息以及权重
灰度路由从nacos服务拉取元数据信息以及权重,然后根据权重算法,返回符合要求的服务实例给自定义的filter
网关配置文件配置需要灰度路由的服务(因为本文代码没有网关实现动态路由,不然灰度路由可以配置在配置中心,从配置中心拉取)
filter通过责任链模式,把服务实例透传给其他filter比如NettyRoutingFilter
下边进入实战
正文1、所使用的开发版本
<jdk.version>1.8</jdk.version> <!-- spring cloud --> <spring-cloud.version>Hoxton.SR3</spring-cloud.version> <spring-boot.version>2.2.5.RELEASE</spring-boot.version> <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>2、pom.xml引入
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>ps:nacos的jar注意排除ribbon依赖,不然loadbalancer无法生效
3、编写权重路由
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer { private static final Log log = LogFactory.getLog(GrayLoadBalancer.class); private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; private String serviceId; public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { HttpHeaders headers = (HttpHeaders) request.getContext(); if (this.serviceInstanceListSupplierProvider != null) { ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers)); } return null; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) { if (instances.isEmpty()) { return getServiceInstanceEmptyResponse(); } else { return getServiceInstanceResponseWithWeight(instances); } } /** * 根据版本进行分发 * @param instances * @param headers * @return */ private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) { String versionNo = headers.getFirst("version"); System.out.println(versionNo); Map<String,String> versionMap = new HashMap<>(); versionMap.put("version",versionNo); final Set<Map.Entry<String,String>> attributes = Collections.unmodifiableSet(versionMap.entrySet()); ServiceInstance serviceInstance = null; for (ServiceInstance instance : instances) { Map<String,String> metadata = instance.getMetadata(); if(metadata.entrySet().containsAll(attributes)){ serviceInstance = instance; break; } } if(ObjectUtils.isEmpty(serviceInstance)){ return getServiceInstanceEmptyResponse(); } return new DefaultResponse(serviceInstance); } /** * * 根据在nacos中配置的权重值,进行分发 * @param instances * * @return */ private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) { Map<ServiceInstance,Integer> weightMap = new HashMap<>(); for (ServiceInstance instance : instances) { Map<String,String> metadata = instance.getMetadata(); System.out.println(metadata.get("version")+"-->weight:"+metadata.get("weight")); if(metadata.containsKey("weight")){ weightMap.put(instance,Integer.valueOf(metadata.get("weight"))); } } WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap); if(ObjectUtils.isEmpty(weightMeta)){ return getServiceInstanceEmptyResponse(); } ServiceInstance serviceInstance = weightMeta.random(); if(ObjectUtils.isEmpty(serviceInstance)){ return getServiceInstanceEmptyResponse(); } System.out.println(serviceInstance.getMetadata().get("version")); return new DefaultResponse(serviceInstance); } private Response<ServiceInstance> getServiceInstanceEmptyResponse() { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); }