RPC框架分解
本地调用
本地进程内调用就不用多讲了,我们每天写的代码大部分都是这个EchoService service = new EchoServiceImpl(); service.sayHello("Dubbo");虽然是调用的接口,但是实际上调用的是实现类的方法,所谓的面向对象的多态性。对于本地进程内调用来说,因为jvm可以直接加载到实现类,所以调用接口方法和调用实现类方法没有任何区别。
远程调用
随着应用从单机发展到分布式,某个接口的实现可能部署到了其它机器上,这个时候要调用方法就变成了下图这样: 左边是调用方,一般把它叫Consumer,Consumer这边只有接口定义(一般叫api),没有接口实现。Consumer发起请求之后,请求通过网络发送给右边的服务实现方,一般叫Provider。Provider收到请求,调用本地的实现类得到结果,然后把结果通过网络发回去,这样就叫一次远程调用了。当然,还有一种Provider不直接返回结果,而是通过异步回调的方式,原理差别不大。 如果上面这个过程全由用户自己实现的话,事还是很多的。如果有一个中间层干两件事,1)让Consumer端只要调用接口,不用管接口后面是本地实现类还是需要远程网络传输;2)Provider端只要把实现类放在那里,不用管调你的人是谁,按正常返回结果就好了,中间层把其它的事全做了,这个中间层就是RPC框架了。 当然,主流的框架在中间还提供了容错、监控这些功能,原则上说这些都不是RPC的本质功能。下面我们采用从中间往两边扩散的方式来分解下RPC的职责。序列化
程序中接口调用传的是对象,实际上是一堆只有虚拟机能读的内存地址,而网络上能传的是0和1组成的字节数据,所以就有一个转换的过程,就是我们常说的序列化。常见的序列化协议如json、Protobuf等。加了序列化之后,用户调用时传参的对象实例,比如一个User,按照一个固定的格式转换,比如转成json字符串,provider侧拿到这个json,再把它转成一个User。所以User定义和接口定义一样,也是在双方约定的api定义中。这个定义不一定是个jar包,因为对方不见得是java写的,这就是序列化存在的另外一个意义。网络通信
网络上传输的是一个个数据包组成的数据流,数据在经过一个个网络中间节点的时候,会不断的被合并成大个数据包或者拆分成小的。所以要依赖网络协议来在对端重新组合数据包。流行的rpc框架一般都采用基于tcp协议的上层协议,比如http,当然也有很多跟Dubbo一样使用基于tcp的私有协议。封装网络协议的模块一般叫Codec,里面包含编码(encode)和解码(decode)方法。 所以上面的图就变成这样: Codec只负责对象和0/1之间转换,但是网络通信还包括建立和Provider的链接并发送和接收数据包。这时候又会抽象出一个传输层(Transporter)专门负责建立和关闭链接,维护链接池,处理连接和断开的事件。在java中,netty是应用最多的框架。 所以,上面的图又变成这样了: Transporter层通常不会直接接收Consumer发过来的对象,原因嘛肯定是针对每个对象都做协议适配和转换,这个得多少if-else。所以通常会封装一个Request和Response,我只接收Request并把它发出去,然后收回来response。这个Request随便想想的话应该有这么些属性:接口名、版本号、方法名、参数列表等等;而Response应该有执行状态Code和返回数据。 所以,上面的图变成下面这样:代理
Consumer调用一个接口的方法,实际上是调用的一个接口实现的引用。本地方法调用的时候,无非两种方式,一种直接在类里初始化实例,就是new一个对象,另外一种依赖容器注入,比如使用Spring注入。对于远程接口,第一种肯定是不行了,所以只能选择容器来注入对象引用,这就给了RPC操作空间,RPC只要在容器中构造一个接口实现就可以了,在方法实现中调用远程网络接口。这个就是常说的代理(Proxy)了,Java中一般使用动态代理来实现。 最终的模块图应该是下面这样了,中间一部分全是对RPC的基本要求: 终于把做为一个RPC框架的基本素养折腾清楚了,为了降低复杂性,这里面没有涉及集群及高可用等Dubbo为了支撑微服务相关的特性,这些部分会在RPC基础之后再涉及。Dubbo RPC组件
下面按照上面对RPC的拆解来分析下Dubbo对应的模块,这里只是为了展现一个大体的框架,不会对每个模块做详细的解析,所以有个大概印象就可以了。后续分析到具体模块的源码,可以回来翻查一下。 为了和思维方式对上,我们从左至右过一下RPC框架图,先从客户端调用接口开始。代理
Proxy
第一步是接口要有个代理实现,下面是Dubbo官方的Consumer例子,因为绝大多数人都是Dubbo和Spring一起用的,所以选用SpringBoot的例子:@EnableAutoConfiguration public class DubboAutoConfigurationConsumerBootstrap { private final Logger logger = LoggerFactory.getLogger(getClass()); @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345") private DemoService demoService; public static void main(String[] args) { SpringApplication.run(DubboAutoConfigurationConsumerBootstrap.class).close(); } @Bean public ApplicationRunner runner() { return args -> logger.info(demoService.sayHello("mercyblitz")); } }代码中DemoService就是远程API接口,Dubbo使用自定义的@Reference注解来注入,被注入的自然就是Dubbo的Proxy实现了,对这个接口的调用,会通过Proxy将请求发到127.0.0.1的12345端口。 Dubbo通过调用代理工厂ProxyFactory的getProxy来获得一个接口的代理实现类。
@SPI("javassist") public interface ProxyFactory { @Adaptive({PROXY_KEY}) <T> T getProxy(Invoker<T> invoker) throws RpcException; }可以看到,这个代理工厂也是一个接口,Dubbo中提供了两种实现,一种使用的JDK中的动态代理实现(JdkProxyFactory),这个就是用的java.lang.reflect.Proxy.newProxyInstance()方法,关于这个的具体原理和用法网上很多,这里就不解释了。 Dubbo默认采用的是另一个实现JavassistProxyFactory,Javaassist是一个运行期字节码工具,它允许在运行时动态编译代码成class。Dubbo默认做法就是在初始化时生成一个接口实现类的源代码,通过javaassist编译后加载,Consumer实际上就是调用的这个类。 上面的源代码在dubbo-rpc的dubbo-rpc-api模块中。
代理实现原理
代理有了,做的事情无非就是按传输层要求的格式把调用参数封装成一个Request,通过传输层发出去,比如通过http调用的话就封装成一个HttpRequest。但是在Dubbo这里没有这么简单,因为Dubbo是需要支持多种协议的,每个接口用的网络接口调用Client可能都不一样,而且还要保证扩展性,方便以后添加新的协议实现。 有3个类需要重点关注下:Protocol,Invoker和Invocation。- Invoker
Invoker是调用动作封装类,作用类似于java.lang.Runnable相对于Thread。 调用它的invoke()方法发起一次调用,这次调用有可能是远程有可能是本地,对调用方来说是透明的。一般来说,每个远程服务接口都会对应有一个Invoker实例。
public interface Invoker<T> extends Node { /** * 获取Invoker调用的target接口类 */ Class<T> getInterface(); /** * 发起一次调用 */ Result invoke(Invocation invocation) throws RpcException; }
- Invocation
invoke() 操作的输入参数,就是把真实调用的method,输入参数等封装一下。下面是它定义的部分方法:
public interface Invocation { ... /** * 获取方法名 */ String getMethodName(); /** * 获取接口名 */ String getServiceName(); /** * 参数类型 */ Class<?>[] getParameterTypes(); /** * 调用参数 */ Object[] getArguments(); ... }
- Protocol
Dubbo对接口协议的抽象,主要是两个功能,对于Provider来说通过Protocol把本地接口暴露成一个指定协议Server;对于Consumer来说可以获取一个针对特定接口协议的Invoker
@SPI("dubbo") public interface Protocol { //暴露服务接口 @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; //获取接口调用的Invoker @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; }看完上面几个接口,把关系捋一下,Consumer获取到接口的Proxy后,调用方法时,Proxy根据远程服务的配置协议找对应的Protocol实现,默认是DubboProtocol。然后,调用Protocol的refer方法获取到Invoker实现,比如从DubboProtocol获取到的就是DubboInvoker。然后,将Consumer原始的输入参数封装成一个Invocation(对应于远程调用就是RpcInvocation),调用Invoker的invoke() 方法。 注意以上只是逻辑流程,不是真正的代码调用流程,因为代码实现中对象都是提前初始化的,初始化的代码流程和逻辑流程不是完全一样。
请求传输
继续按照前面的RPC流程图往右走,下一个模块就是传输模块了。Dubbo的传输层模块抽象层数较多,这里只列举比较关键的部分。Client
作为Consumer和服务提供端通信,针对不同协议都会提供Cient,Dubbo将其抽象为ExchangeClient,用于发送Request并将收到结果转化成Response。public interface ExchangeClient extends Client, ExchangeChannel { }ExchangeClient接口是一个组合接口,组合了Client和ExchangeChannel 。
public interface Client extends Endpoint, Channel, Resetable, IdleSensible { /** * reconnect. */ void reconnect() throws RemotingException; ... } public interface ExchangeChannel extends Channel { /** * 发送请求 */ CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException; ExchangeHandler getExchangeHandler(); /** * 关闭Channel */ @Override void close(int timeout); }从上面两个接口定义,可以看出Client负责连接的建立,ExchangeChannel负责请求的异步发送(request())以及结果的接收(ExchangeHandler)。Dubbo中接口的默认实现类分别是HeaderExchangeClient和HeaderExchangeHandler。 Client仅仅属于传输层的一半,对于整个传输层的抽象则是Exchanger接口:
@SPI(HeaderExchanger.NAME) public interface Exchanger { /** * 开启一个服务端Server */ @Adaptive({Constants.EXCHANGER_KEY}) ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException; /** * 获取Client */ @Adaptive({Constants.EXCHANGER_KEY}) ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException; }
Transporter
对于Client来说,发送和接收是Request和Response,它所依赖的底层数据的传输抽象为Transporter。@SPI("netty") public interface Transporter { /** * Bind a server. */ @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY}) RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException; /** * Connect to a server. */ @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY}) Client connect(URL url, ChannelHandler handler) throws RemotingException; }看起来是不是和Exchanger一样,其实本来就是一样的,只是在Exchanger层存在Request和Response的概念,而Transporter层则只有Object的概念。也就是说,Transporter层只管传数据,而没有交互的概念,不管数据背后的意义。Exchanger是应用传输层,Transporter是数据传输层。Transporter的默认实现类是NettyTransporter。 到现在位置,我们把上面的RPC的图又往右推了一步,上一节中的Invoker执行时,创建一个ExchangeClient,请求参数被封装成一个Request发出,正常的话就可以收到一个Response,Response中包含了调用方法的返回值。
编码
Transporter在收到数据后,需要按协议来打成数据包,Dubbo中负责编码的是Codec接口。 @SPI public interface Codec2 { @Adaptive({Constants.CODEC_KEY}) void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException; @Adaptive({Constants.CODEC_KEY}) Object decode(Channel channel, ChannelBuffer buffer) throws IOException; enum DecodeResult { NEED_MORE_INPUT, SKIP_SOME_INPUT } } 接口简单直接,一个编码,一个解码,默认实现类是ExchangeCodec。序列化
终于走到最后一步了, 序列化的意义上面已经说了,Dubbo对序列化的抽象接口是Serialization,默认实现类是Hessian2Serialization。@SPI("hessian2") public interface Serialization { /** * Get content type unique id */ byte getContentTypeId(); /** * content type */ String getContentType(); /** * 序列化 */ @Adaptive ObjectOutput serialize(URL url, OutputStream output) throws IOException; /** * 反序列化 */ @Adaptive ObjectInput deserialize(URL url, InputStream input) throws IOException;