写点什么

抓到 Dubbo 异步调用的小 BUG,再送你一个贡献开源代码的机会

作者:捉虫大师
  • 2022 年 7 月 04 日
  • 本文字数:3771 字

    阅读完需:约 12 分钟

hello,大家好呀,我是小楼。


最近一个技术群有同学 at 我,问我是否熟悉 Dubbo,这我熟啊~



他说遇到了一个 Dubbo 异步调用的问题,怀疑是个 BUG,提到 BUG 我可就不困了,说不定可以水,哦不...写一篇文章。


问题复现

遇到问题,尤其不是自己遇到的,必须要复现出来才好排查,截一个当时的聊天记录:



他的问题原话是:


今天发现一个问题 有一个 dubbo 接口返回类型是 boolean, 把接口从同步改成异步 server 端返回 true 消费端却返回 false,把 boolean 改成 Boolean 就能正常返回结果 有碰到过这个问题吗


注意几个重点:


  • 接口返回类型是 boolean

  • 同步改为异步调用返回的 boolean 和预期不符合

  • boolean 基本类型改成包装类型 Boolean 就能正常返回


听到这个描述,我的第一反应是这个返回结果定义为 boolean 肯定有问题!


《Java 开发手册》中就强调了 RPC 接口返回最好不要使用基本类型,而要使用包装类型:



但这个是业务编码规范,如果 RPC 框架不能使用 boolean 作为返回值,岂不是个 BUG?而且他强调了是同步改为异步调用才出现这种情况,说明同步没问题,有可能是异步调用的锅。


于是我顺口问了 Dubbo 的版本,说不定是某个版本的 BUG。得到回复,是 2.7.4 版本的 Dubbo。


于是我拉了个工程准备复现这个问题。


哎,等等~


Dubbo 异步调用的写法可多了,于是我又问了下他是怎么写的。



知道怎么写的就好办了,写个 Demo 先:


  1. 定义 Dubbo 接口,一个返回 boolean,一个返回 Boolean


public interface DemoService {    boolean isUser();    Boolean isFood();}
复制代码


  1. 实现 Provider,为了简单,都返回 true,并且打了日志


@Servicepublic class DemoServiceImpl implements DemoService {
@Override public boolean isUser() { System.out.println("server is user : true"); return true; }
@Override public Boolean isFood() { System.out.println("server is food : true"); return true; }}
复制代码


  1. 实现 Consumer,为了方便调用,实现了一个 Controller,为了防止本机调用,injvm 设置为 false,这里是经验,injvm 调用逻辑和远程调用区别挺大,为了防止干扰,统一远程调用。


@RestControllerpublic class DemoCallerService {
@Reference(injvm = false, check = false) private DemoService demoService;
@GetMapping(path = "/isUser") public String isUser() throws Exception { BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1); RpcContext.getContext().asyncCall( () -> demoService.isUser() ).handle( (isUser, throwable) -> { System.out.println("client is user = " + isUser); q.add(isUser); return isUser; }); q.take(); return "ok"; }
@GetMapping(path = "/isFood") public String isFood() throws Exception { BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1); RpcContext.getContext().asyncCall( () -> demoService.isFood() ).handle( (isFood, throwable) -> { System.out.println("client is food = " + isFood); q.add(isFood); return isFood; }); q.take(); return "ok"; }}
复制代码


  1. 启动一个 Provider,再启动一个 Consumer 进行测试,果然和提问的同学表现一致:


  • 先调用isUser(返回 boolean),控制台打印:


// client ...client is user = false// server ...server is user : true
复制代码


  • 再调用isFood(返回 Boolean),控制台打印:


// client ...client is food = true// server ...server is food : true
复制代码

问题排查

  1. Debug


先猜测一下是哪里的问题,server 端返回 true,应该问题不大,可能是 client 端哪里转换出错了。但这都是猜想,我们直接从 client 端接受到的数据开始,如果接收的数据没问题,肯定就是后续处理出了点小差错。


如果你非常熟悉 Dubbo 的调用过程,直接知道大概在这里


com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived


如果你不熟悉,那就比较困难了,推荐读一下之前的文章《我是一个Dubbo数据包...》,知道得越多,干活就越快。


我们打 3 个断点:




  • 断点①为了证明我们的请求进来了

  • 断点②为了证明进了回调

  • 断点③为了能从接受到数据包的初始位置开始排查


按照我们的想法,执行顺序应该是①、③、②,但是这里很奇怪,并没有按照我们的预期执行,而是先执行①,再执行②,最后执行③!


这是为什么?对于排查问题中的这些没有符合预期的蛛丝马迹,要特别留心,很可能就是一个突破点


于是我们对asyncCall这个方法进行跟踪:



