本文转载自微信公众号《JAVA日知录》,作者飘渺果酱。转载本文请联系JAVA日知录公众号。前言本文来自粉丝提出的一个问题:如何解决多环境统一注册中心服务实例跑来跑去的问题?你怎么理解的?假设开发环境中的AccountService已经注册到Nacos中,现在张某需要对其进行修改升级,在本地启动AccountService后,也注册到Nacos中,但是在调试的时候,请求经常会直接跳转到开发环境中通过网关,所以小张没法安心调试。其实这个问题归结为如何基于SpringCloudGateway实现灰度发布,让请求流量通过指定的规则到达特定的实例。在SpringCloud2020版本中,官方推荐使用SpringCloudLoadBalancer来替代原有的Ribbon负载均衡器。所以在本文中,我们直接基于SpringCloudLoadBalancer来实现。Tips:什么是灰度发布灰度发布(也称为金丝雀发布)是指一种可以在黑白之间平滑过渡的发布方式。可以对其进行A/B测试,即让部分用户继续使用产品特性A,部分用户开始使用产品特性B,如果用户对B没有异议,则逐步扩大范围,全部迁移用户来B来。灰度发布可以保证整个系统的稳定性,可以在灰度初期发现问题并进行调整,保证其影响。实现目标目标非常明确。小张希望调试时发送的请求能直达自己本地的开发环境,方便调试。实现思路要实现这个目标,我们需要解决两个关键问题:如何区分不同的实例,需要给小张一个本地启动的AccountService服务实例的特殊标识,以区别于开发环境。这里我们可以使用注册中心的元数据来区分,可以通过spring.cloud.nacos.discovery.metadata.version=dev配置指定,也可以直接在nacos服务列表中添加元数据信息。实现自定义的负载均衡规则,负载均衡器可以通过这些规则找到我们需要的服务实例。请求完header信息后,去服务实例中找到配置了mtadata.version=dev的服务实例。SpringCloudLoadBalancer(SCL)SCL负载均衡策略在SpringCloudLoadBalancer官方文档中有描述:SpringCloudprovidesitsownclient-sideload-balancerabstractionandimplementation。对于负载均衡机制,添加了ReactiveLoadBalancer接口,并为其提供了基于Round-Robin和Random的实现。为了让实例从反应中选择,使用了ServiceInstanceListSupplier。目前我们支持基于服务发现的ServiceInstanceListSupplier实现,它使用类路径中可用的客户端从服务发现中检索可用实例。结合文档中的其他内容,提取出几条关键信息:SpringCloudLoadBalancer提供了两种负载均衡算法:Round-Robin-based和Random。默认情况下,Round-Robin-based可以通过实现ServiceInstanceListSupplier来过滤满足要求的服务实例,需要使用LoadBalancerClient注解来指定服务级别的负载均衡策略和实例选择策略提示:如果需要探索SCL的实现原理,可以从GatewayReactiveLoadBalancerClientAutoConfiguration说起。自定义灰度发布结合以上,我们有两种使用SpringCloudLoadBalancer实现灰度的方式:简单粗暴,直接实现一个新的负载均衡策略,然后通过LoadBalancerClient注解指定使用该策略的服务实例。自定义服务实例过滤逻辑,在返回前端实例时过滤掉符合要求的服务实例。当然,你还需要通过LoadBalancerClient注解来指定使用这个选择器的服务实例。代码实现版本说明SpringCloud项目使用的版本是SpringCloudalibaba推荐的毕业版本2.4.22021.12020.0.0自定义负载均衡策略首先我们来看第一种实现方式,通过自定义负载均衡策略来实现。网关模块引入了SCL,需要去掉nacos注册中心自带的Ribbon负载均衡器。com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discoveryorg.springframework.cloudspring-cloud-starter-netflix-ribbonorg.springframework.cloud弹簧云-loadbalancer自定义负载均衡策略VersionGrayLoadBalancer/***说明:*自定义灰度*通过在请求头中添加Version和ServiceInstance元数据属性进行对比*@authorJam*@date2021/6/117:26*/@Log4j2publicclassVersionGrayLoadBalancerimplementsReactorServiceInstanceLoadBalancer{privatefinalObjectProviderserviceInstanceListSupplierProvider;privatefinalStringserviceId;privatefinalAtomicIntegerposition;publicVersionGrayLoadBalancer(ObjectProviderserviceInstanceListSupplierProvider,StringserviceId){this(serviceInstanceListSupplierProvider,serviceId,newRandom().nextInt(1000));}publicVersionGrayLoadBalancer(ObjectProviderserviceInstanceListSupplierProvider,StringserviceId,intseedPosition){this.serviceId=serviceId;this.serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider;这.position=newAtomicInteger(seedPosition);}@OverridepublicMono>选择(Requestrequest){ServiceInstanceListSuppliersupplier=this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);returnsupplier.get(request).next().map(serviceInstances->processInstanceResponse(serviceInstances,request));}privateResponseprocessInstanceResponse(Listinstances,Requestrequest){if(instances.isEmpty()){log.warn("Noserversavailableforservice:"+this.serviceId);returnnewEmptyResponse();}else{DefaultRequestContextrequestContext=(DefaultRequestContext)request.getContext();RequestDataclientRequest=(RequestData)requestContext.getClientRequest();HttpHeadersheaders=clientRequest.getHeaders();//getRequestHeaderStringreqVersion=headers.getFirst("version");if(StringUtils.isEmpty(reqVersion)){returnprocessRibbonInstanceResponse(instances);}log.info("requestheaderversion:{}",reqVersion);//filterserviceinstancesListserviceInstances=instances.stream().filter(instance)->reqVersion.equals(instance.getMetadata().get("version"))).collect(Collectors.toList());if(serviceInstances.size()>0){returnprocessRibbonInstanceResponse(serviceInstances);}else{returnprocessRibbonInstanceResponse(实例);}}}/***负载均衡器*参考org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#getInstanceResponse*@authorjavadaily*/privateResponseprocessRibbonInstanceResponse(Listinstances){intpos=Math.abs(this.position.incrementAndGet());ServiceInstanceinstance=instances.get(pos%instances.size());returnnewDefaultResponse(instance);}}获取请求头版本属性,然后根据服务实例元数据中的版本属性进行匹配。符合条件的实例参考基于Round-Robin的实现方式编写配置类VersionLoadBalancerConfiguration,用于替换默认的负载均衡算法/***说明:*自定义负载均衡器配置实例*@authorjavadaily*@date2021/6/316:02*/publicclassVersionLoadBalancerConfiguration{@BeanReactorLoadBalancerversionGrayLoadBalancer(Environmentenvironment,LoadBalancerClientFactoryloadBalancerClientFactory){Stringname=environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);returnnewVersionGrayLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,ServiceInstanceListSupplier.class),name);}}VersionLoadBalancerConfiguration配置类不能添加@Configuration注解。在网关启动类中使用注解@LoadBalancerClient通过@LoadBalancerClient(value="auth-service",configuration=VersionLoadBalancerConfiguration.class)指定哪些服务使用自定义负载均衡算法,并为auth-service启用自定义负载均衡算法;或通过@LoadBalancerClients(defaultConfiguration=VersionLoadBalancerConfiguration.class)为所有服务启用自定义负载平衡算法。自定义服务实例过滤逻辑接下来我们来看第二种实现方式。通过实现ServiceInstanceListSupplier来自定义服务过滤逻辑,我们可以直接继承DelegatingServiceInstanceListSupplier来实现。在网关模块引入SpringCloudLoadBalancer(同上)自定义服务实例筛选逻辑VersionServiceInstanceListSupplier/***自定义服务实例筛选逻辑*@authorjavadaily*参考:org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier*/@Log4j2publicclassVersionServiceInstanceListSupplierextendsDelegatingServiceInstanceListSupplier{publicVersionServiceInstanceListSupplier(ServiceInstanceListSupplierdelegate){super(delegate);}@OverridepublicFlux>get(){returndelegate.get();}@OverridepublicFlux>get(Requestrequest){returndelegate.get(request).map(instances->filteredByVersion(instances,getVersion(request.getContext())));}/***filterinstancebyrequestVersion*@authorjavadaily*/privateListfilteredByVersion(Listinstances,StringrequestVersion){log.info("requestversionis{}",requestVersion);if(StringUtils.isEmpty(requestVersion)){returninstances;}ListfilteredInstances=instances.stream().filter(instance->requestVersion.equalsIgnoreCase(instance.getMetadata().getOrDefault("version",""))).collect(Collectors.toList());if(filteredInstances.size()>0){returnfilteredInstances;}returninstances;}privateStringgetVersion(ObjectrequestContext){if(requestContext==null){returnnull;}Stringversion=null;if(requestContextinstanceofRequestDataContext){version=getVersionFromHeader((RequestDataContext)requestversionContext);}retur;}/***getversionfromheader*@authorjavadaily*/privateStringgetVersionFromHeader(RequestDataContextcontext){if(context.getClientRequest()!=null){HttpHeadersheaders=context.getClientRequest().getHeaders();if(headers!=null){//couldextracttothepropertiesreturnheaders.getFirst("version");}}returnnull;}}实现原理同自定义负载均衡策略,根据版本匹配满足要求的服务实例编写配置类VersionServiceInstanceListSupplierConfiguration,用于替换默认服务实例筛选逻辑publicclassVersionServiceInstanceListSupplierConfiguration{@BeanServiceInstanceListSupplierserviceInstanceListSupplier(ConfigurableApplicationContextcontext){ServiceInstanceListSupplierdelegate=ServiceInstanceListSupplier.builder().withDiscoveryClient().withCaching().build(context);returnnewVersionServiceInstanceListSupplier(delegate);}}在网关启动类使用注解@LoadBalancerClient通过@LoadBalancerClient(value="auth-service",configuration=VersionServiceInstanceListSupplierConfiguration.class)指定哪些服务使用自定义负载均衡算法,并为auth-service启用自定义负载均衡算法;或通过@LoadBalancerClients(defaultConfiguration=VersionServiceInstanceListSupplierConfiguration.class)为所有服务启用自定义负载平衡算法。测试启动多个AccountService实例,在58302端口为实例配置metadataversion=devpostman,调用接口时指定请求头,通过debug模式观察两种实现逻辑,观察结果是否符合预期。小结本文基于SCL,通过扩展负载均衡算法,修改服务实例筛选逻辑,实现了一个简单的灰度发布功能。可以参考这个实现扩展的SCL负载均衡算法或者定制自己的服务筛选逻辑。