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

捉虫大师
• 阅读 772

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

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

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

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

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

问题复现

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

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

他的问题原话是:

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

注意几个重点:

  • 接口返回类型是boolean
  • 同步改为异步调用返回的boolean和预期不符合
  • boolean基本类型改成包装类型Boolean就能正常返回

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

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

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

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

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

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

哎,等等~

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

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

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

  1. 定义Dubbo接口,一个返回boolean,一个返回Boolean
public interface DemoService {
    boolean isUser();
    Boolean isFood();
}
  1. 实现Provider,为了简单,都返回true,并且打了日志
@Service
public 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调用逻辑和远程调用区别挺大,为了防止干扰,统一远程调用。
@RestController
public 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个断点: 抓到Dubbo异步调用的小BUG,再送你一个贡献开源代码的机会

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

  • 断点①为了证明我们的请求进来了
  • 断点②为了证明进了回调
  • 断点③为了能从接受到数据包的初始位置开始排查

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

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

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

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

发现这里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

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

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

jad org.apache.dubbo.common.bytecode.proxy0

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

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

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

我们再回头看asyncCall

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

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

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

//local invoke will return directly

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

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

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

server is user : true
client 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的好机会~

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

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

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

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

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

点赞
收藏
评论区
推荐文章
捉虫大师 捉虫大师
2年前
惨,给Go提的代码被批麻了
hello大家好,我是小楼。不知道大家还记不记得我上次找到了一个Go的Benchmark执行会超时的Bug?就是这篇文章。之后我就向Go提交了一个PR进行修复,本想等着代码被Merge进去,以后也可以吹牛说自己是个Go的Contributor,但事情并不顺利,今天就来分享一下这次失败的代码提交。第一次提交在我意识到Bug时,就迫不及待想去修复,于是有了这一
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
捉虫大师 捉虫大师
2年前
我是一个Dubbo数据包...
hello,大家好呀,我是小楼!今天给大家带来一篇关于DubboIO交互的文章,本文是一位同事写的文章,用有趣的文字把枯燥的知识点写出来,通俗易懂,非常有意思,所以迫不及待找作者授权然后分享给大家:一些有趣的问题Dubbo是一个优秀的RPC框架,其中有错综复杂复杂的线程模型,本篇文章笔者从自己浅薄的认知中,来剖析Dubbo的整个IO过程。在开始之前,我们
捉虫大师 捉虫大师
3年前
排查dubbo接口重复注销问题,我发现了一个巧妙的设计
背景我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错。经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会因为实例已经注销而报实例找不到的错误。虽然这个报错仅会打印一条错误日志,不影响业务,但本着followthrough的精神,我决定还是一探究竟,更何况重复注销
Wesley13 Wesley13
3年前
2个小bug,有点小门道
还有近1个月就要离职了,最近整理下解决过的问题,发现2个小bug,有点小门道。Bug1:2017年的某日,小辉(我的同事)遇到了一个bug,解决了一下午还是没有找到,气的摔键盘,骂人,我看在眼里,急在心中。在他发作了5分钟后。我提心吊胆的问:    “小辉,别着急遇到了什么问题啊?大家一块看看吧!“小辉:   
Wesley13 Wesley13
3年前
MySQL 查询不区分大小写的问题以及编码格式问题
查询不区分大小写最近,在用SSH框架完成一个实践项目时,碰到了一个莫名其妙的Bug困扰了我好久,最后终于解决,记录如下。问题:同学在测试系统的时候突然发现,数据库保存的账户本来应该是admin,结果该同学用Admin账户居然登录成功了…………EXM???这样也行?好吧,我还是查找这个Bug发生的原因吧。然后就是各种排查程序的过程
Stella981 Stella981
3年前
Dubbo错误No provider available for the service
  最近要开发dubbo服务,因为以前没用过,其实dubbo服务很简单,网上有很多例子,还有官方文档http://dubbo.io/Homezh.htm,由于新手上路难免遇到各种各样的问题,我就遇到一个问题让我很是费解,百度也没有多少可用的博客,浪费了1天时间,最后找我们技术总监才搞定,这个错误如下:com.alibaba.dubbo.rpc.Rp
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
捉虫大师 捉虫大师
2年前
这不会又是一个Go的BUG吧?
hello,大家好呀,我是小楼。最近我又双叒叕写了个BUG,一个线上服务死锁了,不过幸亏是个新服务,没有什么大影响。出问题的是Go的读写锁,如果你是写Java的,不必划走,更要看看本文,本文的重点在于Java和Go的读写锁对比,甚至看完后你会有一个隐隐的感觉:Go的读写锁是不是有BUG?故障回放背景简单抽象一下:一个server服务(Go语言实现),提供了
Python进阶者 Python进阶者
8个月前
他说遇到了循环导入,但是我怎么看我的代码都没有循环导入
大家好,我是Python进阶者。一、前言前几天在Python白银交流群【Kim】问了一个Python基础的问题,问题如下:他说遇到了循环导入,但是我怎么看我的代码都没有循环导入。二、实现过程这里【魏哥】给了一个解答:这个之前搞Python2的时候好像经常遇