mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-19 15:43:44 +08:00
del
This commit is contained in:
209
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/19 | 分布式通信之远程调用:我是你的千里眼.md
Normal file
209
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/19 | 分布式通信之远程调用:我是你的千里眼.md
Normal file
@@ -0,0 +1,209 @@
|
||||
<audio id="audio" title="19 | 分布式通信之远程调用:我是你的千里眼" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/06/7282f4c85130fa2cf96b4868574b8806.mp3"></audio>
|
||||
|
||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
|
||||
|
||||
在前面三个模块中,我带你学习了分布式领域中的分布式协调与同步、分布式资源管理与负载调度,以及分布式计算技术,相信你对分布式技术已经有了一定的了解。
|
||||
|
||||
通过前面的学习,不知道你有没有发现分布式的本质就是多进程协作,共同完成任务。要协作,自然免不了通信。那么,多个进程之间是如何通信的呢?这也就是在“第四站:分布式通信技术”模块中,我将要为你讲解的问题。
|
||||
|
||||
话不多说,接下来我们就一起进入分布式通信的世界吧。今天,我首先带你打卡的是,分布式通信中的远程调用。
|
||||
|
||||
## 什么是远程调用?
|
||||
|
||||
首先,我通过一个例子,来让你对远程调用和本地调用有一个直观了解。
|
||||
|
||||
以电商购物平台为例,每一笔交易都涉及订单系统、支付系统和库存系统,假设三个系统分别部署在三台机器A、B、C中独立运行,订单交易流程如下所示:
|
||||
|
||||
1. 用户下单时,调用本地(机器A)的订单系统进行下单;
|
||||
1. 下单完成后,会远程调用机器B上的支付系统进行支付,待支付完成后返回结果,之后在本地更新订单状态;
|
||||
1. 在本地远程调用机器C上的仓库系统出货,出货完成后返回出货结果。
|
||||
|
||||
在整个过程中,“下单”和“订单状态更新”两个操作属于本地调用,而“支付”和“出货”这两个操作是通过本地的订单系统调用其他两个机器上的函数(方法)实现的,属于远程调用。
|
||||
|
||||
整个订单交易流程如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/c7/b3c2c87a63ae80d09ed48bb692fa98c7.png" alt="">
|
||||
|
||||
通过这个例子,你应该对本地调用和远程调用有了一个初步的认识了。那到底什么是本地调用,什么是远程调用呢?
|
||||
|
||||
**本地调用**通常指的是,进程内函数之间的相互调用;而**远程调用**,是进程间函数的相互调用,是一种进程间通信模式。通过远程调用,一个进程可以看到其他进程的函数、方法等,这是不是与我们通常所说的“千里眼”有点类似呢?
|
||||
|
||||
在分布式领域中,一个系统由很多服务组成,不同的服务由各自的进程单独负责。因此,远程调用在分布式通信中尤为重要。
|
||||
|
||||
根据进程是否部署在同一台机器上,远程调用可以分为如下两类:
|
||||
|
||||
- **本地过程调用(Local Procedure Call,LPC)**,是指同一台机器上运行的不同进程之间的互相通信,即在多进程操作系统中,运行的不同进程之间可以通过LPC进行函数调用。
|
||||
- **远程过程调用(Remote Procedure Call,RPC)**,是指不同机器上运行的进程之间的相互通信,某一机器上运行的进程在不知道底层通信细节的情况下,就像访问本地服务一样,去调用远程机器上的服务。
|
||||
|
||||
在这两种远程调用中,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调用的完整流程吧:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/48/696562e99521599e71564557b4739048.png" alt="">
|
||||
|
||||
1. 本地服务器也就是机器A中的订单系统,调用本地服务器上的支付系统中的支付操作Pay(Order),该方法会直接调用Client Stub(其中,Stub是用于转换RPC过程中在订单系统和支付系统所在机器之间传递的参数),这是一次正常的本地调用。
|
||||
1. Client Stub将方法Pay、参数Order等打包成一个适合网络传输的消息,通过执行一次系统调用(也就是调用操作系统中的函数)来发送消息。
|
||||
1. 订单系统所在机器A的本地操作系统通过底层网络通信,将打包好的消息根据支付系统所在机器B的地址发送出去。
|
||||
1. 机器B上的操作系统接收到消息后,将消息传递给Server Stub。
|
||||
1. 机器B上的Server Stub将接收到的消息进行解包,获得里面的参数,然后调用本地的支付订单的操作Pay(Order)。
|
||||
1. 机器B上的支付操作Par(Order)完成后,将结果发送给Server Stub,其中结果可使用XDR(External Data Representation,一种可以在不同计算机系统间传输的数据格式)语言表示。
|
||||
1. 机器B上的Server Stub将结果数据打包成适合网络传输的消息,然后进行一次系统调用发送消息。
|
||||
1. 机器B的本地操作系统将打包好的消息通过网络发送回机器A。
|
||||
1. 机器A的操作系统接收到来自机器B的消息,并将消息发送给本地的Client Stub。
|
||||
1. 本地的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框架的设计者,你会如何设计呢?
|
||||
|
||||
首先必须得有服务的提供方和调用方。如下图所示,假设服务提供方1~4为调用方1~4提供服务,每个调用方都可以任意访问服务提供方。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/1f/93ec9b3b937d74c4729e32eefa5a361f.png" alt="">
|
||||
|
||||
当服务提供方和服务调用方越来越多时,服务调用关系会愈加复杂。假设服务提供方有n个, 服务调用方有m个,则调用关系可达n*m,这会导致系统的通信量很大。此时,你可能会想到,为什么不使用一个服务注册中心来进行统一管理呢,这样调用方只需要到服务注册中心去查找相应的地址即可。
|
||||
|
||||
这个想法很好,如下图所示,我们在服务调用方和服务提供方之间增加一个服务注册中心,这样调用方通过服务注册中心去访问提供方相应的服务,这个服务注册中心相当于服务调用方和提供方的中心枢纽。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/87/d8acf9be105b8235f79e3566839ec987.png" alt="">
|
||||
|
||||
这样是不是好多了呢?
|
||||
|
||||
Dubbo就是在引入服务注册中心的基础上,又加入了监控中心组件(用来监控服务的调用情况,以方便进行服务治理),实现了一个RPC框架。如下图所示,Dubbo的架构主要包括4部分:
|
||||
|
||||
- **服务提供方。**服务提供方会向服务注册中心注册自己提供的服务。
|
||||
- **服务注册中心。**服务注册与发现中心,负责存储和管理服务提供方注册的服务信息和服务调用方订阅的服务类型等。
|
||||
- **服务调用方。**根据服务注册中心返回的服务所在的地址列表,通过远程调用访问远程服务。
|
||||
- **监控中心。**主要统计服务的调用次数和调用时间等信息,以方便进行服务管理或服务失败分析等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/22/5c/228161058c3055c13d9592ba47626f5c.png" alt="">
|
||||
|
||||
可以看到,Dubbo的大致工作流程如下:
|
||||
|
||||
1. 服务提供方将自身提供的服务注册到服务注册中心;
|
||||
1. 服务调用方需要向注册中心预订调用服务的提供方地址列表;
|
||||
1. 服务注册中心将服务对应的提供方地址列表返回给调用方;
|
||||
1. 服务调用方根据服务地址信息进行远程服务调用;
|
||||
1. 服务调用方和服务提供方定时向监控中心发送服务调用次数及调用时间等信息。
|
||||
|
||||
接下来,我再带你学习另一个远程调用机制RMI。
|
||||
|
||||
### RMI的原理及应用
|
||||
|
||||
RMI是一个用于实现RPC的Java API,能够让本地Java虚拟机上运行的对象调用远程方法如同调用本地方法,隐藏通信细节。
|
||||
|
||||
RMI可以说是RPC的一种具体形式,其原理与RPC基本一致,唯一不同的是**RMI是基于对象的,充分利用了面向对象的思想去实现整个过程,其本质就是一种基于对象的RPC实现**。
|
||||
|
||||
RMI的具体原理如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/d2/4919fcec189fe095d503e421cd9894d2.png" alt="">
|
||||
|
||||
RMI的实现中,客户端的订单系统中的Stub是客户端的一个辅助对象,用于与服务端实现远程调用;服务端的支付系统中Skeleton是服务端的一个辅助对象,用于与客户端实现远程调用。
|
||||
|
||||
也就是说,客户端订单系统的Pay(Order)调用本地Stub对象上的方法,Stub对调用信息(比如变量、方法名等)进行打包,然后通过网络发送给服务端的Skeleton对象,Skeleton对象将收到的包进行解析,并调用服务端Pay(Order)系统中的相应对象和方法进行计算,计算结果又会以类似的方式返回给客户端。
|
||||
|
||||
为此,我们可以看出,**RMI与PRC最大的不同在于调用方式和返回结果的形式**,RMI通过对象作为远程接口来进行远程方法的调用,返回的结果也是对象形式,比如Java对象类型,或者是基本数据类型等。
|
||||
|
||||
RMI的典型实现框架有EJB(Enterprise JavaBean,企业级JavaBean),如果你需要深入了解这个框架的话,可以参考其官方文档。
|
||||
|
||||
### RPC与RMI对比分析
|
||||
|
||||
好了,上面我带你学习了RPC和RMI,接下来我通过一个表格来对比下它们的异同吧,以方便你进一步理解与记忆。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/18/95f17b69cc412d690ef8c9abf9c47318.jpg" alt="">
|
||||
|
||||
## 知识扩展:远程过程调用存在同步和异步吗?
|
||||
|
||||
分布式领域中,我们经常会听到同步和异步这两个词,那么远程过程调用存在同步和异步吗?
|
||||
|
||||
答案是肯定的。
|
||||
|
||||
远程过程调用包括同步调用和异步调用两种,它们的含义分别是:
|
||||
|
||||
- 同步调用,指的是调用方等待被调用方执行完成并返回结果。这就好比在现实生活中,用户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框架,服务提供方首先将自身提供的服务注册到注册中心,调用方通过注册中心获取提供的相对应的服务地址列表,然后选择其中一个地址去调用相应的服务。
|
||||
|
||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/02/e923a661c2d404562f4656eefed38702.png" alt="">
|
||||
|
||||
现在,是不是觉得RPC没有之前那么神秘了呢?如果你对RPC感兴趣的话,Dubbo就是一个很棒的出发点。加油,赶紧开启你的分布式通信之旅吧。
|
||||
|
||||
## 思考题
|
||||
|
||||
在Dubbo中引入了一个注册中心来存储服务提供方的地址列表,若服务消费方每次调用时都去注册中心查询地址列表,如果频繁查询,会导致效率比较低,你会如何解决这个问题呢?
|
||||
|
||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
|
||||
162
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/20 | 分布式通信之发布订阅:送货上门.md
Normal file
162
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/20 | 分布式通信之发布订阅:送货上门.md
Normal file
@@ -0,0 +1,162 @@
|
||||
<audio id="audio" title="20 | 分布式通信之发布订阅:送货上门" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/f7/91f6b30857ecca3f9b39bc680f5795f7.mp3"></audio>
|
||||
|
||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
|
||||
|
||||
在上一篇文章中,我带你一起学习了分布式通信中的远程调用。远程调用的核心是在网络服务层封装了通信协议、序列化、传输等操作,让用户调用远程服务如同进行本地调用一样。
|
||||
|
||||
其实,这种方式就是通过网络服务层的封装实现了不同机器上不同进程之间的直接通信,因为是直接通信,所以通过线程阻塞的方式实现同步调用比较容易,因此通常被用于同步调用。比如,机器1上的进程A调用机器2上的进程B,进程A被挂起,进程B开始执行,当进程B将值返回给A时,A继续执行。
|
||||
|
||||
虽然这种方式也可以用于异步通信,但因为进程之间是直接交互的,所以当进程比较多时,会导致进程维护通信的复杂度非常高,且一个进程通信接口改变,与其通信的进程都会受到影响。
|
||||
|
||||
随着业务和分布式计算规模的逐渐增大和复杂化,远程调用模型有点心有余力而不足了,为此出现了专门的异步通信模式,也就是消息发布订阅模式和消息队列模式。在接下来的两篇文章中,我将与你详细讲述这两种通信模式。
|
||||
|
||||
话不多说,今天,我就带你一起打卡分布式通信中的发布订阅模式吧。
|
||||
|
||||
## 什么是发布订阅?
|
||||
|
||||
其实,发布订阅的思想在我们的生活中随处可见。
|
||||
|
||||
比如,学术届电子论文的订阅方式。通常,各个会议方或出版社会将学术论文发布到论文网站(或平台上,比如ACM、知网等),然后学生或老师向论文网站订阅自己感兴趣的论文,比如分布式相关的、AI相关的等。
|
||||
|
||||
当会议方或出版社将论文发布到论文网站后,论文网站会根据订阅信息,将相应的论文推送给订阅者(比如通过邮件的方式)。这里的会议方或出版社就相当于生产者,负责发布论文,学生或老师就相当于消费者,而论文网站就相当于一个消息中心。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/0b/762052350ff125044669d64d2b18e10b.png" alt="">
|
||||
|
||||
由此可以看出,**发布订阅的三要素是生产者、消费者和消息中心,**生产者负责产生数据放到消息中心,消费者向消息中心订阅自己感兴趣的消息,当发布者推送数据到消息中心后,消息中心根据消费者订阅情况将相关数据推送给对应的订阅者。这种将数据送到消费者手里的行为,是不是和我们现在常说的“送货上门”一样呢?
|
||||
|
||||
## 发布订阅的原理及应用
|
||||
|
||||
这个论文订阅的例子,充分体现了发布订阅的思想。接下来,我就与你进一步分析下发布订阅的原理吧。
|
||||
|
||||
### 发布订阅的基本工作原理
|
||||
|
||||
在分布式通信领域中,消息系统一般有两种典型的模式。一种是点对点模式(P2P,Point to Point),另一种是发布订阅模式(Pub/Sub,Publish/Subscribe)。接下来,我们就一起看看这两种模式,以帮助你深入理解发布订阅模式的原理。
|
||||
|
||||
首先,我们一起看一下**什么是点对点模式**。
|
||||
|
||||
生产者将消息发送到消息中心,然后消费者从消息中心取出对应的消息进行消费。消息被消费后,消息中心不再存储该消息,因此其他消费者无法再消费该消息。也就是说,点对点模式虽然支持多个消费者,但一个消息只能被一个消费者消费,不允许重复消费。
|
||||
|
||||
这种模式就好比,限定了每篇论文只能被一个用户消费,比如现在有一篇分布式相关的论文,这篇论文推送给学生A之后,论文网站就必须将其删除或下架,也就是说其他用户无法再获取或阅读该论文了。(当然实际情况并不是这样的,这里只是为了方便你理解,我做了相应的假设。)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/37/ae8560a6309f69ec709294104e419037.png" alt="">
|
||||
|
||||
接下来,我们看一下**发布订阅模式**。
|
||||
|
||||
生产者可以发送消息到消息中心,而消息中心通常以主题(Topic)进行划分,每条消息都会有相应的主题,消息会被存储到自己所属的主题中,订阅该主题的所有消费者均可获得该消息进行消费。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/b1/73cc7ee9a845ab0f9cd8e1b954515ab1.png" alt="">
|
||||
|
||||
比如图中假设生产者1发布一个Topic相关数据或消息,消费者1~3均订阅了该Topic消息,则该消息会推送消费者1~3,也就是说同一个消息被3个消费者消费了。
|
||||
|
||||
这种模式就好比,不同的方向代表不同的主题,比如分布式领域代表一个主题,当会议方或出版社发布分布式相关的论文时,该论文会被存储到论文网站的分布式主题下,同时学生或老师也会根据自己感兴趣的主题进行订阅。如果学生A订阅了分布式主题,那么当会议方或出版社发布分布式相关的论文后,会议网站会将这些论文推送给学生A。
|
||||
|
||||
**与点对点模式相比,发布订阅模式中一个消息可以被多个消费者进行消费,这也是和点对点模式的本质区别。**
|
||||
|
||||
以上就是发布订阅中的两种典型模式了。
|
||||
|
||||
在分布式系统中,通常会为多用户服务,而多个用户通常会关注相同类型的消息,因此发布订阅模式在分布式系统中非常常见。接下来,我再结合经典的分布式发布订阅消息系统Kafka的发布订阅原理及工作机制,来帮助你巩固对发布订阅的理解。
|
||||
|
||||
### Kafka发布订阅原理及工作机制
|
||||
|
||||
Kafka是一种典型的发布订阅消息系统,其系统架构也是包括生产者、消费者和消息中心三部分。
|
||||
|
||||
- 生产者(Producer)负责发布消息到消息中心,比如电子论文的会议方或出版社;
|
||||
- 消费者(Consumer)向消息中心订阅自己感兴趣的消息,获得数据后进行数据处理,比如订阅电子论文的老师或学生;
|
||||
- 消息中心(Broker)负责存储生产者发布的消息和管理消费者订阅信息,根据消费者订阅信息,将消息推送给消费者,比如论文网站。在Kafka中,消息中心本质上就是一组服务器,也可以说是Kafka集群。
|
||||
|
||||
Kafka的架构图,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/c9/16b4d3889e444dcbff45d6c76429c4c9.png" alt="">
|
||||
|
||||
可以看到,Kafka中除了Producer、Broker、Consumer之外,还有一个ZooKeeper集群。Zookeeper集群用来协调和管理Broker和Consumer,实现了Broker和Consumer的解耦,并为系统提供可靠性保证。
|
||||
|
||||
ZooKeeper集群可以看作是一个提供了分布式服务协同能力的第三方组件,Consumer和Broker启动时均会向ZooKeeper进行注册,由ZooKeeper进行统一管理和协调。
|
||||
|
||||
ZooKeeper中会存储一些元数据信息,比如对于Broker,会存储主题对应哪些分区(Partition),每个分区的存储位置等;对于Consumer,会存储消费组(Consumer Group)中包含哪些Consumer,每个Consumer会负责消费哪些分区等。
|
||||
|
||||
接下来,我们看看**分区和消费组的原理和作用**吧。
|
||||
|
||||
从上面的介绍可以看出,Broker负责存储消息数据,Consumer负责消费数据,Consumer消费数据的能力会影响Broker数据存储是否溢出的问题。若Consumer消费太慢,会导致Broker存储溢出,Broker就会丢弃一部分消息。
|
||||
|
||||
因此,Broker和Consumer是Kafka的核心。接下来,我将带你进一步了解Kafka中Broker和Consumer的关键技术,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/30/7b/300232bcc955ab523e9d25bf2e52ab7b.png" alt="">
|
||||
|
||||
**首先,我们看一下Broker。**
|
||||
|
||||
在Kafka中,为了解决消息存储的负载均衡和系统可靠性问题,所以引入了主题和分区的概念。其中,主题是一个逻辑概念,指的是消息类型或数据类型,就好比电子论文案例所讲的分布式是一个主题。
|
||||
|
||||
而分区是针对主题而言的,指的是一个主题的内容可以被划分成多个集合,分布在不同的Broker上,不同的Broker在不同的节点上。这里的集合就是分区,其中同一个分区只属于一个Broker。
|
||||
|
||||
那么,**分区有什么好处呢?**
|
||||
|
||||
在我看来,分区的好处主要包括如下两点:
|
||||
|
||||
- 实现负载均衡,避免单个Broker上的负载过高。比如,Topic 0被分为Partiton-0、Partiton-1和Partiton-2三个分区,分别分布在Broker 0、Broker 1和Broker 2上。这,就使得Topic 0的消息可以分布在这3个分区中,实现负载均衡。
|
||||
- 实现消息的备份,从而保证系统的高可靠。比如,Topic 1包含两个分区Partiton-0、Partiton-1,每个分区内容一致,分别存储在Broker 0和Broker 1上,借此实现了数据备份。
|
||||
|
||||
**接下来,我们再看看Consumer吧。**
|
||||
|
||||
Kafka中的消费组,指的是多个消费者的一个集合。一个消费组中的消费者共同消费主题消息,并且主题中每个消息只可以由消费组中的某一个消费者进行消费。
|
||||
|
||||
引入消费组的目的是什么呢?我们知道,在消息过多的情况下,单个消费者消费能力有限时,会导致消费效率过低,从而导致Broker存储溢出,丢弃一部分消息。Kafka为了解决这个问题,所以引入了消费组。
|
||||
|
||||
这样一来,我们对发布订阅的基本工作机制就比较清楚了。接下来,我们再结合电商购物平台的例子,来看看发布订阅技术的具体应用吧。
|
||||
|
||||
### 发布订阅实践应用
|
||||
|
||||
假设在电商购物平台(为了方便理解,我对电商购物平台做了一定的简化)中,用户首先在订单系统下单,下单后库存系统会进行出货,通知系统则负责通知用户,整个流程可以用发布订阅的模式进行,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/be/bc/be74e506ddf09736ab3a911f59b5d7bc.png" alt="">
|
||||
|
||||
- 订单系统对应发布订阅模式中的生产者,消息中心有个主题专门存放下单信息,每次用户下单后,订单系统会向该主题写入数据;
|
||||
- 库存系统和通知系统对应发布订阅模式中的消费者,它们会向消息中心订阅下单信息相关的主题;
|
||||
- 订单系统向消息中心发布订单信息后,库存系统和通知系统都会获取到相应的下单信息,然后进行各自后续的操作,即库存系统进行出货,通知系统通过短信或邮件等方式通知用户。
|
||||
|
||||
接下来,我们总结下**发布订阅模式的关键特征**吧。
|
||||
|
||||
- **实现了系统解耦,易于维护**。生产者/发布者只负责消息的发布,不需要知道订阅者/消费者的数量,也不需要知道订阅者/消费者获取消息用来做什么,而订阅者/消费者也不需要知道什么时候生产者/发布者会发布消息。
|
||||
|
||||
所以,生产者/发布者和订阅者/消费者互相独立,进而实现了系统解耦,每个部分可以单独维护,减少了因为生产者和消费者的耦合引入的一些相互影响。比如,如果两者耦合在一起,当生产者逻辑更改需要修改代码时,消费者部分的代码也受影响,因此每个部分单独维护降低了维护的复杂度。
|
||||
|
||||
- **实现了异步执行,避免高负载**。生产者/发布者发布消息到消息中心,当消息超过消息中心可以存储的容量后,消息中心会丢弃掉超出的消息,这样系统就不会因为消息数量多而导致系统故障。
|
||||
|
||||
## 知识扩展:观察者模式和发布订阅模式的区别是什么?
|
||||
|
||||
我们还经常会听到一个概念,叫作观察者模式,也会在分布式系统中都会经常用到。那么,观察者模式和发布订阅模式的区别到底是什么呢?
|
||||
|
||||
**首先,我们看一下观察者模式**。顾名思义,观察者模式下有观察者,那么就有被观察者,两者之间的关系是什么呢?
|
||||
|
||||
观察者负责监控被观察者的状态变更,如果被观察者的状态发生了改变,那么观察者根据状态的变更执行相关操作。举个例子,现在进程A是被观察者,进程B和进程C是观察者,当进程B观察到进程A中变量X由3变为4时,执行X+1的操作;当进程C观察到进程A中变量X由3变为4时,执行X-1的操作。也就是说,观察者模式,定义了被观察者与观察者的直接交互或通信关系。
|
||||
|
||||
**接下来,我们看一下发布订阅模式**。发布订阅模式中存在发布者、订阅者和消息中心,订阅者需要向消息中心指定自己对哪些数据感兴趣,发布者推送的数据放入消息中心后,消息中心根据订阅者订阅信息推送数据。也就是说,发布者和订阅者之间引入了消息中心,实现的是间接通信。
|
||||
|
||||
总结来讲,观察者模式采用了直接通信,观察者和被观察者通信时延会低一些,但它们的依赖关系比较强,不管是被观察者还是观察者逻辑或接口有更改,另外一个均会受影响。而发布者和订阅者模式采用间接通信,引入了消息中心,相对比较厚重,且通信时延相对会高一点,但实现了订阅者与发布者的解耦。
|
||||
|
||||
## 总结
|
||||
|
||||
我首先通过论文订阅的案例,与你介绍了什么是发布订阅以及发布订阅的基本原理,然后介绍了一个经典的分布式发布订阅消息系统Kafka,最后以一个电商购物平台的案例描述了发布订阅模式的应用场景。
|
||||
|
||||
我再和你总结下今天的核心知识点吧。
|
||||
|
||||
- 发布订阅就是生产者产生消息发布到消息中心,消费者订阅自己感兴趣的消息,消息中心根据消费者的订阅情况将相关消息发给对应的消费者。
|
||||
- Kafka是一个经典的发布订阅消息系统,采用多分区实现了消息备份、负载均衡,并引入消费组提高了消费者的消费能力,防止Broker因为存储资源不够丢弃消息的情况,从而提高了Kafka系统的可靠性。
|
||||
- 发布订阅模式可以使系统松耦合易于维护,也可异步执行解决高负载问题,适用于系统解耦、流量削峰等场景。
|
||||
|
||||
最后,我再通过一张思维导图梳理下今天的核心知识点,以帮助你理解与记忆。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/f2/8e24456abc8dc8c3e9abee53b5bedff2.png" alt="">
|
||||
|
||||
发布订阅模式易于理解,与点对点模式很类似。不同的是,点对点模式中一个消息只能由一个消费者消费,而发布者订阅者模式中一个消息可以由多个消费者消费。
|
||||
|
||||
不同的通信模式适用于不同的分布式场景,其中发布订阅模式适合具备多个生产者、多个消费者且异步处理的场景,比如现在的视频App,多个用户都可以通过同一款App看同一部电视剧,当然这个电视剧可以是被不同的生产者发布。点对点模式由于其局限性,一般适用于需要进行点对点通信的场景,比如近场投屏等。
|
||||
|
||||
相信你通过本讲的学习后,可以针对不同的分布式场景选择合适的通信模式,加油!
|
||||
|
||||
## 思考题
|
||||
|
||||
发布订阅模式下,当发布者消息量很大时,单个订阅者的处理能力是有限的,那么能否实现订阅者负载均衡消费呢?又该如何实现呢?
|
||||
|
||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
|
||||
|
||||
|
||||
146
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/21 | 分布式通信之消息队列:货物自取.md
Normal file
146
极客时间专栏/geek/分布式技术原理与算法解析/第四站:分布式通信技术/21 | 分布式通信之消息队列:货物自取.md
Normal file
@@ -0,0 +1,146 @@
|
||||
<audio id="audio" title="21 | 分布式通信之消息队列:货物自取" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/6d/f59c2a0027fb21e0990fe255f745ee6d.mp3"></audio>
|
||||
|
||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
|
||||
|
||||
在上一篇文章,我带你学习了分布式通信技术中的发布订阅。总结来说,发布订阅就是发布者产生数据到消息中心,订阅者订阅自己感兴趣的消息,消息中心根据订阅者的订阅情况,将相关消息或数据发送给对应的订阅者。所以,我将其思想,概括为“送货上门”。
|
||||
|
||||
在实际使用场景中,还有一种常用的通信方式,就是将消息或数据放到一个队列里,谁需要谁就去队列里面取。在分布式领域中,这种模式叫“消息队列”。与发布订阅相比,消息队列技术的核心思想可以概括为“货物自取”。
|
||||
|
||||
接下来,我们就一起打卡分布式通信技术中的消息队列吧。
|
||||
|
||||
## 什么是消息队列?
|
||||
|
||||
回想一下,在上一篇学术电子论文订阅的例子中,出版社或会议方将论文发布到论文网站(或平台)上,然后论文网站再将论文推送给订阅相关论文的老师或学生。这里的论文网站就是消息中心,负责根据订阅信息将论文送货上门,角色非常关键。
|
||||
|
||||
但其实,除了将论文送货上门外,我们还能想到另外一种模式,也就是出版社或会议方将论文发布到论文网站进行存储,老师或学生根据需要到论文网站按需购买文章。
|
||||
|
||||
这种思想,在分布式通信领域中称为消息队列模式,论文网站充当的就是消息队列的角色,也非常关键。接下来,**我再通过一个具体的应用案例来帮助你更加深入地理解什么是消息队列吧**。
|
||||
|
||||
比如,很多系统都提供了用户注册功能,注册完成后发送通知邮件。如下图所示,假设用户通过邮箱进行注册,填写完注册信息并点击提交后,系统的处理过程主要分为两步:
|
||||
|
||||
1. 检查用户注册信息的合法性,如果合法则将注册信息写入数据库中,若不合法,直接返回,流程结束;
|
||||
1. 将用户注册信息写入数据库后,给用户发送通知邮件,以告知用户注册的相关信息,比如注册账号等信息。
|
||||
|
||||
假设,系统将注册信息写入数据库需要花费400ms、给用户发送通知邮件需要花费600ms。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/8c/ecaeeedd9aad12e4fce0080759d3048c.png" alt="">
|
||||
|
||||
这时,注册消息写入数据库和发送通知邮件这两个组件间是直接交互,且是同步通信方式。那么,从用户提交注册到收到响应,需要等系统完成这两个步骤。也就是说,如果不考虑通信延迟的话,注册系统对用户的响应时间是1000ms,即1s。
|
||||
|
||||
如下图所示,如果引入消息队列作为注册消息写入数据库和发送通知邮件这两个组件间的中间通信者,那么这两个组件就可以实现异步通信、异步执行。引入消息队列后,上述步骤可以分为三步:
|
||||
|
||||
1. 检查用户注册信息的合法性,如果合法则将注册信息写入数据库中,若不合法则直接返回,流程结束;
|
||||
1. 注册消息写入消息数据库后,将消息写入消息队列的队尾;
|
||||
1. 发送通知邮件的组件去消息队列取出队首的消息,给用户发送通知邮件,告知用户注册的相关信息。
|
||||
|
||||
也就是说,采用消息队列模式,只需要第2步完成,即可给用户返回响应。第3步发送通知邮件可以在返回响应之后执行。
|
||||
|
||||
用户的注册信息写入数据库之后,通过数据库的可靠性设计来保证用户注册信息不会丢失,也就是说发送通知邮件的组件一定可以获取到用户注册信息,即保证会给注册用户发送通知邮件。也就是说,**消息队列的引入不会影响用户注册网站,但会提升用户响应效率**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/f5/6a993825383977615cac1f746731abf5.png" alt="">
|
||||
|
||||
通常情况下,将消息写入消息队列的速度很快,假设需要100ms。那么,引入消息队列后,发送通知邮件实现了异步读取,系统响应时间缩短为500ms,响应速度提升了一倍,提升了用户体验。
|
||||
|
||||
讲完了用户注册这个例子,我们再来看消息队列的定义就比较容易理解了。
|
||||
|
||||
队列是一种具有先进先出特点的数据结构**,消息队列是基于队列实现的,存储具有特定格式的消息数据**,比如定义一个包含消息类型、标志消息唯一性的ID、消息内容的一个结构体作为消息数据的特定格式。消息以特定格式放入这个队列的尾部后可以直接返回,并不需要系统马上处理,之后会有其他进程从队列头部开始读取消息,按照消息放入的顺序逐一处理。
|
||||
|
||||
从上面的例子中,我们也可以看出引入消息队列的好处是,提高响应速度,以及实现组件间的解耦。
|
||||
|
||||
## 消息队列的原理
|
||||
|
||||
现在,我把消息队列的工作原理从用户注册这个例子中剥离出来,给你一个更加直接的解释吧。
|
||||
|
||||
### 消息队列工作原理
|
||||
|
||||
消息队列的核心结构,如下图所示。与发布订阅模式类似,消息队列模式也是包括3个核心部分:
|
||||
|
||||
- **生产者**。生产者会产生消息或数据,并将消息或数据插入到消息队列中。
|
||||
- **消息队列**。一种具有先进先出特点的数据结构,用于存储消息。
|
||||
- **消费者**。从消息队列中获取消息或数据,进行相关处理。
|
||||
|
||||
具体流程是,生产者将发送的消息插入消息队列,也就是入队,之后会有一个消费者从消息队列中逐次取出消息进行处理,完成出队。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/ed/fcaca073e4bc70e923d2211cd83844ed.png" alt="">
|
||||
|
||||
了解了消息队列的工作原理,接下来我以阿里开源的RocketMQ为例,与你进一步介绍消息队列的原理、工作机制和实践应用。
|
||||
|
||||
### RocketMQ消息队列原理及工作机制
|
||||
|
||||
首先,我们看一下RocketMQ的架构图,形成一个整体认知。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/81/3a751ee27c5d9b4541f82662cecdb381.png" alt="">
|
||||
|
||||
RokcetMQ共包括NameServer Cluster、Producer Cluster、Broker Cluster和Consumer Cluster共4部分。接下来,我们一起看看每部分的具体功能吧。
|
||||
|
||||
**NameServer Cluster**,指的是名字服务器集群。这个集群的功能与Kafka中引入的ZooKeeper类似,提供分布式服务的协同和管理功能,在RocketMQ中主要是管理Broker的信息,包括有哪些Broker、Broker的地址和状态等,以方便生产者获取Broker信息发布消息,以及订阅者根据Broker信息获取消息。
|
||||
|
||||
**Producer Cluster**,指的是生产者集群,负责接收用户数据,然后将数据发布到消息队列中心Broker Cluster。那么,生产者按照集群的方式进行部署,好处是什么呢?在我看来,好处可以概括为以下两点:
|
||||
|
||||
- 一是,多个Producer可以并发接收用户的输入数据,提升业务处理效率;
|
||||
- 二是,考虑到可靠性问题,如果只有一个Producer接收用户输入数据,当这个Producer故障后,整个业务就无法运行了。
|
||||
|
||||
**Consumer Cluster**,指的是消费者集群,负责从Broker中获取消息进行消费。Consumer以集群方式进行部署的好处是,提升消费者的消费能力,以避免消息队列中心存储溢出,消息被丢弃。
|
||||
|
||||
**Broker Cluster**,指的是Broker集群,负责存储Producer Cluster发布的数据,以方便消费者进行消费。
|
||||
|
||||
Broker Cluster中的每个Broker都进行了主从设计,即每个Broker分为Broker Master 和 Broker Slave,Master 既可以写又可以读,Slave 不可以写只可以读。每次Broker Master会把接收到的消息同步给Broker Slave,以实现数据备份。一旦Broker Master崩溃了,就可以切换到Broker Slave继续提供服务。这种设计的好处是,提高了系统的可靠性。
|
||||
|
||||
可以看出,Broker Cluster就是我们今天要讲的核心“消息队列中心”,那么它到底是如何采用队列实现的呢?接下来,我们就一起看看**Broker Cluster的实现方式**吧。
|
||||
|
||||
如下图所示,在Broker Cluster中,消息的存储采用主题(Topic)+消息队列(Queue)的方式实现:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ad/e4/ad93fad50b4eac4b44b41b299095d1e4.png" alt="">
|
||||
|
||||
与Kafka一样,RocketMQ中的主题也是一个逻辑概念。一个主题可以分区,分布在各个不同的Broker中,每个Broker上只有该主题的部分数据。每个主题分区中,队列的数量可以不同,由用户在创建主题时指定。队列是资源分配的基本单元,消息进行存储时会存放到相应主题的分区中。
|
||||
|
||||
上面我为你介绍了RocketMQ的关键组件。接下来,我们再看看**RocketMQ的工作流程**,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/7a/a6770507a60f1fab71e4523779fa097a.png" alt="">
|
||||
|
||||
1. 首先启动NameServer,然后启动Broker。Broker启动后,会主动找NameServer建立连接,并将自己的信息注册到NameServer上。注册完毕后,Broker会周期性地给NameServer发送心跳包,比如每隔1s发送一次,以告知NameServer自己还活着;心跳包里还可以包括Broker当前存储的数据信息,也就是说Broker可以周期性地向NameServer更新自己的数据信息,以保证NameServer上存储的数据是最新的。
|
||||
1. 创建主题,并确定这个主题的数据放入哪些Broker中。
|
||||
1. 当Producer生产消息发送到主题时,需要先到NameServer查询该主题存放在哪些Broker中,获取到相关Broker信息后,将消息发送给这些Broker进行存储。
|
||||
1. Consumer要从主题消费消息,也需要首先到NameServer查询一下该主题的消息存储在哪些Broker上,然后去相应的Broker获取消息进行消费。
|
||||
|
||||
通过对RocketMQ的介绍,相信你已经对消息队列有比较深刻的认识了。接下来,我们再看看**消息队列模式适用于什么场景**吧。
|
||||
|
||||
消息队列模式,是根据消费者需求到消息队列获取数据消费的,消费者只需要知道消息队列地址即可,消息队列中心也无需提前知道消费者信息。也就是说,这种模式对消费者没有特别需求,因此比较适合消费者为临时用户的场景。
|
||||
|
||||
比如目前,阿里内部将RocketMQ应用于购物交易、充值、消息推送等多个场景,因为在这些场景下,每个消费者不是常驻进程或服务,几乎都是临时存在。此外,滴滴、联想等公司也都有采用RocketMQ。
|
||||
|
||||
## 知识扩展:发布订阅和消息队列模式都支持系统解耦,两者是否一致呢?
|
||||
|
||||
概括地说,发布订阅和消息队列模式虽然都支持系统解耦,但它们在实现时采用的数据结构和方式并不相同。
|
||||
|
||||
首先,我们看一下它们实现解耦的数据结构。
|
||||
|
||||
- 发布订阅模式采用了消息中心,消息队列模式采用了消息队列中心,它们均用来存储生产者发布的数据,并均有主题、Broker等概念;
|
||||
- 唯一不同之处,是消息队列模式中采用了具有先进先出特征的队列结构进行存储,而订阅发布采用了map或数组等方式存储。
|
||||
|
||||
然后,我们再看看它们实现解耦的方式。
|
||||
|
||||
- 消息队列模式中,生产者发布数据到消息队列中心,消息队列中心会存储数据,等待消费者按需获取数据。这样生产者就不需要和消费者进行直接通信了,实现了生产者和消费者的解耦。
|
||||
- 而在发布订阅模式中,消费者需要提前向消息中心订阅自己感兴趣的数据,待生产者发布数据到消息中心后,消息中心根据订阅者订阅信息将数据主动推送给消费者,也实现了消费者和生产者的解耦。
|
||||
|
||||
对于消息队列模式,消息队列中心无需提前获取消费者信息,因此对消费者比较灵活,适合消费者为临时用户的场景;而发布订阅模式,需要消费者提前向消息中心订阅消息,也就是说消息中心需要提前获取消费者信息,比较适合消费者为长驻进程或服务的场景。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我主要与你分享的是分布式通信技术中的消息队列模式。
|
||||
|
||||
首先,我通过用户注册的案例,与你介绍了什么是消息队列模式,以及它的好处。其中,消息队列模式中的核心是以一种具有先进先出特点的队列结构来存储数据,实现组件间的解耦和异步执行。
|
||||
|
||||
然后,我与你介绍了消息队列的基本原理,并以RocketMQ为例对其架构、核心组件和工作原理做了更深入的讲解,以帮助你进一步了解消息队列模型。
|
||||
|
||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/ef/258dbc95914e8971b584400c528a00ef.png" alt="">
|
||||
|
||||
加油,行动起来,试着将发布订阅和消息队列这两种通信模式用到你的业务场景中吧,相信你可以的。如果你需要进一步了解这两种通信模式对应的产品源码的话,相信你结合这两篇文章中讲述的原理,可以比较容易地开启你的源码之旅了。
|
||||
|
||||
## 思考题
|
||||
|
||||
消息队列模型中,消费者是主动去消息队列获取消息的,而消息队列需要保证多个消费者可以获取到消息,也就是说一个消费者获取消息后并不会删除该消息,那么如何保证同一个消息不被同一个消费者重复消费呢?
|
||||
|
||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
|
||||
@@ -0,0 +1,113 @@
|
||||
<audio id="audio" title="22 | 答疑篇:分布式体系架构与分布式计算相关问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/dc/39035deb766ba09f009ad8bfb7a084dc.mp3"></audio>
|
||||
|
||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
|
||||
|
||||
到目前为止,“分布式技术原理与算法解析”专栏已经更新21篇文章了,我已经为你介绍了分布式技术四纵四横知识体系中的三横,即“分布式资源管理”“分布式计算技术”和“分布式通信”,以及四纵中的“分布分布式式协同”和“分布式调度”。
|
||||
|
||||
在这里,我首先要感谢你们坚持学习每一篇文章,以及对每一道思考题的积极思考与讨论,并且还在此基础上对类似问题进行了扩展。
|
||||
|
||||
比如,@1024、@每天晒白牙、@游弋云端、@Jackey和@Dale等同学,对双主问题展开了激烈的讨论;再比如,@xj_zh、@mt11912、@小白啊、@随心而至等同学,对Master如何判断Slave是否存活的问题进行了讨论,特别是@小白啊还专门查询了Kubernetes的方法,在留言区进行了回复。
|
||||
|
||||
这样的同学还有很多,我就不再一一点名了。今天,我就针对前面文章涉及的与思考题有关的留言,做一次进一步的梳理与分析,以帮助你夯实前面所学的知识点。
|
||||
|
||||
留言涉及的问题有很多,但我经过进一步地分析和总结后,发现大家特别感兴趣和有疑惑的思考题主要分为两类:
|
||||
|
||||
- 分布式体系架构中,如何判断节点存活的问题;
|
||||
- 分布式计算技术中,离线计算、批量计算、实时计算和流式计算的区别。
|
||||
|
||||
今天,我主要就对这两类思考题进行一下分析和讲解。
|
||||
|
||||
## 分布式体系架构相关问题
|
||||
|
||||
在第9篇文章“[分布式体系结构之集中式结构:一人在上,万人在下](https://time.geekbang.org/column/article/148187)”中,我给你留了一个思考题:**在集中式架构中,Master 如何判断 Slave 是否存活呢**?
|
||||
|
||||
首先,我先和你说说Slave故障的两类情况:一种是Slave进程退出,另一种是Slave所在服务器宕机或重启了。你可能会说,这两种情况的判断方法,难道还不一致吗?别着急,且听我慢慢道来。
|
||||
|
||||
如下图所示,假设Master节点与3个Slave节点相连。请注意,我在图中,Master与Slave之间画了两条线,实线旁写的是TCP长连接,虚线旁写的是心跳。因为Master与Slave之间的监控关系是固定的,因此我用了两种机制协同来判断Slave是否存活。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/a1/a04e5d8e89239be68a1ed239b66d6ea1.png" alt="">
|
||||
|
||||
其中,**TCP长连接就是针对Slave进程退出,但是Slave所在服务器未故障的情况。**这种方式是借助TCP长连接的工作原理进行判断的。TCP长连接中,TCP会对对端的Socket进行检测,当发现对端Socket不可用时,比如不能发出探测包或探测包未收到响应,会返回-1的状态,表示连接断开。所以,这种方式可以快速检测到Slave进程的退出。
|
||||
|
||||
对于Slave所在服务器故障的情况,由于服务器宕机或重启,那么系统环境等均不工作了,这种情况TCP长连接也无法进行探测了,也就是说TCP长连接方法在这种场景下无法判断节点是否故障。
|
||||
|
||||
对于这种场景,现有的软件架构中,基本都采用了**心跳方式**。其核心策略是,Master按照周期性(比如每隔1s)的方式给Slave发送心跳包,正常情况下Slave收到Master发送的心跳包后,会立即回复一个心跳包,告知Master自己还活着。当某个Slave(比如Slave1)所在服务器故障后,由于Slave无法接收到Master的心跳包,也就无法回复了。
|
||||
|
||||
因此,Master也无法接收到这个Slave(比如Slave1)的回复信息。通常情况下,**系统会设置一个阈值(一般设置为与心跳周期一致),若超过这个阈值还未收到Slave节点的回复,Master就会标记自己与该Slave心跳超时。**
|
||||
|
||||
其中,设置阈值的目的是,解决Slave故障情况下,Master一直收不到心跳信息而阻塞在那里等待心跳回复的问题。一般连续k次Master与Slave的心跳超时,Master就会判断该Slave故障了。其中,设置连续k次的目的是,降低因为系统做垃圾回收或网络延迟导致误判的概率。
|
||||
|
||||
这里的k,主要是根据业务场景进行设置的。如果k设置得太小,容易导致故障误判率过高,因为系统在做垃圾回收或系统进程正在占用资源时,会阻塞心跳,导致心跳包无法及时回复而超时,从而被误判。如果k设置得太大,会导致故障发现的时间过长,因为故障发现时间=k*心跳发送周期。
|
||||
|
||||
接下来,我们继续延展下这个问题吧。
|
||||
|
||||
### 追问1:非集中式架构中,如何判断节点是否存活?
|
||||
|
||||
集中式架构中,采用了TCP连接和心跳协同判断节点是否存活,那么非集中式架构中是否也是这样判断的呢?
|
||||
|
||||
其实,**在非集中式架构与集中式架构中,判断节点是否存活的原理有所不同。**因为,非集中式架构中节点之间是对等的,没有Master与Slave之分。如果每个节点间都建立TCP长连接,假设集群中有n个节点,那么每个节点均需要与其他n-1个节点建长连接,这将导致每个节点的资源占用都会非常多。因此,非集中式架构是采用心跳的方式进行判断的。
|
||||
|
||||
这里你可能会问,如果像集中式架构那样,每个节点与其他n-1个节点都发送心跳的话,整个集群中同一时间心跳消息为n*(n-1),消息量也特别大,甚至会导致网络风暴,应该怎么办。
|
||||
|
||||
其实,与集中式架构中的心跳包不同,非集中式架构中采用的心跳方式的核心思想是,**每个节点被b(1≤b<n)个节点监控,以减少心跳信息量。**
|
||||
|
||||
接下来,**我们以Akka的原理为例,先来看看b的取值原则吧**。
|
||||
|
||||
- 如果用户不设置b的值,那么b默认取值的原则是:若集群中节点总数n小于6,b=n-1;若n大于等于6,b=5。
|
||||
- 若用户设置b的值,则b以用户设置的值为准。
|
||||
|
||||
接下来,我们再看看**Akka集群中具体是如何通过心跳方式判断节点是否存活的**。
|
||||
|
||||
1. Akka中集群组建完成后,每个节点拥有整个集群中的节点列表。
|
||||
1. 每个节点根据集群节点列表,计算哈希值(比如根据节点ID计算一个哈希值),然后基于哈希值,将所有节点组成一个哈希环(比如,从小到大的顺序),如下图所示。由于每个节点上的计算方法一致,因此虽然每个节点独立计算,但每个节点上维护的哈希环是一致的。
|
||||
1. 根据哈希环,针对每个节点逆时针或顺时针方向选择b(图中设置b=2)个临近节点作为监控节点,比如图中Node 2和 Node3监控Node1,Node 3和 Node4监控Node2,以此类推。由于每个节点被b个节点监控,反过来也可以说,在这个环上每个节点监控b个节点,因此具体的实现方式是每个节点按照逆时针或顺时针方向选择b个节点进行监控。
|
||||
1. 当某个节点发现自己监控的节点心跳超时时(比如Node 2发现Node1心跳超时),则标记该节点不可达(Node2标记Node1不可达),并将该信息通过Gossip协议传播给集群中的其他节点。
|
||||
1. 如果某个节点被标记为不可达之后(比如Node1不可达),若不将该节点踢出集群,那么Node2和Node3仍然会给Node1发送心跳,若后面Node2又发现Node1心跳可达时,则重新将Node1更新为可达状态,然后同步给集群中其他节点。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/51/031e8d6c6efceff4d9c299d583e6a151.png" alt="">
|
||||
|
||||
这里的**判断心跳超时机制,可采用集中式方法中的连续k次心跳超时的方法进行判断,也可以通过历史心跳信息进行预测**。具体的预测方法,我将在第31篇文章“分布式高可用之故障恢复:知错能改,善莫大焉”中做进一步讲解。
|
||||
|
||||
### 追加2: 一个集群为什么会存在双主的场景呢?
|
||||
|
||||
上面,我提到判断节点存活的方法主要是通过心跳的方式。如果是因为网络连接断开,那么节点之间就会被误判为对方故障了。在主备场景下,通常会出现双主的情况。这也就是第4篇文章“[分布式选举:国不可一日无君](https://time.geekbang.org/column/article/143329)”的课后思考题答案了。
|
||||
|
||||
在主备场景下,正常情况下,主节点提供服务,备节点是对主节点的数据、状态进行备份,以确保主故障后备升主后业务可以正常运行。主备节点之间通常会通过心跳的方式进行检测,目的是监控主节点是否故障,若故障则备升主,保证业务运行。
|
||||
|
||||
想象一下,如果主备节点之间的网络连接断开了,那么主节点与备节点之间心跳均不可达,因此主节点会认为备节点故障,此时主节点会继续提供服务,而备节点会认为主节点故障,备升主。所以,集群中就出现了双主的场景。
|
||||
|
||||
好了,以上就是关于分布式体系结构中如何判断节点是否存活的相关问题了,相信你对这几个问题有了比较深刻的理解。接下来,我们再看看分布计算技术的相关问题吧。
|
||||
|
||||
## 分布计算技术相关问题
|
||||
|
||||
在分布式计算技术中,我们经常会听到离线计算、批量计算、实时计算和流式计算这四个概念,也常常会弄混。那么,**离线计算和批量计算,实时计算和流式计算到底是什么呢?离线计算和批量计算、实时计算和流式计算分别是等价的吗?**
|
||||
|
||||
接下来,就请你带着问题,随我一起进入下文。
|
||||
|
||||
首先,我们来看一下**离线计算。**通常我们提到的离线计算,主要的应用场景是对时延要求不敏感、计算量大、需要计算很长时间(比如需要数天、数周甚至数月)的场景,比如大数据分析、复杂的AI模型训练(比如神经网络)等。
|
||||
|
||||
这种场景如果采用在线计算或实时计算的话,通常会存在数据量不够或大量计算影响正在运行的业务等问题,因此往往会采用离线计算的方式。
|
||||
|
||||
离线计算方式的核心思想是,先采集数据,并将这些数据存储起来,待数据达到一定量或规模时再进行计算,然后将计算结果(比如离线训练的模型)应用到实际业务场景中。
|
||||
|
||||
其次,我们看一下**批量计算。**批量计算通常是指,将原始数据集划分为多个数据子集,然后每个任务负责处理一个数据子集,多个任务并发执行,以加快整个数据的处理。比如,我在第15篇文章“[分布式计算模式之MR: 一门同流合污的艺术](https://time.geekbang.org/column/article/155575)”中,讲MR计算模式时提到,MapReduce中的Map其实就属于批量计算,Map计算的结果会通过Reduce进行汇总。
|
||||
|
||||
接下来,我们再看一下**实时计算。**实时计算其实是和离线计算相对应的,离线计算对时延要求不敏感,相反,实时计算对时延的要求比较敏感。这种模式需要短时间执行完成并输出结果,比如秒级、分钟级,也就是说强调时效,通常用于秒杀、抢购等场景。实时计算由于时延要求低,因此计算量通常不大、数据量也不会太多,所计算的数据往往是K、M级别的。
|
||||
|
||||
最后,我们在看看**流式计算。**我在第16篇文章“[分布式计算模式之Stream: 一门背锅的艺术](https://time.geekbang.org/column/article/156937)”中,与你讲述了流式计算。流计算强调的是实时性,数据一旦产生就会被立即处理,当一条数据被处理完成后,会立刻通过网络传输到下一个节点,由下一个节点继续处理。这种模式通常用于商业场景中每天的报表统计、持续多天的促销活动效果分析等。
|
||||
|
||||
为了便于你理解与记忆,我将这四种计算模式的特点,汇总为了一张表格,如下所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/6d/ea93ecf04130f6546924554f1664696d.jpg" alt="">
|
||||
|
||||
通过对这四种计算模式的讲解,相信你已经发现了,离线计算和批量计算对任务执行的时延不是特别敏感,而实时计算和流式计算对任务执行的时延敏感。但,离线计算和实时计算是从计算时延的维度进行分类的,而批量计算和流式计算是从计算方式的维度进行分类的,因此我们**不能将离线计算和批量计算直接等同,也不能将实时计算和流式计算直接等同**。
|
||||
|
||||
## 总结
|
||||
|
||||
我把前面21篇文章中,大家针对思考题的讨论和困惑,筛选出了分布式系统架构中如何判断节点是否存活,以及四种分布式计算模式的异同,做了进一步展开,梳理成了今天的这篇答疑文章。
|
||||
|
||||
如果还有哪些思考题或者留言问题,还没来得及扩展的话,你可以留言给我,后续我会再找机会进行解答。最后,我要和你说的是,和我一起打卡分布式核心技术,一起遇见更优秀的自己吧。
|
||||
|
||||
篇幅所限,留言区见。
|
||||
|
||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
|
||||
Reference in New Issue
Block a user