RPC原理

一旦踏入公司尤其是大型互联网公司就会发现,公司的系统都由成千上万大大小小的服务组成,各服务部署在不同的机器上,有不同的团队负责。这时就会遇到两个问题:1)要搭建一个新服务,免不了需要依赖他人的服务,而现在他人的服务都在远端,怎么调用?2)其他团队要使用我们的服务,我们的服务该怎么发布以便他人调用?

如何调用他人的远程服务?
由于各服务器部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都需要写一堆网络通信相关的代码,不仅复杂而且极易出错。
如果有一种方式能让我们像调用本地服务一样调用远程服务,而让调用者对网络通信这些细节透明,那么将大大提高生产力,比如服务消费方在执行某个函数调用时,实际上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call Protocol),在各大互联网公司中被广泛使用,如阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle等。
要让网络通信细节对使用者透明,我们自然需要对通信细节进行封装,我们先看一下RPC调用的流程:
  1. 服务消费方(client)以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  3. client stub找到服务地址,并将消息发送到服务端;
  4. server stub收到消息后进行解码;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给server stub;
  7. server stub将返回结果打包成消息并发送至消费方;
  8. client stub接收到消息,并进行解码;
  9. 服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
RPC原理

什么是stub程序?
stub:RPC(Remote Procedure Call Protocol)的一个重要思想就是使远程调用看起来像本地调用一样,也就是说调用进程无需知道被调进程具体在哪台机器上执行。stub就是用来保证此特性的很重要的部分。具体的讲,比如在客户端,一个进程在执行过程中调用到了某个函数fn(),此函数的具体实现是在远程的某台机器上,那么此进程实际上是调用了位于本地机器上的另一个版本的fn()(起名为c_fn()),此c_fn就是客户端的一个stub。对应的,当客户端的消息发送到服务器时,服务端也不是直接就交给真正的fn(),而是同样先交给一个不同版本的fn()(起名为s_fn())。此s_fn()就是服务端的一个stub。
注:远程过程调用(RPC)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式程序在内的应用程序更加简单。
RPC采用了客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用过程接收答复信息,获得进程结果,然后继续执行下面的语句。

怎么做到透明化远程服务调用?
怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对Java来说就是使用代理。Java代理有两种方式:1)JDK动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码不易维护,大部分公司实现RPC框架时还是选择动态代理方式。

怎么对消息进行编码和解码?
首先需要确定请求消息结构,包括:
  1. 接口名称
  2. 方法名
  3. 参数类型&参数值
  4. 超时时间
  5. requestID,标识唯一请求ID(设计requestID确保请求的结果可以正确地返回给请求的进程)
同理服务端返回的消息结构一般包括:
  1. 返回值
  2. 状态code
  3. requestID
当然要将上述消息结构进行序列化,才可以方便地在网络上传输,到达目的机器上再进行反序列化解析获取消息内容。实际上,序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程;反序列化是将所生成的二进制串转换成数据结构或者对象的过程。
从RPC的角度来看,序列化应该满足(1)通用性,比如是否支持map等复杂的数据结构;(2)性能,序列化是否能节约网路带宽和存储内存;(3)可扩展性,对互联网来说,服务需求变化很快,要求序列化协议需要具有很好的可扩展性。