发现这里 callable 调用 call 返回了 false,然后 false 不为 null 且不是 CompletableFuture 的实例,于是直接调用了CompletableFuture.completedFuture(o)


看到这里估计有部分小伙伴发现了问题,正常情况下,Dubbo 的异步调用,执行调用后,不会立马得到结果,只会拿到一个 null 或者一个 CompletableFuture,然后在回调方法中等待 server 端的返回。


这里的逻辑是如果返回的结果不为 null 且不为 CompletableFuture 的实例就直接将 CompletableFuture 设置为完成,立马执行回调。


暂且不管这个逻辑。


我们先看为什么会返回 false。这里的 callable 是 Dubbo 生成的一个代理类,其实就是封装了调用 Provider 的逻辑,有没有办法看看他封装的逻辑呢?有!用 arthas。


  1. arthas


我们下载安装一个 arthas,可以参考如下文档:


https://arthas.aliyun.com/doc/quick-start.html


attach 到我们的 Consumer 进程上,执行sc命令(查看已加载的类)查看所有生成的代理类,由于我们的 Demo 就生成了一个,所以看起来很清晰


sc *.proxy0
复制代码



再使用jad命令反编译已加载的类:


jad org.apache.dubbo.common.bytecode.proxy0
复制代码



看到这里估计小伙伴们又揭开了一层疑惑,this.handler.invoke就是去调用 Provider,由于这里是异步调用,必然返回的是 null,所以返回值定义为 boolean 的方法返回了false


看到这里,估计小伙伴们对《Java 开发手册》里的规范有了更深的理解,这里的处理成 false 也是无奈之举,不然难道返回 true?属于信息丢失了,无法区分是调用的返回还是其他异常情况。


我们再回头看asyncCall



圈出来的这段代码令人深思,尤其是最后一行,为啥直接将 CompletableFuture 设置为完成?


从这个方法的名字能看出它是执行异步调用,但这里有行注释:


//local invoke will return directly
复制代码


首先这个注释的格式上下不一,//之后讲道理是需要一个空格的,我觉得这里提个 PR 改下代码格式肯定能被接受~


其次 local invoke,我理解应该是 injvm 这种调用,为啥要特殊处理?这个处理直接就导致了返回基本类型的接口在异步调用时必然会返回 false 的 BUG。


我们测试一下 injvm 的调用,将 demo 中 injvm 参数改为 true,Consumer 和 Provider 都在一个进程中,果然和注释说的一样:


server is user : trueclient is user = true
复制代码

如何修复

我觉得这应该算是 Dubbo 的一个 BUG,虽然这种写法不提倡,但作为一款 RPC 框架,这个错误还是不应该。


修复的办法就是在 injvm 分支这里加上判断,如果是 injvm 调用还是保持现状,如果不是 injvm 调用,直接忽略,走最后的 return 逻辑:


public <T> CompletableFuture<T> asyncCall(Callable<T> callable) {    try {        try {            setAttachment(ASYNC_KEY, Boolean.TRUE.toString());            final T o = callable.call();            //local invoke will return directly            if (o != null) {                if (o instanceof CompletableFuture) {                    return (CompletableFuture<T>) o;                }                if (injvm()) { // 伪代码                    return CompletableFuture.completedFuture(o);                }            } else {                // The service has a normal sync method signature, should get future from RpcContext.            }        } catch (Exception e) {            throw new RpcException(e);        } finally {            removeAttachment(ASYNC_KEY);        }    } catch (final RpcException e) {        // ....    }    return ((CompletableFuture<T>) getContext().getFuture());}
复制代码

最后

排查过程中还搜索了 github,但没有什么发现,说明这个 BUG 遇到的人很少,可能是大家用异步调用本来就很少,再加上返回基本类型就更少,所以也不奇怪。


而且最新的代码这个 BUG 也还存在,所以你懂我意思吧?这也是个提交 PR 的好机会~


不过话说回来,我们写代码最好还是遵循规范,这些都是前人为我们总结的最佳实践,如果不按规范来,可能就会有意想不到的问题。


当然遇到问题也不要慌,代码就在那躺着,工具也多,还怕搞不定吗?


最后,感谢群里小伙伴提供素材,感谢大家的阅读,如果能动动小手帮我点个在看就更好了。我们下期再见~


对了,标题为什么叫《再送你一次》?因为之前送过呀~



搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

发布于: 刚刚阅读数: 4
用户头像

捉虫大师

关注

还未添加个人签名 2018.09.19 加入

欢迎关注我的公众号“捉虫大师”

评论

发布
暂无评论
抓到Dubbo异步调用的小BUG,再送你一个贡献开源代码的机会_开源_捉虫大师_InfoQ写作社区