当前位置: 首页 > 科技观察

Dubbo异步调用的小BUG_0

时间:2023-03-12 07:58:37 科技观察

如何捕捉大家好,我是小楼。最近技术组的一个同学问我对Dubbo熟悉不?好熟啊~他说遇到了Dubbo异步调用的问题,怀疑是BUG。哦不...写一篇文章。问题复现如果遇到问题,尤其是自己没有遇到,一定要复现才能排查问题。拿个当时的聊天记录:他原来的问题是:今天发现一个返回类型为boolean的dubbo接口有问题。将接口从同步更改为异步。服务器端返回true,但消费者端返回false。把boolean改成Boolean,就可以正常返回结果了。你遇到过这个问题吗?注意几个关键点:接口返回类型从booleansynchronous改为异步调用返回boolean不符合预期的boolean基本类型。改成boolean类型的wrapper可以正常返回。听到这个描述,我的第一反应是返回结果定义为boolean肯定有问题!《Java开发手册》强调最好不要使用RPC接口返回的基本类型,而是使用包装类型:但这是业务编码规范。如果RPC框架不能使用boolean作为返回值,那不是BUG吗?并且他强调,只有同步调用改成异步调用才会出现这种情况,说明同步没有问题,有可能是异步调用的错。于是问了一下dubbo的版本,可能是某个版本的bug。收到回复是Dubbo2.7.4版本。所以我拉了一个项目来重现这个问题。哎,等等~dubbo有很多异步调用的写法,我就问他是怎么写的。知道如何写作很容易。先写个demo:定义Dubbo接口,一个返回boolean,一个返回BooleanpublicinterfaceDemoService{booleanisUser();BooleanisFood();}实现Provider。为简单起见,两者都返回true,Logged@ServicepublicclassDemoServiceImplimplementsDemoService{@OverridepublicbooleanisUser(){System.out.println("serverisuser:true");返回真;}@OverridepublicBooleanisFood(){System.out.println("serverisfood:true");返回真;}}实现消费者。为了方便调用,实现了一个Controller。为了防止本地调用,injvm设置为false。这是经验。injvm调用逻辑和远程调用区别比较大,为了防止干扰,统一远程调用。@RestControllerpublicclassDemoCallerService{@Reference(injvm=false,check=false)privateDemoServicedemoService;@GetMapping(path="/isUser")publicStringisUser()throwsException{BlockingQueueq=newArrayBlockingQueue<>(1);RpcContext.getContext().asyncCall(()->demoService.isUser()).handle((isUser,throwable)->{System.out.println("clientisuser="+isUser);q.add(isUser);返回isUser;});q.take();返回“确定”;}@GetMapping(path="/isFood")publicStringisFood()throwsException{BlockingQueueq=newArrayBlockingQueue<>(1);RpcContext.getContext().asyncCall(()->demoService.isFood()).handle((isFood,throwable)->{System.out.println("客户是食物="+isFood);q.add(isFood);返回是食物;});q.take();返回“确定”;}}启动一个Provider,然后启动一个Consumer进行测试,果然表现和提问的同学一致:先调用isUser(returnboolean),控制台打印://client...clientisuser=false//server...serverisuser:true然后调用isFood(返回布尔值),控制台打印://client...clientisfood=true//server...serverisfood:true故障排除调试先猜测是哪里出了问题,服务端返回true,应该问题不大,可能是客户端哪里转换错了,不过这都是猜测。我们直接从客户端收到的数据开始。如果接收到的数据没问题,那么后面的处理肯定有小错误。如果你非常熟悉Dubbo的调用流程,你会直接知道com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived我们打了3个断点:断点①证明我们的请求是在断点②来证明进入回调断点③为了从接收到的数据包的初始位置开始检查,按照我们的想法,执行顺序应该是①、③、②,但是这里很奇怪,并没有按照我们预期的那样执行,但是先执行①,再执行②,最后执行③!这是为什么?在排查问题时要特别注意这些不符合预期的线索,可能是一个突破点。于是我们追查了asyncCall这个方法:发现callable调用在这里返回了false,然后false不为null,也不是CompletableFuture的实例,所以直接调用了CompletableFuture.completedFuture(o)。看到这里,估计有小伙伴发现问题了。一般情况下,Dubbo的异步调用在调用执行后并不会立即得到结果,而只会得到一个null或者一个CompletableFuture,然后在回调方法中等待服务端的返回。这里的逻辑是,如果返回结果不为null且不是CompletableFuture的实例,那么直接设置CompletableFuture完成,并立即执行回调。现在不管这个逻辑。让我们先看看为什么它会返回false。这里的callable是Dubbo生成的代理类。其实就是封装了调用Provider的逻辑。有没有办法看到它封装的逻辑?有!使用阿尔萨斯。arthas我们下载安装一个arthas,可以参考如下文档:https://arthas.aliyun.com/doc/quick-start.html附加到我们的Consumer进程,执行sc命令(查看加载类)查看所有生成Proxy类,因为我们的Demo生成了一个,所以看起来很清晰sc*.proxy0然后用jad命令反编译加载的类:jadorg.apache.dubbo.common.bytecode.proxy0朋友们看这里我们有又揭开了一层疑惑,this.handler.invoke是调用Provider,由于是异步调用,必须返回null,所以返回值定义为boolean的方法返回false。看到这里,估计小伙伴们对《Java开发手册》中的规范有了更深的理解,这里的处理也是无奈之举,不然返回true?归属信息丢失,无法区分是呼叫返回还是其他异常情况。回过头来看asyncCall:圈出来的代码耐人寻味,尤其是最后一行,为什么直接设置CompletableFuture完成?从这个方法的名字就可以看出是一个异步调用,但是这里有一行注释://localinvokewillreturn首先这个注释的格式不一样,//后面的原因,需要一个空格,我想这里放个PR改代码格式肯定能接受~其次,localinvoke,我理解应该是injvm调用,为什么要特意处理?这种处理直接导致返回基本类型的接口在异步调用时必然会返回false的bug。我们来测试一下injvm的调用,将demo中的injvm参数改为true,Consumer和Provider都在同一个进程中,和评论一样:serverisuser:trueclientisuser=true如何修复我觉得这应该是Dubbo的一个BUG,虽然不推荐这种写法,但是作为一个RPC框架,这个错误应该不会有。修复方法是在injvm分支中添加判断。如果是injvm调用,保持现状。如果不是injvm调用,直接忽略,按照最终的返回逻辑:publicCompletableFutureasyncCall(Callablecallable){);finalTo=callable.call();//本地调用将直接返回if(o!=null){if(oinstanceofCompletableFuture){return(CompletableFuture)o;}if(injvm()){//伪代码returnCompletableFuture.completedFuture(o);}}else{//该服务有一个正常的同步方法签名,应该从RpcContext获得未来。}}catch(Exceptione){抛出新的RpcException(e);}最后{removeAttachment(ASYNC_KEY);}}catch(finalRpcExceptione){//....}return((CompletableFuture