写点什么

Dubbo 泛化调用引发的“血案”

作者:Java-fenn
  • 2022 年 9 月 25 日
    湖南
  • 本文字数:4672 字

    阅读完需:约 15 分钟

1、背景上个月公司 ZooKeeper 集群发生了一次故障,要求所有项目组自检有无使用 Dubbo 编程式或者泛化调用,强制使用 @Reference 生成 Consumer。


平台部给出的故障原因:


泛化调用时候,provider 没启动,导致每次请求都在 ZooKeeper 创建消费节点,导致在短时间大量访问 ZooKeeper 并创建了 240 万+ 的节点,导致 ZooKeeper 所有节点陆续崩溃导致,多个应用因无法连接到 ZooKeeper 报错。


原因是听说泛化调用时候,provider 没启动,导致每次请求都在 ZooKeeper 创建消费节点。


由于并不是自己负责的项目,为了弄清楚背后的原因,通过进行实验来探究该故障的深层次原因。


2、求证 2.1 泛化不使用缓存测试代码如下:


public Result<Map> getProductGenericCache(ProductDTO dto) {ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();ApplicationConfig application = new ApplicationConfig();application.setName("dubbo-demo-client-consumer-generic");// 连接注册中心配置 RegistryConfig registry = new RegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");// 服务消费者缺省值配置 ConsumerConfig consumer = new ConsumerConfig();consumer.setTimeout(5000);consumer.setRetries(0);


reference.setApplication(application);reference.setRegistry(registry);reference.setConsumer(consumer);reference.setInterface(com.demo.dubbo.api.ProductService.class); // 弱类型接口名//        reference.setVersion("");//        reference.setGroup("");reference.setGeneric(true); // 声明为泛化接口GenericService svc = reference.get();Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});return Result.success((Map)target);
复制代码


}由于没有缓存 reference,因此每次请求这个方法,就会在 ZooKeeper 创建个消费节点(无论 provider 是否启动)。请求量大的时候,就会导致 ZooKeeper 所有节点陆续崩溃。


如果泛化不使用缓存,请求量大时会创建大量 ZooKeeper 节点。


2.2 泛化使用缓存测试代码如下:


@Overridepublic Result<Map> getProductGenericCache(ProductDTO dto) {ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();


// 使用缓存,否则每次请求都会创建一个 ReferenceConfig// 并在 ZooKeeper 注册节点,最终可能导致 ZooKeeper 节点过多影响性能ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();ApplicationConfig application = new ApplicationConfig();application.setName("pangu-client-consumer-generic");// 连接注册中心配置RegistryConfig registry = new RegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");

// 服务消费者缺省值配置ConsumerConfig consumer = new ConsumerConfig();consumer.setTimeout(5000);consumer.setRetries(0);

reference.setApplication(application);reference.setRegistry(registry);reference.setConsumer(consumer);reference.setInterface(com.demo.dubbo.api.ProductService.class); // 弱类型接口名// reference.setVersion("");// reference.setGroup("");reference.setGeneric(true); // 声明为泛化接口// cache.get方法中会缓存 Reference对象,// 并且调用 ReferenceConfig.get 方法启动 ReferenceConfigGenericService svc = referenceCache.get(reference);Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});return Result.success((Map)target);
复制代码


}经过测试,如果使用缓存,无论 provider 端无论是否启动,都只会在 ZooKeeper 创建一个消费节点。


2.3 设置服务检查为 true 设置 check=true,测试代码如下:


@Overridepublic Result<Map> getProductGenericCache(ProductDTO dto) {ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();


// 使用缓存,否则每次请求都会创建一个 ReferenceConfig// 并在 ZooKeeper 注册节点,最终可能导致 ZooKeeper 节点过多影响性能ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();ApplicationConfig application = new ApplicationConfig();application.setName("pangu-client-consumer-generic");// 连接注册中心配置RegistryConfig registry = new RegistryConfig();registry.setAddress("zookeeper://127.0.0.1:2181");

// 服务消费者缺省值配置ConsumerConfig consumer = new ConsumerConfig();consumer.setTimeout(5000);consumer.setRetries(0);

reference.setApplication(application);reference.setRegistry(registry);reference.setConsumer(consumer);reference.setCheck(true);//试验3,设置检测服务存活reference.setInterface(org.pangu.api.ProductService.class); // 弱类型接口名// reference.setVersion("");// reference.setGroup("");reference.setGeneric(true); // 声明为泛化接口// cache.get方法中会缓存 Reference对象,// 并且调用 ReferenceConfig.get 方法启动 ReferenceConfigGenericService svc = referenceCache.get(reference);// 实际网关中,方法名、参数类型、参数是作为参数传入Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});return Result.success((Map)target);
复制代码


}情况一启动 provider 服务,然后启动消费端泛化,请求此泛化方法时,在 ZooKeeper 只注册了一个 consumer 节点;停止 provider,再请求此泛化方法,发现 ZooKeeper 上此节点数量不变化。


