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,并且打了日志
@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个断点:

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

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

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

于是我们对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 : 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的好机会~

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

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

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

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

  • 本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。
  • 搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

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

  1. 限时购校验小工具&dubbo异步调用实现限

    本文来自网易云社区 作者:张伟 背景 限时购是网易考拉目前比较常用的促销形式,但是前期创建一个限时购活动时需要各个BU按照指定的Excel格式进行选品提报,为了保证提报数据准确,运营需要人肉校验很多信 ...

  2. 9.4 dubbo异步调用原理

    9.1 客户端发起请求源码.9.2 服务端接收请求消息并发送响应消息源码.9.3 客户端接收响应信息(异步转同步的实现) 分析了dubbo同步调用的源码,现在来看一下dubbo异步调用. 一.使用方式 ...

  3. 将前端js异步调用的多个服务合并为一个前端服务

    将前端js异步调用的多个服务合并为一个前端服务 1. 减少前端js异步请求的次数改善浏览体验 2. 方便地针对单个接口做异常降级处理

  4. dubbo异步调用三种方式

    异步通讯对于服务端响应时间较长的方法是必须的,能够有效地利用客户端的资源,在dubbo中,消费端<dubbp:method>通过 async="true"标识. < ...

  5. dubbo异步调用原理 (1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 一.使用方式 服务提供方不变,调用方代码如下: 1     <dubbo:reference id=& ...

  6. Dubbo学习笔记4:服务消费端泛化调用与异步调用

    本文借用dubbo.learn的Dubbo API方式来解释原理. 服务消费端泛化调用 前面我们讲解到,基于Spring和基于Dubbo API方式搭建简单的分布式系统时,服务消费端引入了一个SDK二 ...

  7. dubbo同步调用、异步调用和是否返回结果源码分析和实例

    0. dubbo同步调用.异步调用和是否返回结果配置 (1)dubbo默认为同步调用,并且有返回结果. (2)dubbo异步调用配置,设置 async="true",异步调用可以提 ...

  8. c#异步调用

    首先来看一个简单的例子: 小明在烧水,等水烧开以后,将开水灌入热水瓶,然后开始整理家务 小文在烧水,在烧水的过程中整理家务,等水烧开以后,放下手中的家务活,将开水灌入热水瓶,然后继续整理家务 这也是日 ...

  9. VB.NET中使用代表对方法异步调用

    按照我们常规的思维方式,计算机应该是干完一件事,然后再干下一件.用术语来说,这种执行任务的方式叫做同步执行(Synchronous Execution).既然这样,那么为什么要引入异步执行的概念呢? ...

随机推荐

  1. 转载:Linux图形界面知识(介绍X、X11、GNOME、Xorg、KDE等之间的关系)

    转载 http://blog.csdn.net/zhangxinrun/article/details/7332049Linux初学者经常分不清楚linux和X之间,X和Xfree86之间,X和KDE ...

  2. .NET桌面程序应用WebView2组件集成网页开发3 WebView2的进程模型

    系列目录     [已更新最新开发文章,点击查看详细] WebView2 运行时使用与 Microsoft Edge 浏览器相同的进程模型. WebView2 运行时中的进程 WebView2 进程组 ...

  3. 单片机DIY制作-基于STM32单片机甲醛二氧化碳温度湿度采集系统

    基于STM32单片机甲醛二氧化碳温度湿度采集系统 实践制作DIY-GC008-甲醛二氧化碳温度湿度采集系统 一.功能说明: 基于STM32单片机设计-甲醛二氧化碳温度湿度采集系统 二.功能介绍: 1. ...

  4. 操作系统深度研究(75页PPT)

    上一篇:命令行版的斗地主你玩过没? 内容覆盖操作系统基本概念.分类.关键技术,体系架构,发展历程和主流国产操作系统厂商分析. 文中报告节选自兴业证券经济与金融研究院已公开发布研究报告,具体报告内容及相 ...

  5. 公司为什么要使用OKR,目的是什么?

    原创不易,求分享.求一键三连 站在公司角度,会有一些诉求: 想知道每个人在干什么,干得怎么样: 想知道如何把更多的人卷起来: 人是不想被管束的,无论是想"度量我"还是想卷我,都是我 ...

  6. Node.js + TypeScript + ESM +HotReload ( TypeScript 类型的 Node.js 项目从 CommJS 转为 ESM 的步骤)

    当前 Node.js 版本:v16.14.0 当前 TypeScript 版本:^4.6.3 步骤 安装必要的依赖 yarn add -D typescript ts-node @tsconfig/n ...

  7. 图解Dijkstra算法+代码实现

    简介 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的 ...

  8. 好客租房24-react中的事件处理(事件绑定)

    3.1事件绑定 React事件绑定语法和DOM事件语法相似 语法:on+事件名称={事件处理程序} 比如οnclick={()=>{}} //导入react     import React f ...

  9. 2020级cpp上机考试题解#B卷

    A卷的第七题我只会一个个排除的方法 意思就是暂时没有好办法所以A卷不搞了 1:递归函数求数列 题意: 有一个递归函数int f(int m),计算结果代表了数列的第m项.当m等于1时,函数结果返回1: ...

  10. 关于JNPF3.4版本的三大改变,你真的了解了吗?