当前位置: 首页 > 科技观察

生产失败|Dubbo泛化调用引发“血案”

时间:2023-03-20 23:29:26 科技观察

1.背景上个月,公司的zk集群出现故障,要求所有项目组自查是否使用了Dubbo程序化/泛化调用,强制使用@ReferenceGenerateConsumer。平台部门给出的失败原因:泛化调用时,provider没有启动,导致每次请求都会在zk中创建consumer节点,导致短时间内大量访问zk一段时间内,创建了240万+个节点,导致zk的所有节点接连崩溃,导致多个应用因为无法连接zk而报错。原因是泛化调用时,provider没有启动,导致每次请求都会在zk中创建consumer节点。由于不是我负责的项目,为了找出背后的原因,我们进行实验,探究失败的深层次原因。2.验证2.1泛化不使用缓存测试代码如下:publicResultgetProductGenericCache(ProductDTOdto){ApplicationConfigapplication=newApplicationConfig();application.setName("dubbo-demo-client-consumer-generic");//连接到注册表配置RegistryConfigregistry=newRegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");//服务消费者缺少省值配置ConsumerConfigconsumer=newConsumerConfig();consumer.setTimeout(5000);消费者.setRetries(0);reference.setApplication(应用程序);reference.setRegistry(注册表);reference.setConsumer(消费者);reference.setInterface(com.demo.dubbo.api.ProductService.class);//弱类型接口名//reference.setVersion("");//reference.setGroup("");reference.setGeneric(true);//声明为通用服务svc=reference.get();对象目标=svc.$invoke("findProduct",newString[]{ProductDTO.class.getName()},newObject[]{dto});returnResult.success((Map)target);}由于没有缓存引用,每次请求该方法时,都会在zk中创建一个消费者节点(不管provider是否启动),当请求量为大,所有的zk节点都会相继崩溃。如果泛化不使用缓存,请求量大时会创建大量的zk节点。2.2通用缓存测试代码如下:@OverridepublicResultgetProductGenericCache(ProductDTOdto){ReferenceConfigCachereferenceCache=ReferenceConfigCache.getCache();ReferenceConfigreference=newReferenceConfig();//缓存,否则每次请求都会创建一个ReferenceConfig并在zk中注册节点,最终可能导致zk节点过多影响性能ApplicationConfigapplication=newApplicationConfig();application.setName("pangu-client-consumer-generic");//连接注册中心ConfigureRegistryConfigregistry=newRegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");//服务消费者默认配置ConsumerConfigconsumer=newConsumerConfig();consumer.setTimeout(5000);消费者.setRetries(0);reference.setApplication(应用程序);reference.setRegistry(注册表);reference.setConsumer(消费者);reference.setInterface(com.demo.dubbo.api.ProductService.class);//弱类型接口名//reference.setVersion("");//reference.setGroup("");参考ence.setGeneric(真);//声明为通用接口GenericServicesvc=referenceCache.get(reference);//引用对象会缓存在cache.get方法中,调用ReferenceConfig.get方法启动ReferenceConfig对象target=svc.$invoke("findProduct",newString[]{ProductDTO.class.getName()},newObject[]{dto});returnResult.success((Map)target);}经测试,如果使用缓存,无论provider终端是否启动,zk中只会创建一个consumer节点2.3将服务检查设置为true并设置check=true,测试代码如下:@OverridepublicResultgetProductGenericCache(ProductDTOdto){ReferenceConfigCachereferenceCache=ReferenceConfigCache.getCache();ReferenceConfigreference=newReferenceConfig();//缓存,否则每次请求都会创建一个ReferenceConfig并在zk中注册节点,最终可能导致zk节点过多影响性能ApplicationConfigapplication=newApplicationConfig();application.setName("pangu-client-consumer-generic");//连接到注册表配置RegistryConfigregistry=newRegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");//服务消费者默认值配置ConsumerConfigconsumer=newConsumerConfig();消费者.setTimeout(5000);消费者.setRetries(0);reference.setApplication(应用程序);reference.setRegistry(注册表);reference.setConsumer(消费者);reference.setCheck(true);//测试3,设置检测服务存活reference.setInterface(org.pangu.api.ProductService.class);//弱类型接口名//reference.setVersion("");//reference.setGroup("");reference.setGeneric(true);//声明为通用接口GenericServicesvc=referenceCache.get(reference);//cache.get方法会缓存ReferenceObject,并调用ReferenceConfig.get方法启动ReferenceConfigObjecttarget=svc.$invoke("findProduct",newString[]{ProductDTO.class.getName()},newObject[]{dto});//在实际网关中,将方法名、参数类型、参数作为参数传递,返回Result.success((Map)target);}情况一:启动provider服务,然后启动consumer端的泛化,请求这个泛化方法,只在zk中注册一个consumer节点;停止provider,然后请求这个泛化方法,发现zk上的节点数没有变化。为什么?provider停止后,request不再创建zk节点的原因是RegistryConfig的ref在启动时已经生成了proxy(由于provider服务在启动时存在,check=true已经通过验证),所以它不再创建。情况二:不启动provider服务,直接启动consumer端泛化,请求这种泛化方式,发现每次请求都会在zk中创建一个consumer节点。至此,故障得到验证。那么既然如此,为什么每次请求都要在zk中创建一个消费者节点呢?根本原因是什么?privateTcreateProxy(Mapmap){//忽略其他代码if(isJvmRefer){//忽略其他代码}else{if(url!=null&&url.length()>0){//忽略othercodes}else{//从注册中心的配置Listus=loadRegistries(false);//code@1if(us!=null&&!us.isEmpty()){for(URLu:us){网址monitorUrl=loadMonitor(u);if(monitorUrl!=null){map.put(Constants.MONITOR_KEY,URL.encode(monitorUrl.toFullString()));}urls.add(u.addParameterAndEncoded(Constants.REFER_KEY,StringUtils.toQueryString(map)));//code@2}}if(urls.isEmpty()){thrownewIllegalStateException("Nosuchanyregistrytoreference"+interfaceName+"ontheconsumer"+NetUtils.getLocalHost()+"使用dubbo版本"+Version.getVersion()+",请将配置到你的spring配置中。");}}if(urls.size()==1){invoker=refprotocol.refer(interfaceClass,urls.get(0));//代码@3}else{List>invokers=newArrayList<调用者>();网址registryURL=null;for(URLurl:urls){//代码@4invokers.add(refprotocol.refer(interfaceClass,url));如果(Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())){registryURL=url;//使用最后一个注册表url}}if(registryURL!=null){//注册表url可用//只有当寄存器的集群可用时才使用AvailableClusterURLu=registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY,AvailableCluster.NAME);invoker=cluster.join(newStaticDirectory(u,invokers));}else{//不是注册表urlinvoker=cluster.join(newStaticDirectory(invokers));}}}布尔值c=检查;if(c==null&&consumer!=null){c=consumer.isCheck();}if(c==null){c=true;//defaulttrue}if(c&&!invoker.isAvailable()){//check=true,提供者服务不存在,抛出异常//如果提供者暂时不可用,消费者可以稍后重试initialized=false;thrownewIllegalStateException("无法检查服务的状态"+interfaceName+"。没有可用于该服务的提供者"+(group==null?"":group+"/")+interfaceName+(version==null?"":":"+version)+"从url"+invoker.getUrl()+"到消费者"+NetUtils.getLocalHost()+"使用dubbo版本"+Version.getVersion());}如果(记录器.isInfoEnabled()){logger.info("引用dubbo服务"+interfaceClass.getName()+"fromurl"+invoker.getUrl());}//创建服务代理return(T)proxyFactory.getProxy(invoker);}第一次请求泛化方法时,由于ReferenceConfig的ref为null,执行createProxy,执行代码@1,@2,@3,并在zk中创建消费者节点,但是因为check=true,抛出IllegalStateException,最后ReferenceConfig的ref还是null第二次请求泛化方法,因为ReferenceConfig已经被缓存了,所以这次ReferenceConfig对象是第一个ReferenceConfig对象,获取ReferenceConfig的代理对象ref,因为ReferenceConfig的ref为null,所以执行createProxy,执行代码@1、@2、@4,在zk中创建consumer节点,但是因为check=true,抛出IllegalStateException,最后ReferenceConfig的ref还是null。第三个请求和后续请求与第二个请求具有相同的效果。为什么每次都在zk中创建一个消费者节点?只能说明订阅url不一样。如果url相同,则不会在zk中创建。那么一个服务的订阅url的构成有什么区别呢?查看ReferenceConfig.init(),发现订阅url上有一个时间戳,就是当前的时间戳。这也解释了为什么每次都要注册,因为订阅url不一样。如下图所示:订阅URL加上这个时间戳是不是不合理?查看官网,订阅URL中的时间戳在2.7.5版本已经去掉,只订阅一次一个URL。3.故障结论因为使用了广义调用,但是没有启动initiator,并且check等于true,所以每次调用都会尝试注册,但是在dubbo2.7.5之前,注册的url是有时间戳的,导致每次请求在zk上一次创建一个节点,导致节点数量众多,最终导致zk崩溃。