gitbook/分布式技术原理与算法解析/docs/160408.md
2022-09-03 22:05:03 +08:00

210 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 19 | 分布式通信之远程调用:我是你的千里眼
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
在前面三个模块中,我带你学习了分布式领域中的分布式协调与同步、分布式资源管理与负载调度,以及分布式计算技术,相信你对分布式技术已经有了一定的了解。
通过前面的学习,不知道你有没有发现分布式的本质就是多进程协作,共同完成任务。要协作,自然免不了通信。那么,多个进程之间是如何通信的呢?这也就是在“第四站:分布式通信技术”模块中,我将要为你讲解的问题。
话不多说,接下来我们就一起进入分布式通信的世界吧。今天,我首先带你打卡的是,分布式通信中的远程调用。
## 什么是远程调用?
首先,我通过一个例子,来让你对远程调用和本地调用有一个直观了解。
以电商购物平台为例每一笔交易都涉及订单系统、支付系统和库存系统假设三个系统分别部署在三台机器A、B、C中独立运行订单交易流程如下所示
1. 用户下单时调用本地机器A的订单系统进行下单
2. 下单完成后会远程调用机器B上的支付系统进行支付待支付完成后返回结果之后在本地更新订单状态
3. 在本地远程调用机器C上的仓库系统出货出货完成后返回出货结果。
在整个过程中,“下单”和“订单状态更新”两个操作属于本地调用,而“支付”和“出货”这两个操作是通过本地的订单系统调用其他两个机器上的函数(方法)实现的,属于远程调用。
整个订单交易流程如下图所示。
![](https://static001.geekbang.org/resource/image/b3/c7/b3c2c87a63ae80d09ed48bb692fa98c7.png)
通过这个例子,你应该对本地调用和远程调用有了一个初步的认识了。那到底什么是本地调用,什么是远程调用呢?
**本地调用**通常指的是,进程内函数之间的相互调用;而**远程调用**,是进程间函数的相互调用,是一种进程间通信模式。通过远程调用,一个进程可以看到其他进程的函数、方法等,这是不是与我们通常所说的“千里眼”有点类似呢?
在分布式领域中,一个系统由很多服务组成,不同的服务由各自的进程单独负责。因此,远程调用在分布式通信中尤为重要。
根据进程是否部署在同一台机器上,远程调用可以分为如下两类:
* **本地过程调用Local Procedure CallLPC**是指同一台机器上运行的不同进程之间的互相通信即在多进程操作系统中运行的不同进程之间可以通过LPC进行函数调用。
* **远程过程调用Remote Procedure CallRPC**,是指不同机器上运行的进程之间的相互通信,某一机器上运行的进程在不知道底层通信细节的情况下,就像访问本地服务一样,去调用远程机器上的服务。
在这两种远程调用中RPC中的不同进程是跨机器的适用于分布式场景。因此在今天这篇文章中我主要针对RPC进行详细讲解。接下来我再提到远程调用时主要指的就是RPC了。
## 远程调用的原理及应用
我们经常会听别人提起 B/S ( Browser/Server浏览器/服务器) 架构。在这种架构中被调用方服务器有一个开放的接口然后调用方用户通过Browser使用这个接口来间接调用被调用方相应的服务从而实现远程调用。
比如用户A在自己的电脑上通过浏览器查询北京今天的天气 浏览器会将用户查询请求通过远程调用方式调用远程服务器相应的服务,然后为用户返回北京今天的天气预报。
但是B/S架构是基于HTTP协议实现的每次调用接口时都需要先进行HTTP请求。这样既繁琐又浪费时间不适用于有低时延要求的大规模分布式系统所以远程调用的实现大多采用更底层的网络通信协议。
接下来,我将为你介绍两种常用的远程调用机制:**远程过程调用RPC**(Remote Procedure Call)和**远程方法调用RMI**(Remote Method Invocation)。
首先我们一起看一下RPC的原理和应用吧。
### RPC的原理及应用
简单地说RPC就是像调用本机器上的函数或方法一样去执行远程机器上的函数或方法并返回结果。在整个过程中不感知底层具体的通信细节。
如下图所示我们以刚才电商购物平台例子中的“支付”操作为例来详细看看一次RPC调用的完整流程吧
![](https://static001.geekbang.org/resource/image/69/48/696562e99521599e71564557b4739048.png)
1. 本地服务器也就是机器A中的订单系统调用本地服务器上的支付系统中的支付操作Pay(Order)该方法会直接调用Client Stub其中Stub是用于转换RPC过程中在订单系统和支付系统所在机器之间传递的参数这是一次正常的本地调用。
2. Client Stub将方法Pay、参数Order等打包成一个适合网络传输的消息通过执行一次系统调用也就是调用操作系统中的函数来发送消息。
3. 订单系统所在机器A的本地操作系统通过底层网络通信将打包好的消息根据支付系统所在机器B的地址发送出去。
4. 机器B上的操作系统接收到消息后将消息传递给Server Stub。
5. 机器B上的Server Stub将接收到的消息进行解包获得里面的参数然后调用本地的支付订单的操作Pay(Order)。
6. 机器B上的支付操作Par(Order)完成后将结果发送给Server Stub其中结果可使用XDRExternal Data Representation一种可以在不同计算机系统间传输的数据格式语言表示。
7. 机器B上的Server Stub将结果数据打包成适合网络传输的消息然后进行一次系统调用发送消息。
8. 机器B的本地操作系统将打包好的消息通过网络发送回机器A。
9. 机器A的操作系统接收到来自机器B的消息并将消息发送给本地的Client Stub。
10. 本地的Client Stub对消息进行解包然后将解包得到的结果返回给本地的订单系统。
到此整个RPC过程结束。
从整个流程可以看出机器A上的Pay(Order)、 Client Stub 和网络调用之间的交互属于本地调用机器B上的Pay(Order)、Server Stub和网络调用之间的交互也属于本地调用。而机器A和机器B之间的远程调用的核心是发生在机器A上的网络调用和机器B上的网络调用。
RPC的目的其实就是要将第2到第8步的几个过程封装起来让用户看不到这些细节。从用户的角度看订单系统的进程只是做了一次普通的本地调用然后就得到了结果。
也就是说,**订单系统进程并不需要知道底层是如何传输的在用户眼里远程过程调用和调用一次本地服务没什么不同。这就是RPC的核心。**
接下来,我再带你一起看一下**RPC与本地调用进程内函数调用的区别**吧以加深你对RPC的理解。
你可以先想象一下,本地调用过程是怎样的。
简单来说,同一进程是共享内存空间的,用户可以通过{函数名+参数}直接进行函数调用。
而在RPC中由于不同进程内存空间无法共享且涉及网络传输所以不像本地调用那么简单。所以RPC与本地调用主要有三点不同。
**第一个区别是调用ID和函数的映射。**在本地调用中由于在进程内调用即使用的地址空间是同一个因此程序可直接通过函数名来调用函数。而函数名的本质就是一个函数指针可以看成函数在内存中的地址。比如调用函数f()编译器会帮我们找到函数f()相应的内存地址。但在RPC中由于不同进程的地址空间不一样因此单纯通过函数名去调用相应的服务是不行的。
所以在RPC中所有的函数必须要有一个调用ID来唯一标识。一个机器上运行的进程在做远程过程调用时必须附上这个调用ID。
另外我们还需要在通信的两台机器间分别维护一个函数与调用ID的映射表。两台机器维护的表中相同的函数对应的调用ID必须保持一致。
当一台机器A上运行的进程P需要远程调用时它就先查一下机器A维护的映射表找出对应的调用ID然后把它传到另一台机器B上机器B通过查看它维护的映射表从而确定进程P需要调用的函数然后执行对应的代码最后将执行结果返回到进程P。
**第二个区别是,序列化和反序列化。**我们知道了调用方调用远程服务时需要向被调用方传输调用ID和对应的函数参数那调用方究竟是怎么把这些数据传给被调用方的呢
在本地调用中进程之间共享内存等因此我们只需要把参数压到栈里然后进程自己去栈里读取就行。但是在RPC中两个进程分布在不同的机器上使用的是不同机器的内存因此不可能通过内存来传递参数。
而网络协议传输的内容是二进制流,无法直接传输参数的类型,因此这就需要调用方把参数先转成一个二进制流,传到被调用方后,被调用方再把二进制流转换成自己能读取的格式。调用方将参数转换成二进制流,通常称作序列化。被调用方对二进制的转换通常叫作反序列化。
同理被调用方将结果返回给调用方也需要有序列化和反序列化的过程。也就是说RPC与本地调用相比参数的传递需要进行序列化和反序列化操作。
**第三个区别是,网络传输协议。**序列化和反序列化解决了调用方和被调用方之间的数据传输格式问题但要想序列化后的数据能在网络中顺利传输还需要有相应的网络协议比如TCP、UDP等因此就需要有一个底层通信层。
调用方通过该通信层把调用ID和序列化后的参数传给被调用方被调用方同样需要该通信层将序列化后的调用结果返回到调用方。
也就是说只要调用方和被调用方可以互传数据就可以作为这个底层通信层。因此它所使用的网络协议可以有很多只要能完成网络传输即可。目前来看大部分RPC框架采用的是TCP协议。
说完RPC的核心原理下面我以一个具有代表性的**RPC框架Apache Dubbo为例**帮助你更加深入地了解RPC。
在讲解Dubbo之前你可以先想一下如果你是一个RPC框架的设计者你会如何设计呢
首先必须得有服务的提供方和调用方。如下图所示假设服务提供方14为调用方14提供服务每个调用方都可以任意访问服务提供方。
![](https://static001.geekbang.org/resource/image/93/1f/93ec9b3b937d74c4729e32eefa5a361f.png)
当服务提供方和服务调用方越来越多时服务调用关系会愈加复杂。假设服务提供方有n个 服务调用方有m个则调用关系可达n\*m这会导致系统的通信量很大。此时你可能会想到为什么不使用一个服务注册中心来进行统一管理呢这样调用方只需要到服务注册中心去查找相应的地址即可。
这个想法很好,如下图所示,我们在服务调用方和服务提供方之间增加一个服务注册中心,这样调用方通过服务注册中心去访问提供方相应的服务,这个服务注册中心相当于服务调用方和提供方的中心枢纽。
![](https://static001.geekbang.org/resource/image/d8/87/d8acf9be105b8235f79e3566839ec987.png)
这样是不是好多了呢?
Dubbo就是在引入服务注册中心的基础上又加入了监控中心组件用来监控服务的调用情况以方便进行服务治理实现了一个RPC框架。如下图所示Dubbo的架构主要包括4部分
* **服务提供方。**服务提供方会向服务注册中心注册自己提供的服务。
* **服务注册中心。**服务注册与发现中心,负责存储和管理服务提供方注册的服务信息和服务调用方订阅的服务类型等。
* **服务调用方。**根据服务注册中心返回的服务所在的地址列表,通过远程调用访问远程服务。
* **监控中心。**主要统计服务的调用次数和调用时间等信息,以方便进行服务管理或服务失败分析等。
![](https://static001.geekbang.org/resource/image/22/5c/228161058c3055c13d9592ba47626f5c.png)
可以看到Dubbo的大致工作流程如下
1. 服务提供方将自身提供的服务注册到服务注册中心;
2. 服务调用方需要向注册中心预订调用服务的提供方地址列表;
3. 服务注册中心将服务对应的提供方地址列表返回给调用方;
4. 服务调用方根据服务地址信息进行远程服务调用;
5. 服务调用方和服务提供方定时向监控中心发送服务调用次数及调用时间等信息。
接下来我再带你学习另一个远程调用机制RMI。
### RMI的原理及应用
RMI是一个用于实现RPC的Java API能够让本地Java虚拟机上运行的对象调用远程方法如同调用本地方法隐藏通信细节。
RMI可以说是RPC的一种具体形式其原理与RPC基本一致唯一不同的是**RMI是基于对象的充分利用了面向对象的思想去实现整个过程其本质就是一种基于对象的RPC实现**。
RMI的具体原理如下图所示
![](https://static001.geekbang.org/resource/image/49/d2/4919fcec189fe095d503e421cd9894d2.png)
RMI的实现中客户端的订单系统中的Stub是客户端的一个辅助对象用于与服务端实现远程调用服务端的支付系统中Skeleton是服务端的一个辅助对象用于与客户端实现远程调用。
也就是说客户端订单系统的Pay(Order)调用本地Stub对象上的方法Stub对调用信息比如变量、方法名等进行打包然后通过网络发送给服务端的Skeleton对象Skeleton对象将收到的包进行解析并调用服务端Pay(Order)系统中的相应对象和方法进行计算,计算结果又会以类似的方式返回给客户端。
为此,我们可以看出,**RMI与PRC最大的不同在于调用方式和返回结果的形式**RMI通过对象作为远程接口来进行远程方法的调用返回的结果也是对象形式比如Java对象类型或者是基本数据类型等。
RMI的典型实现框架有EJBEnterprise JavaBean企业级JavaBean如果你需要深入了解这个框架的话可以参考其官方文档。
### RPC与RMI对比分析
好了上面我带你学习了RPC和RMI接下来我通过一个表格来对比下它们的异同吧以方便你进一步理解与记忆。
![](https://static001.geekbang.org/resource/image/95/18/95f17b69cc412d690ef8c9abf9c47318.jpg)
## 知识扩展:远程过程调用存在同步和异步吗?
分布式领域中,我们经常会听到同步和异步这两个词,那么远程过程调用存在同步和异步吗?
答案是肯定的。
远程过程调用包括同步调用和异步调用两种,它们的含义分别是:
* 同步调用指的是调用方等待被调用方执行完成并返回结果。这就好比在现实生活中用户A让用户B完成一篇文章用户A就在那里等着一直等用户B将写好的文章交给用户A后才离开并对文章进行审核。
* 异步调用指的是调用方调用后不用等待被调用方执行结果即返回返回结果调用方可以通过回调通知等方式获取。这就好比在现实生活中用户A让用户B完成一篇文章用户A告知用户B后用户A离开去做其他事情当用户B完成文章后反馈给用户A用户A收到反馈后开始审核文章。
也就是说,**同步调用和异步调用的区别是,是否等待被调用方执行完成并返回结果**。
因此,同步调用通常适用于需要关注被调用方计算结果的场景,比如用户查询天气预报,调用方需要直接返回结果;异步调用通常适用于对响应效率要求高、但对结果正确性要求相对较低的场景,比如用户下发部署一个任务,但真正执行该任务需要进行资源匹配和调度、进程拉起等过程,时间比较长,如果用户进程阻塞在那里,会导致体验很差,这种情况下可以采用异步调用。
## 总结
今天,我主要与你分享了分布式通信中的远程调用。
我以电商购物平台为例首先让你对本地调用和远程调用有了一定的认识然后分析了两种常用的远程调用机制RPC和RMI并对两者进行了比较。除此之外我还介绍了Dubbo这个代表性的RPC框架。
接下来,我们再回顾下今天涉及的几个与远程调用相关的核心概念吧。
**本地调用**通常指的是同一台机器进程间函数的相互调用,而**远程调用**是指不同机器进程间函数的相互调用。
**RPC**是指调用方通过参数传递的方式调用远程服务并得到返回的结果。在整个过程中RPC会隐藏具体的通信细节使得调用方就像在调用本地函数或方法一样。
**RMI**可以说是一个用于实现RPC的Java API能够让本地Java虚拟机上运行的对象调用远程方法如同调用本地方法隐藏通信细节。
**Dubbo**是一个代表性的RPC框架服务提供方首先将自身提供的服务注册到注册中心调用方通过注册中心获取提供的相对应的服务地址列表然后选择其中一个地址去调用相应的服务。
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
![](https://static001.geekbang.org/resource/image/e9/02/e923a661c2d404562f4656eefed38702.png)
现在是不是觉得RPC没有之前那么神秘了呢如果你对RPC感兴趣的话Dubbo就是一个很棒的出发点。加油赶紧开启你的分布式通信之旅吧。
## 思考题
在Dubbo中引入了一个注册中心来存储服务提供方的地址列表若服务消费方每次调用时都去注册中心查询地址列表如果频繁查询会导致效率比较低你会如何解决这个问题呢
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!