这是为什么呢?


provider 停止后,请求不再创建 ZooKeeper 节点的原因是 RegistryConfig 的 ref 已经在启动时候生成了代理(由于启动时候 provider 服务存在,check=true 校验过通过),因此不再创建。


情况二不启动 provider 服务,直接启动消费端泛化,请求此泛化方法,发现每请求一次,在 ZooKeeper 就会创建一个消费节点。至此验证到故障。


那么这种情况,为什么会每次请求都在 ZooKeeper 创建消费节点呢?根本原因是什么?private T createProxy(Map<String, String> map) {//忽略其它代码


if (isJvmRefer) {//忽略其它代码} else {    if (url != null && url.length() > 0) {         //忽略其它代码    } else { // assemble URL from register center's configuration        List<URL> us = loadRegistries(false); //代码@1        if (us != null && !us.isEmpty()) {            for (URL u : us) {                URL 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)));//代码@2            }        }        if (urls.isEmpty()) {            throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");        }    }

if (urls.size() == 1) { invoker = refprotocol.refer(interfaceClass, urls.get(0));//代码@3 } else { List<Invoker<?>> invokers = new ArrayList<Invoker<?>>(); URL registryURL = null; for (URL url : urls) {//代码@4 invokers.add(refprotocol.refer(interfaceClass, url)); if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) { registryURL = url; // use last registry url } } if (registryURL != null) { // registry url is available // use AvailableCluster only when register's cluster is available URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME); invoker = cluster.join(new StaticDirectory(u, invokers)); } else { // not a registry url invoker = cluster.join(new StaticDirectory(invokers)); } }}

Boolean c = check;if (c == null && consumer != null) { c = consumer.isCheck();}if (c == null) { c = true; // default true}if (c && !invoker.isAvailable()) {//check=true,provider服务不存在,抛出异常 // make it possible for consumer to retry later if provider is temporarily unavailable initialized = false; throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());}if (logger.isInfoEnabled()) { logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());}// create service proxyreturn (T) proxyFactory.getProxy(invoker);
复制代码


}首次请求泛化方法,由于 ReferenceConfig 的 ref 为 null,因此执行 createProxy,执行代码 @1、@2、@3,在 ZooKeeper 创建消费节点,但是由于 check=true,因此抛出 IllegalStateException 异常,最终 ReferenceConfig 的 ref 依然为 null。


第二次请求泛化方法,由于 ReferenceConfig 已经被缓存,这次的 ReferenceConfig 对象就是首次的 ReferenceConfig 对象,获取 ReferenceConfig 的代理对象 ref,由于 ReferenceConfig 的 ref 为 null,因此执行 createProxy,执行代码 @1、@2、@4,在 ZooKeeper 创建消费节点,但是由于 check=true,因此抛出 IllegalStateException 异常,最终 ReferenceConfig 的 ref 依然为 null。


第三次以及后续的请求,都和第二次请求是一样效果。


为什么每次在 ZooKeeper 都创建消费节点,只能说明订阅 URL 不同导致的。如果 UR L 相同,在 ZooKeeper 是不会创建的。那么订阅 UR L 的组成对一个服务来说有哪些不同呢?


查看 ReferenceConfig.init(),发现订阅 UR L 上有 timestamp,是当前时间戳,这也说明了为什么每次都去注册,因为订阅 UR L 不同,如下图:


订阅 UR L 上加上这个 timestamp 是否有些不合理呢?


查看官方文档,在 2.7.5 版本中已经将订阅的 URL 中的 timestamp 去掉了,只会对一个 URL 订阅一次。


由于使用了泛化调用,但启动者没有启动,而且使用了 check 等于 true,每次调用都会尝试去注册。但在 Dubbo 2.7.5 之前,注册的 URL 带了时间戳,导致每请求一次就在 ZooKeeper 上创建一个节点,导致产生大量节点,最终导致 ZooKeeper 崩掉。

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
Dubbo 泛化调用引发的“血案”_Java_Java-fenn_InfoQ写作社区