自定义Filter引起的 Failed to invoke the method 问题解决
一:问题的表现和最终结论
1 问题的表现:
出问题的项目,使用的分布式服务框架是dubbo。正常时启动项目需要启动多个服务,这里暂时定义为 server-task,server-application,server-pay 三个服务。在不知道哪一次的代码迭代后,出现了问题。服务在启动时都很顺利,没有明显报错,但是 server-application,server-pay 这两个服务都无法正常调用 server-task 服务下的 TaskRpcService的方法,且报出如下错误:
Internal server error: Failed to invoke the method getTaskInfo in the service com.test.server-task.ITaskRpcService. No provider available for the service com.test.server-task.ITaskRpcService from registry 127.0.0.1:2181 on the consumer xxx.xxx.x.xxx using the dubbo version 2.8.4. Please check if the providers have been started and registered.
乍一看,以为是 server-task 挂了或者服务启动过程有问题。在此假设的基础上,尝试重启task服务,发现问题确实解决了,调用server-task 服务下的 TaskRpcService 的方法时,不再报错。
然而,一旦其他两个服务有任意一个重启,或所有服务重启,问题会重新重现,仍然无法正常调用 server-task 服务下的 TaskRpcService 的方法,此时,又需要再次重启 server-task服务。每次项目有改动,需要启动服务时,都要对 server-task服务进行单独照顾,很麻烦。。。。同时,一直不处理,也担心会有其他的隐患。虽然最终服务还是可以正常跑起来,但是作为技术人(小白),不整的通透,怎么会进步嘞?
所以,在本地跑服务,开始了改BUG的道路。。。。
2 最终结论:
在我们项目中有一个自定义的Filter类,这里命名为 CustomFilter 类,实现了dubbo 的Fitler接口,dubbo服务会在启动时执行RegistryDirectory中的refreshInvoker方法,构建一个Filter链,同时为消费者所调用的服务创建一个 methodInvokerMap,这个 methodInvokerMap 会在消费者调用服务提供者时,生成invoker。正是构建Filter链时,这个CustomFilter 类中成员变量使得CustomFilter无法初始化导致链构建失败,methodInvokerMap为null,导致报 Failed to invoke the method 的错误。
二:解决过程详述
1 看日志
出了问题,第一反应便是查看日志。关键性的报错内容如下:
com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.checkInvokers(AbstractClusterInvoker.java:251)
com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:55)
com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:227)
com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72)
com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52)
2 问题溯源
根据日志中的报错信息,首先找到 checkInvokers 方法,如下所示:
invokers==null 或者 invokers.size() == 0,导致出现了日志中的报错信息。根据日志中的信息,继续追查上一级FailoverClusterInvoker.doInvoker。FailoverClusterInvoker.doInvoker如下所示:
在FailoverClusterInvoker的doInvoker方法中,invokers仍然是外部传入的。继续追查上一级AbstractClusterInvoker.invoke。AbstractClusterInvoker.invoke如下:
可以看出invokers由invocation经list()方法产生。
dubbo中 invocation 和 invoker 是什么?
这里简单介绍一下invocation和invoker的定义:
Invocation:是会话域,它持有调用过程中的变量,比如方法名,参数等。其接口如下:
Invoker:是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
继续往list()方法中看:
可以看出有两处影响了invokers,分别为doList(invocation)和router.route()方法。对于router的作用,简单查阅了大佬们的文章,具体如下图:
看Router的介绍,可以分析出invokers大概率是在doList()时为 null 或 size为0 。
继续跟进doList(),并进入其子类:RegistryDirectory。
可以看出,invokers是从methodInvokerMap中获得。
那么是否是因为methodInvokerMap的原因导致的呢?
跑DEBUG验证一下:
果然是 methodInvokerMap==null 搞的鬼。
正常情况下methodInvokerMap是这样的:
到这里,可确定是TaskRpcService(开头出问题的service)的methodInvokerMap为null,导致问题出现。
那么methodInvokerMap什么时候生成呢?
作为一个不懂dubbo奥秘的小白,就通过以下内容断定 methodInvokerMap 的生成方法:
- 百度了methodInvokerMap,有如下说法:
这个map存储的key是dubbo方法名,value是提供者的访问方式。methodInvokerMap是通过refreshInvoker方法生成的。 - 又进行了一番百度,得知,methodInvokerMap所在的类–>RegistryDirectory,有如下作用:
- 获取 invoker 列表
- 监听注册中心的变化
- 刷新 invokers。
a. 获取 invoker 列表,RegistryDirectory 实现的父类抽象方法 doList,其目的就是得到 invoker 列表,而其内部的实现主要是做了层方法名的过滤,通过方法名找到对应的 invokers。(这里的doList就是我们追溯问题时遇到的那个)
b. 监听注册中心的变化,通过实现 NotifyListener 接口能感知到注册中心的数据变更,这其实是在服务引入的时候就订阅的。RegistryDirectory 定义了三种集合,分别是 invokerUrls 、routerUrls 、configuratorUrls 分别处理相应的配置变化,然后对应转化成对象。)
c. 刷新 Invoker 列表,其实就是根据监听变更的 invokerUrls 做一波操作,其方法名为refreshInvoker(invokerUrls) ,会根据配置更新 invokers。(监听到注册中心变化后,对第二条作用中提到的invokerUrls进行操作)
- 在RegistryDirectory类中搜索methodInvokerMap,查看到底是在哪里赋过值。
通过以上3条证据,可以肯定,有且仅有 refreshInvoker 方法会为methodInvokerMap赋值。
3 问题定位
通过反复的启动项目进行debug,发现问题在执行toInvokers()方法后 newUrlInvokerMap == null ,其原因为 toInvokers 内部执行时抛出异常,具体如下:
这里,可以确定问题出在 protocol.refer() 方法。
查看protocol接口,根据其SPI注解,可以找到实现类 DubboProtocol:
跟进====>
这里没有直接进入DubboProtocol,而是先进入了ProtocolFIiterWrapper的refer()方法,如下图所示:
然后进入DubboProtocol.refer()方法,执行完后,执行ProtocolFIiterWrapper的buildInvokerChain()方法。
这里,需要进行说明:
Protocol存在两个wrapper类,分别为:
1 com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper、
2 com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper。
在dubbo中存在wrapper类的类会被wrapper实例包装后返回,因此在protocol.refer方法调用的时候,会先经过wrapper类。
ListenerInvokerWrapper依然不是真正的调用者,它主要是监听了invoker的创建与销毁事件,它维护的invoker为经过ProtocolFilterWrapper转换过的Invoker,该Invoker在执行前需要先经过filter链的处理。
在上图中,buildInvokerChain()方法,看字面意思,就是创建一个链。
继续跟进----->
最终,方法执行到上图中红框位置边报错,错误逐层抛出,最终导致了调用远程服务时报错。
4 错误分析,解决问题
变量 EXTENSION_INSTANCES 为一个 ConcurrentHashMap,由其源码和报错位置的debug截图可知,报错是clazz.newInstance()为null。即 CustomerFilter 无法在dubbo启动,调用refreshInvoker方法 为 ITaskRpcService 进行相关操作时,进行初始化。
但是这里有个问题,clazz是个Class类,在报错之前已经被拿到了,说明 CustomerFilter 已经被加载,但是被加载的类不能初始化,why?看看代码再说。CustomFilter 的代码简化版如下:
类在初始化时,对内部代码的加载顺序为:
CustomerFilter 的成员变量是由dubbo提供的 ServiceBean 创建的。那么,是否可能是 ServiceBean 还没有被加载或初始化,导致成员变量初始化失败,最终使得 CustomerFilter 初始化失败。
将上图中三个成员变量注释掉,重新启动服务,没报错,可以正常调用 server-task 服务。
成了!
三:总结
1 知识点汇总
1.1 正常的dubbo消费者调用provider的服务,其流程如何?
dubbo中的消费者去调用服务提供者时,其大致流程如下所示:
因为问题发生在消费者调用server-task服务时,由刚刚展示的源码可以看到invoker并没有生成,而是为null,那么问题发生的阶段很可能就在上图中protocol->Invoker阶段。后续对于问题的分析理解,也是验证了这个推测。
1.2 dubbo中invocation和invoker的大致定义;
1.3 dubbo消费者调用服务部分流程;
1.4 dubbo中 RegistryDirectory 的作用概述;
1.5 dubbo中ProtocolFIiterWrapper;
1.6 dubbo中 SPI 机制;
1.7 JAVA类初始化时内部加载顺序;
2 心得
对于一个dubbo才接触2个月的小菜鸡,在团队的帮助下最终解决了这么一个技术性问题,实在是很开心,哈哈哈~