1. 什么是远程调用呢
通常我们将其简称为 RPC(Remote Procedure Call)即“远程过程调用”,简单来说就是调用方和被调用方不在一个机器上,我们可以比对着“本地过程调用”去理解这一概念。
1)本地过程调用
本地过程调用,其实就是调用方和被调用方在同一个机器上、甚至是同一个进程中,这样它们相互调用起来相对“远程过程调用”就容易太多了。
比如我们在项目的某个类中写了个 public int add(int a, int b) 的方法, 然后我们在这个项目的其它方法中去调用它,这样的调用方式就是“本地过程调用”。或者我们调用一些操作系统内核函数时,虽然不是同一个进程中,但也属于同一个机器上,也是“本地过程调用”。
2)远程过程调用
而与“本地过程调用”不同的是,“远程过程调用”的调用双方并不在同一个机器上,甚至可能相隔万里。这种情况下,就需要借助网络去传输调用过程中的参数信息及返回结果等信息,这个过程就要比“本地过程调用”繁琐很多了。
当然在绝大多数情况下,我们不会自己去实现 RPC 的数据封装和网络传输,而是直接使用 RPC 框架,在框架中已经帮我们做了这些繁琐事情,可以让我们使用起来能和“本地过程调用”有相同的感受。
2. 为什么要使用 RPC
为什么要使用 RPC 呢,直接把各种功能写在同一个系统中不就好了?那么,假设我们要写一个“京东”的订单系统,当用户挑选好商品后,由系统来创建对应的订单,此时一个简化的流程如下:
由上图可以看到,我们创建订单时,需要锁定商品的库存,以此来防止商品被超卖。并且从上图也可以明显看出,库存锁定方法也是由“订单系统”自己实现的,所以直接调用自身的库存锁定的方法即可。
不过实际情况并不会这么做,而是会将这些功能拆分到多个系统中,比如上面的功能可以拆分成“订单系统”和“库存系统”中,然后通过 RPC 来做系统之间的调用。那么,到底是什么原因导致我们要拆分系统呢,如果非要把所有功能写在一个系统中会怎么样?
1)代码维护成本
当系统功能非常多时,它的代码量也是巨大的,这也意味着会有很多人参与维护这些代码。当几十人甚至几百人去维护同一份代码,就会有非常多的代码合并冲突要处理,并且经常会出现我改动了你的代码,你又影响了我的代码逻辑,这样导致你我的代码都得重新测试,会极大增加测试人员的工作量。而且每次上线时可能会涉及到几十个功能,一旦某个小功能出问题,整个系统的所有功能都要被迫回滚,这样每次上线对我们来说都是一种巨大的挑战,也会让系统面临巨大的风险。
所以单从代码维护的角度上,就足以说明拆分系统的必要性。我们将系统功能梳理拆分到各个小系统中,每个小系统有两三个人维护就足够了,这样每个系统的代码各自独立,各自有自己的机器,上线完全可以根据自己的节奏来。只有合理的拆分系统,才能保证团队整体的开发效率,否则系统太过臃肿导致后续难以维护。
2)资源的利用率
当用户体量不断提升时,或者做活动搞促销时,系统中的某些功能模块的压力会增大,这时候我们就需要对机器进行扩容。由于全部的业务功能都是同一个系统中,有些功能对 CPU 要求较高,有些功能对内存使用较多,还有些功能对磁盘 I/O 开销较大,这样我们很难实现精准扩容。并且一些核心功能在促销时流量会有比较大的增长,而有些非核心的功能的流量增长可能不会有那么大,扩容时只能按照核心服务要求的容量标准来扩,但实际上这些增长的容量对那些非核心服务来说可能是浪费的。
另外还有一点,有些情况下我们是允许某些功能暂时不可用的,也就是说可以容忍局部的不可用但整体可用。比如因为我们做活动导致流量突增,导致登录功能会出现较高的延迟,但是其它功能不能受影响,也就是我们可以牺牲小部分用户的体验,而保障整体的可靠性。所以将系统拆分也是我们保证系统稳定的一种重要手段。
3. 一次 RPC 经历过程
现在我们就把之前下单的功能拆分到“订单系统”和“库存系统”中,并通过 RPC 进行系统间的调用,此时流程大致如下:
由上图可以看到,当“订单系统”收到商品的建单请求后,会通过 RPC 调用“库存系统”对商品库存进行锁定,当库存锁定成功后,再继续完成订单的创建。
既然我们决定拆分系统并使用 RPC 调用,那么我们就再了解下 RPC 的调用过程,看看我们的数据是如何被处理并传送到了另一台机器上的,整个过程如下图所示:
从上图可以看到一次 RPC 调用的过程是非常复杂的,在调用过程中会通过网络传输数据(使用 TCP 或 UDP),但此时我们突然想起来 HTTP 调用也是通过网络进行调用,并且调用方和被调用方也不在一台机器上,那么 RPC 调用和 HTTP 调用相比的区别是什么呢?我们直接使用 HTTP 进行系统间的调用不是也挺好吗,为什么又要提出一个 RPC 的概念呢?
1)使用场景不同
HTTP 多用于客户端和服务端之间的调用,比如浏览器、IOS客户端、安卓客户端等,它们大多通过 HTTP 调用服务端,而服务端之间大多采取 RPC 的形式进行调用。当然了,这也并不绝对,我们也完全可以使用 RPC 做前后端之间的调用,也可以使用 HTTP 做服务端之间的调用。
2)传输格式不同
使用 HTTP 调用时,使用的是 HTTP 协议(HTTP 协议有 HTTP1.0、HTTP1.1 和 HTTP2.0,感兴趣的可以自行百度)。而使用 RPC 调用时,可以自由的选择不同协议,也可以自定义协议,甚至也可以直接使用 HTTP 协议。不过 HTTP 协议由于加上了各种消息头,导致消息数据会比较长,而 RPC 调用一般都比较追求传输的性能,所以都会选择一些数据精简的协议格式,这样传输的数据量小,速度会更快些。
3)封装程度不同
上面我们说过 HTTP 也可以做服务端之间的调用,那么为什么不使用它而使用 RPC 呢?或者说使用 RPC 的优势在哪?
原因主要有两个方面,一方面是后端系统之间使用 HTTP 进行调用并不方便,即使有 HttpClient 这样的工具,依然需要我们自己去做一些额外的处理。而 RPC 可以让我们像调用本地服务一样调用远程服务,我们无需关心其中的细节,代码也会看起来更加简洁和美观。这其实就是 RPC 帮我们做了更多的封装,更方便于后端系统之间的相互调用,可以看作 RPC 是在 HTTP 的基础上又做了一层封装。
而另一方面刚刚在讲传输格式时也提过,就是 HTTP 协议比较臃肿,导致我们每次调用时传输的数据会比较多,从而影响了调用的性能。而使用 RPC 时我们可以自由选择协议格式,这样我们会选择一些较为精简的协议格式,以此来提高网络传输效率,也提高了调用的性能。
4. 成熟的 RPC 框架
上面我们说的 RPC 中的处理细节并不需要我们自己实现,目前已经有成熟的 RPC 框架可以供我们选择,如下图所示用虚线圈起来的部分是由 RPC 框架来提供的。
1)最初由阿里开源的 Dubbo 框架
Dubbo 可能很多人都听说过,它是由阿里最初于 2011 年开源的一款 Java RPC 框架,后来停止维护了一段时间后又重启,在 2018 年时阿里将其贡献给了 Apache 软件基金会,后来在 2019 年成为 Apache 的顶级项目 Apache Dubbo。
在网络上 Apache Dubbo 的相关资料很丰富,其使用也比较方便,并且可以和 Spring 框架集成。而在其功能方面,Apache Dubbo 除了能为我们提供基本的 RPC 功能外,它还支持服务注册与发现、服务治理、注册中心等重要功能(这些功能的作用后面我们再统一解释下),这也意味着 Apache Dubbo 不仅仅是一个 RPC 框架,它更是一个服务治理框架(什么是服务治理我们后面再讲)。
2)最初由 facebook 开源的 thrift 框架
Thrift 最初由 facebook 开发,并在 2007 年贡献给了 Apache 软件基金会,成为 Apache 的顶级项目 Apache Thrift。
与 Dubbo 不同的是,虽然 Thrift 由 Java 语言编写,但它是一款跨语言的 RPC 框架。用户通过 Thrift IDL(接口定义语言)描述接口和参数,然后通过 Thrift 提供的编译器并根据我们需要,生成不同语言的代码,例如: C++, C#, Cocoa, Erlang, Haskell, Java, Ocami, Perl, PHP, Python, Ruby, Smalltalk 等。
Thrift 的历史虽然悠久,但是网络上能深入讲解 Thrift 的文章几乎没有,大多都是停留在 Thrift IDL 语法及 Thrift 结构简单讲解的层次上。
3)谷歌开源的 gRPC 框架
Google发布的开源RPC框架,高性能、开源、将移动和HTTP/2放在首位的通用的RPC框架,基于HTTP/2, netty4.1, proto3, 拥有非常丰富而实用的特性,堪称RPC 框架的典范。
但它本身它不是分布式的,所以要实现上面的框架的功能需要进一步的开发。
4)Hessian
是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 基于HTTP协议,采用二进制编解码。
利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化。
gRPC | dubbo | thrift | Hessian | |
---|---|---|---|---|
支持语言 | ---- | ---- | ---- | ---- |
服务治理 | ---- | ---- | ---- | ---- |
资料数量 | ---- | ---- | ---- | ---- |