Dubbo是Alibaba开源的一款很棒的SOA框架,被国内很多公司所采用。去哪儿网也早在2011年就开始采用Dubbo作为我们服务化拆分的基础,并且在Dubbo停止维护后自己fork了分支继续维护,也在Dubbo上投入了大量的精力。
作为服务拆分的基础设施,随着调用链路日趋复杂化,异步调用越来越重要了。
Dubbo默认配置下采用的是同步调用方式。也就是consumer调用一个服务的时候,当前线程会block住,block的线程将不能干其他事情。而线程是昂贵而有限的资源,比如在链路中consumer往往又是其他服务的provider: a -> b -> c。如果这个时候c因为某种原因响应时间恶化,使用同步调用的b将会受到严重的影响,最坏的情况下可能将b的服务线程耗光,最后b拒绝服务,进而对a造成影响。而如果c是一个非关键服务,而b提供的是关键服务,这个时候你肯定后悔莫及。而如果b是异步去调用c就不同了,b发起异步调用之后调用线程即可释放,可以继续处理别的事情了,这样如果是非关键服务可以干脆不用关心它的返回结果了。
上面描述的是consumer异步调用的情况,而provider也有异步处理的场景,比如上面的b如果采用异步调用c,那么如果c有返回结果了,什么时候将结果返回给a呢?Dubbo默认配置是同步等待provider的处理结果,然后将其写回。但是provider端如果是一个异步调用则无法等待结果了,因为发起异步调用后hold住连接的线程就离开了。所以provider端也需要一种机制,处理结果完成时将结果返回。
为了区分上面的两种情况我将其称为consumer端异步和provider端异步。
Dubbo默认也提供了『多种多样』的异步方式,但个人觉得这些异步方式略显混乱和难用。下面将简单的过一下Dubbo现有的一些异步方式。
consumer端异步
1. 使用Future的方式
a. 先要将该服务配置成异步调用
这也是我不喜欢的地方之一。是否异步调用应该是根据当前场景做出的选择,一个服务可能在有的地方使用同步调用,而在另外一些地方使用异步。而这种配置死的方式就很麻烦了,如果有两种用途还需要配置两个。
b. 调用
一直觉得上面的API很反人类。service.longTask()是实际调用的地方,因为是异步调用你不能从这里拿返回结果,所以就孤孤单单的留在这里。还有上面的ResponseCallback,你千万别被那个caught所迷惑,这里只有超时以及框架异常才会调用,而业务异常是不会调用的,业务异常也是走done。而done里的response也不是你期望的返回结果,而是Dubbo里一个Result对象。
2. 配置回调的方式
第二种方式就不详细介绍了,就是异步调用结果返回的时候会调用callback对象的onResult方法。
上面就是Dubbo原有的consumer端异步的方式,然后我们再来看看provider端异步。
provider端异步
如果说consumer端异步的实现方式不友好,那么provider异步的方式就更加繁琐了,繁琐到你用一次就需要查一次文档。
首先如果要使用provider端异步,那么服务的API就需要做出改变。比如你原有的服务接口是:
String sayHello(String name);
那么你需要修改为:
void sayHello(String name, ResultListener listener);
这个ResultListener是你自定义的一个接口,而不是真正的参数。然后对服务的配置要做进一步修改:
这上面的参数还有一些坑,懒得介绍了。
然后consumer调用的时候:
provider端的实现逻辑:
这种方式,每次使用都很不舒服。首先我觉得provider是否异步并不关consumer的事情,不应该对consumer产生任何影响。比如我有个抓取服务,原来使用的是同步的httpclient去访问别的系统,然后有一天我修改为异步httpclient,那我是否要consumer协助我一起修改呢?
以上就是Dubbo的provider异步了。
有了这么多痛点,我们决定重构一下Dubbo的异步实现方式。
新的异步
consumer端异步
实现一套新的API,我还是喜欢从上而下,也就是先写出我喜欢的调用方式,然后再去想想如何去实现:
我希望consumer端异步最好就这样了,而且不用任何预先配置,我想异步就异步,想同步就同步。这样一个API还有一个好处是可以和现有的工具很好的结合,比如google Guava里一整套针对ListenableFuture的处理类。
但是,sayHello的原始签名是这样的: String sayHello(String name)。怎么让它返回一个Future呢?难道每个API都要写两个版本么?写两个版本那provider如何去实现呢?
我们在前文已经说过了,consumer端的异步和provider端并无关系,只是consumer在调用的时候决定是否等待。那么我们现在我们要做的就是怎么弄出这样一个API出来,然后在调用的时候里面偷偷的异步去调用provider。
我们的实现方式是使用java的annotation processor,你只需要在你原有的API上添加这么一个注解:
然后我们实现一个annotation processor自动给你生成一个这个接口的实现接口:
这个接口是自动生成的,provider提供这个接口的实现,甚至不用关心这个接口的存在。然后consumer端使用的时候就可以直接用这个接口了:
因为这个接口实现了同步版本的接口,所以即可以调用异步,也可以调用同步。这样一来,API的变通就搞定了,实现起来也很简单。Dubbo是通过字节码生成的方式来生成网络代理,进行rpc调用的。所以给异步的方法生成的代理只要是异步调用就行了,然后其实里面实际调用的还是同步版本的接口,只不过调用方式变了,就是调用的时候不去wait,而是返回一个Future。而Dubbo里只是用API去做服务发现,去找到provider的地址列表,所以在服务发现的时候我们从AsyncImpl里取出对应的同步接口去找provider列表。
这个的方式是不是比Dubbo原生的方式漂亮多了呢?遵循习惯的模式,API所见即所得。解决了consumer端异步,我们再来看看provider端异步吧。
provider端异步
其实provider端异步也已经有可参照的API了: servlet 3.0里的async servlet。类似下面的方式(伪代码):
那么我们就可以模仿这个对Dubbo的API做一下改进:
这样的API对consumer完全是透明的,consumer可以同步调用这个方法,也可以异步调用这个方法。而Dubbo内部的改造也很简单。其实AsyncContext就像是一个ListenableFuture,当执行完provider的逻辑后,给这个Future注册一个回调,在回调里将结果写回consumer即可(伪代码):
OK,这就是我们改造后的Dubbo异步版本API了,其实对Dubbo本身的改造才几行代码,很容易就实现了。
实际上,在设计API的时候,我们应该更多的从使用方考虑,先将使用API的demo写出来,然后再思考实现。另外,实现API的时候尽量遵循惯用法,可以看看有没有其他的项目里已有这种做法,特别是那些广为人知的项目,比如这里的guava和servlet 3.0之类的。