生产故障|Dubbo泛化调用引发的“血案”

简介: 生产故障|Dubbo泛化调用引发的“血案”

1、背景


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


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


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


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


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,因此每次请求这个方法,就会在zk创建个消费节点(无论provider是否启动),请求量大的时候,就会导致zk所有节点陆续崩溃。


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


2.2 泛化使用缓存


测试代码如下:

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
    ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();
    ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//缓存,否则每次请求都会创建一个ReferenceConfig,并在zk注册节点,最终可能导致zk节点过多影响性能
    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); // 声明为泛化接口
    GenericService svc = referenceCache.get(reference);//cache.get方法中会缓存 Reference对象,并且调用ReferenceConfig.get方法启动ReferenceConfig
    Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});
    return Result.success((Map)target);
}

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


2.3 设置服务检查为true


设置check=true,测试代码如下:

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
    ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();
    ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//缓存,否则每次请求都会创建一个ReferenceConfig,并在zk注册节点,最终可能导致zk节点过多影响性能
    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); // 声明为泛化接口
    GenericService svc = referenceCache.get(reference);//cache.get方法中会缓存 Reference对象,并且调用ReferenceConfig.get方法启动ReferenceConfig
    Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//实际网关中,方法名、参数类型、参数是作为参数传入
    return Result.success((Map)target);
}

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


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


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

c38de4df46870a1f4a329401cbeccb8d.png

那么这种情况,为什么会每次请求都在zk创建消费节点呢?根本原因是什么?

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 proxy
    return (T) proxyFactory.getProxy(invoker);
}

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


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


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


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


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

f77e39060e48fddf3108448348bb80a1.png

605596079bae7d154759c83a1ae0c959.png

订阅url上加上这个timestamp是否有些不合理呢?经过查看官方,在2.7.5版本中已经将订阅的URL中的timestamp去掉了,只会对一个URL订阅一次。


3、故障结论


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


相关文章
|
6月前
|
设计模式 JSON Dubbo
超越接口:探索Dubbo的泛化调用机制
超越接口:探索Dubbo的泛化调用机制
347 0
|
6月前
|
Dubbo Java 应用服务中间件
微服务框架(四)Dubbo泛化调用实现
  此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为服务治理框架Dubbo泛化调用的实现
|
Dubbo 应用服务中间件 测试技术
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(1)
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(1)
112 5
|
JSON Dubbo Java
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(3)
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(3)
121 3
|
Dubbo 应用服务中间件 Apache
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(4)
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(4)
86 4
|
XML Dubbo Java
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(2)
带你读《Apache Dubbo微服务开发从入门到精通》——八、 泛化调用(2)
130 5
|
存储 运维 监控
Dubbo + ZooKeeper丨如何解决线上故障排查链路长的难题
MSE ZooKeeper 最新提供 Dubbo 服务管理能力,同时结合 TopN 监控大盘,推送轨迹等自治能力,帮助用户提高问题排查速度,集群运维效率。
Dubbo + ZooKeeper丨如何解决线上故障排查链路长的难题
|
Dubbo 应用服务中间件
Dubbo泛化
Dubbo泛化是一种基于Dubbo协议进行远程服务调用的方式,它可以实现不需要依赖服务接口实现类的服务调用。通俗地讲,泛化调用就是像调用本地方法一样,通过方法名和参数来调用远程服务,不需要编写服务接口和实现类。
117 0
|
编解码 负载均衡 监控
Dubbo调用流程学习总结
首先我们知道Dubbo是一个RPC框架,因此解决的问题是服务治理,这个治理是解决服务注册和调用列表的维护治理,产生注册中心维护服务列表和更新,同时方便远程调用和本地调用是一样的,同时方便解耦,我猜这个是dubbo框架产生的初衷吧。而服务的调用和服务的引用是采用网络编程框架Netty,由于其基于NIO,因此其具有很高的性能。同时因为服务的调用和服务的引用,与IM通信或者我们看到的Http请求三次握手是类似的,采用的是应答模式。
147 0
Dubbo调用流程学习总结
|
存储 缓存 监控
105. 注册中心宕掉后,Dubbo服务还能进行调用吗
105. 注册中心宕掉后,Dubbo服务还能进行调用吗
196 0