RPC(Remote Process Call)即远程过程调用,允许一台计算机调用另一台计算机上的程序得到结果,就像调用本地程序一样。RPC是分布式系统中一个基础概念,它是相对于本地调用来说的,指调用非本地的服务器上的方法。
RPC基本原理
rpc规范只规定了 Client 与 Server 之间的点对点调用流程,包括 stub(代码存根)、通信协议、消息解析等部分。
RPC框架简介
基于rpc规范,一次RPC调用的过程如下:
简单来说,就是服务调用方发送请求到服务提供方,服务提供方根据请求参数执行服务并获得结果,然后将结果返回给服务请求方。
请求数据在网络上最终是以二进制的数据进行传输的,因此涉及到序列化的问题。当然,最基本的还有网络通信问题。也就是说,最基本的rpc包含传输协议和序列化协议。
而在实际生产过程中,由于服务提供方一般是以集群形式存在的,因此涉及到服务发现,负载均衡的问题。服务上线后,还涉及到服务治理的问题。
最基本的RPC
基于Java的Socket API,就可以实现一个简单的RPC调用:
// 被调用方
public class Provider {
private static Map<String, Object> services = new HashMap<>();
static {
services.put("ISayHelloService", new SayHelloServiceImpl());
}
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
Socket socket = serverSocket.accept(); // 建立socket监听
ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); // 获得socket端口的输入流
String interfaceName = input.readUTF(); // 接口名
String methodName = input.readUTF(); // 方法名
Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); // 参数类型
Object[] argus = (Object[]) input.readObject(); // 参数对象
Class serviceInterfaceClass = Class.forName(interfaceName);
Object service = services.get(interfaceName); // 取得服务实现的对象
Method method = serviceInterfaceClass.getMethod(methodName, parameterTypes); // 获得调用方法
Object result = method.invoke(service, argus);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
}
}
}
// 调用方
public class Consumer {
public static void main(String[] args) throws Exception {
String interfaceName = ISayHelloService.class.getName();
Method method = ISayHelloService.class.getMethod("sayHello", String.class);
Object[] argus = {"hello"};
Socket socket = new Socket("127.0.0.1", 8888);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(interfaceName);
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(argus);
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
Object result = input.readObject();
System.out.println(result.toString());
}
}
// 服务定义
public interface ISayHelloService {
String sayHello(String str);
}
// 服务实现
public class SayHelloServiceImpl implements ISayHelloService {
@Override
public String sayHello(String str) {
return str;
}
}
这个例子中通过socket进行通信,服务请求方将方法名和参数发送给服务提供方,服务提供方收到方法名和方法参数之后,通过发射机制获取到执行方法,发起调用得到结果并返回给服务请求方(这个例子为了简单方便,没有使用代码存根stub)。
传输协议
RPC的传输协议可以基于tcp,也可以基于http。基于tcp的rpc,一般使用私有协议,在tcp的基础上进行定制开发。而http是一种应用层的公有协议,可以直接拿来使用,无需太多额外的工作。
序列化
Java自带的序列化接口Serializable在RPC中很少用到,原因有两个:一是因为Java序列化无法跨语言,只在Java内部支持。更重要的是,Java序列化后的数组体积大,从而导致存储和传输效率低。
当前主流的序列化方式有两类:
文本类序列化方式
主要包括 JSON 和 XML,优点是:支持跨语言、可读性好、配套的支持工具比较全。缺点:序列化之后的码流比较大、冗余内容多,性能相对比较差。
二进制序列化方式
比较流行的有Thrift 序列化框架、MessagePack 和谷歌的 Protobuf 框架。优点:性能高。缺点:可读性差,支撑的工具链不健全。
RPC与Restful API
分布式进程之间可以使用rpc进行通信,也可以使用http,或者更严谨的说法是restful api进行通信。那么这两者之间有何区别呢?
历史进程
RPC在1984年就被人用来做分布式系统的通信,Java在1.1版本提供了Java版本的RPC框架(RMI)。HTTP协议在1990年才开始作为主流协议出现,它发明的目的是用于web架构的前后端通信,而不是分布式系统间通信。
这导致了在很长一段时间内,HTTP都是浏览器程序和后端web系统通信用的东西,使用的文档格式都是HTML,没有人会把HTTP作为分布式系统通信的协议。随着技术的发展,AJAX技术和JSON文档在前端界逐渐成为主流,HTTP调用才摆脱HTML,开始使用JSON这一相对简洁的文档格式,为后面用于系统间调用定下基础。最后随着Restful思潮的兴起,越来越多系统考虑用HTTP来提供服务。与此同时,RPC也已经是各种大型分布式系统调用的标配了。
区别
RPC和Restful API都可以用来作为进程之间通信的选择。
基本的 RPC 实现一般会包含有 传输协议 和 序列化协议 。
Restful API也包含传输协议和序列化协议,只不过都是使用的公有协议,即HTTP和JSON。而RPC一般使用私有协议,也就是自定义tcp协议,主要目的是为了提高效率。当然,RPC也可以使用HTTP作为传输协议,只不过一般不这样做。
下面通过一个例子来简单说明。
通用定义的http1.1协议的tcp报文包含很多信息,一个POST协议的格式大致如下:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
<html>
<body>Hello World</body>
</html>
如上图所示,使用的报文中有效字节(报文体)数仅仅占约 30%,剩下的70%报文头对后端进程通信来说都是无效的。
假如我们使用自定义tcp协议的报文如下:
报头占用的字节数也就只有16个byte,极大地精简了传输内容。
显而易见,传输相同的内容,使用自定义的tcp协议比使用http协议效率更高。
但随着http2.0版本的推出,对编码的效率进行了优化,使用http2.0作为传输协议实际上也是能接受的。Google推出的grpc的传输协议就是使用的http2.0协议。
既然如此,为什么还要用Rpc呢?
效率是一个方面,另一方面还是由于rpc在长期的发展过程中,封装了服务发现,负载均衡,熔断降级这类面向服务的高级特性,针对服务的可用性方面做了很多优化,而Restful API则缺少这些特性。
RPC框架现状与发展
目前常用的RPC框架如下:
dubbo,由阿里巴巴开源的,现已成为apache的顶级项目,https://dubbo.apache.org。
motan,新浪微博使用,https://github.com/weibocom/motan。
rpcx,基于 golang 的,https://github.com/smallnest/rpcx。
thrift,facebook开源的跨语言框架,https://thrift.apache.org。
finagle,twitter开源的,https://twitter.github.IO/finagle。
gRPC,Google开源的,http://www.grpc.IO (Google inside use Stuppy)。
这些RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。
服务治理型 RPC 框架
服务治理型的 RPC 框架有 dubbo、dubboX(当当基于dubbo的发展)等。
这类的 RPC 框架的特点是功能丰富,提供高性能的远程调用以及服务发现及治理功能,适用于大型服务的微服务化拆分以及管理,对于特定语言(Java)的项目可以十分友好的透明化接入。但缺点是语言耦合度较高,跨语言支持难度较大。
跨语言调用型
跨语言调用型的 RPC 框架有 Thrift、gRPC 等。
这一类的 RPC 框架重点关注于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合于为不同语言提供通用远程服务的场景。但这类框架没有服务发现相关机制,实际使用时一般需要代理层进行请求转发和负载均衡策略控制。
Dubbo
Thrift
gRPC
RPC框架与微服务
很多场景下,微服务框架与rpc框架的概念是混用的。但从严格意义上来讲,rpc框架只是微服务框架的一个子集。
微服务更多的是一种设计理念。为了实现这一设计理念,必然要使用多种不同的开发工具,比如rpc,分布式配置中心,服务熔断,服务网关等。
SpringCloud就是典型的微服务框架,提供了一整套的微服务解决方案,Rpc只是它的一个子集,主要解决了微服务之间的通信问题。
微服务的发展趋势
在当前阶段,rpc框架还是主流的微服务通信的解决方案。但rpc框架也存在一些本质的问题:
- 框架本身屏蔽了分布式系统通信的一些通用功能实现细节,但开发者却要花更多精力去掌握和管理复杂的框架本身,在实际应用中,追踪和解决框架出现的问题也绝非易事;
- 框架通常只支持一种或几种特定的语言,而微服务的设计理念里面,一个重要的特性就是语言无关。如果某个服务的编写语言不被框架支持,那就很难融入到面向微服务的架构体系中去;
- 框架以lib库的形式和服务联编,复杂项目依赖时的库版本兼容问题非常麻烦。同时,lib库的升级也无法对服务透明,服务会因为和业务无关的lib库升级而被迫升级。
因此Service Mesh应运而生。
Service Mesh将分布式服务的通信抽象为单独一层,在这一层中实现负载均衡、服务发现、认证授权、监控追踪、流量控制等分布式系统所需的功能,作为一个和服务对等的代理服务(Sidecar),和服务部署在一起,接管服务的流量,通过代理之间的通信间接完成服务之间的通信请求。
Service Mesh功能在于处理服务间通信,职责是实现请求的可靠传递。事实上,Service Mesh由服务与轻量级网络代理(Sidecar)组合而成。当微服务集群扩大到一定规模后,就会形成网格状,即形成了Service Mesh形态。如下所示:
Service Mesh接管整个网络,在服务之间转发所有的请求。业务服务只负责业务处理,而服务间的通信就从服务里面剥离出来,呈现出一个抽象的基础设施层。
Service Mesh的核心价值在于:
- 微服务基础设施下沉:微服务架构支撑,网络通信,治理等相关能力下沉到基础设施层,业务部门无需投入专人开发和维护。
- 降低升级成本:sidecar支持热升级,降低中间件和技术框架客户端,sdk升级成本。
- 语言无关
存在的问题:
- 服务间调用增加了两层sidecar,请求链路变长,带来网络延迟,CPU消耗增加的问题。
- 代理的稳定性成为关键路径,中心化的控制平面也会带来稳定性风险。
- 将系统复杂性转移到了基础设施层,需要专人进行维护和问题排查。
不适用场景:
- 技术栈稳定,无需服务治理/监控等强迭代需求。
- 无长期技术/运维投入。
参考资料
[1]. https://blog.csdn.net/yjp198713/article/details/79410521
[2]. https://zhuanlan.zhihu.com/p/59516670
[3]. https://www.zhihu.com/question/25536695