ZMQ: 基本原理

开源中国

介绍

与其他的基于常规理论基础的(集中)通信系统不同,几乎没有分布式通信系统的什么资料,ØMQ(ZeroMQ)是感兴趣的读者少数能请举出的一个。

本文的目的是解释ØMQ架构的基本概念,它们是如何组合起来的,以及它们被如此设计的原因。

 

拓扑

拓扑是ØMQ最主要的概念,除非你知道“拓扑”代表什么,否则将与其他概念混淆更难理解,甚至理解不当。

按照通俗定义,我们可以说“拓扑”是一组参与同一业务逻辑的应用程序。

比如:假设有一个图像转换服务,调整图像到所需的尺寸和分辨率。所有提供转换的服务、所有使用该服务的应用程序以及所有的中间节点,比如负载均衡等,共同组成了拓扑。

 

通常,拓扑具有以下属性:

  1. 拓扑是张图表,这张图中节点是应用程序、连线时应用程序间的数据通道。
  2. 所有应用程序就为实现它们的业务逻辑遵从相同的线路协议。
  3. 图表力争紧凑,比如任何两个节点或直接或通过一个或多个中介机构连接。

第1点很明显,故意使用单词“通道”而不是使用“连接”这一点是为了描述模型工作的实际情况,即使IP多播或UDP等无连接的底层传输亦是如此。

第2点说明所有拓扑里的应用程序应该就什么达成一致:正被传送的消息(比如“这是一张需调整尺寸的图片”或“这是调整后的图片”)、消息序列(如应用程序中状态机的实现)、实际数据的编码(图像如何被序列化,RGB?CMYK?)等等。

 

第3点表达了这样一个事实:即使存在两个非常相同的(例如两家公司里的)商务逻辑部署,它们仍然形成了两个拓扑,除非它们相互通过数据通道连接。

为了直观地掌握拓扑的概念,明白这个概念是含混不清的非常重要的。

它的含混不清与面向对象编程里的类含混不清相同。正式的定义解释了类是数据成员和方法的集合,然而,没有定义解释商务逻辑的哪一部分应当形成类,哪一部分不能形成类。这完全由编程者决定可封装哪个商务概念为类,哪个不能封装为类。编程人员把所有的商务逻辑放在一个单独的类里也许是错误的-因此这实际上在回避面向对象的设计-另外划分这个逻辑为无限多个小类也是错误的-它把这个程序转变为一大堆难以理解的相互依赖

 

同样含混不清的是没有一个单独的正确方法去分割商务逻辑为多个拓扑。唯一的经验规则是拓扑是扩展的原子单元。你可以把拓扑作为整体进行扩展,然而你不能只扩展拓扑的某一方面。因此,如果你期望将来有扩展独立于功能B的功能B的需求,那么你应当为A创建一个独立的拓扑,为B创建一个独立的拓扑。

让我们举个具体的例子来说明上面所述:

在我们的图像转换应用里,有两个基本功能:调整图像大小和调整图像亮度。我们可以选择要么为这两个功能创建一个单独的拓扑,要么分割为“调整大小”的拓扑和“调整亮度”的拓扑。
 

在前一种情形下,我们将定义线路传输协议,这样就可以传输我们感兴趣的功能。假如消息的第一个字节是“1”表示调整大小,或者为“2”表示调整亮度。我们还应该意识到这种设计紧紧地耦合了两个功能。如果我们将来要对这个拓扑增加更多的处理节点,那么它们中的每个节点都应当既能调整大小也能调整亮度。

在后一种情形下,两个功能没有关联。线路传输格式里不需要特定的“类型”域,因为所有“调整大小”拓扑的请求都是要求调整图像大小的,而所有传递给“调整亮度”拓扑的请求都是要求调整图像亮度的。在这样的设计里我们可以独立于另一个拓扑而扩展一个拓扑。也就是说,如果我们制造出特定的单目标的FPGA来实现调整图像大小,那么我们可以简单地把它们连接到“调整大小”的拓扑,而不会影响“调整亮度”的拓扑。这样的布署如下图所示:

 

注意:客户端应用既可以(通过拓扑A)请求调整图像 ,又可以(通过拓扑B)调整亮度。工作者1仅仅做图像大小调整,工作者3仅仅做亮度调整。而工作者2能够提供两种服务 。

最后应该注意的是,应当说幸亏一个拓扑清晰地独立于其他任何拓扑,才使得拓扑可映射到底层传输的一个属性,比如TCP端口。这就允许底层的网络按照商务标准规范其行为。例如,它可以测量由特定拓扑消耗的网络带宽(因此,特定的商务逻辑,比如调整大小的服务消耗的带宽与调整亮度服务消耗的带宽截然相反),这样可以根据拓扑对流量进行调整,比如通过增加调整亮度等来抑制调整图像大小。

 

传输

通常,除TCP之外的其他传输机制之上要求运行消息层,不管是无限宽带(性能原因),IP组播(最小化带宽使用)或者SCTP(多宿主,心跳等)。 
最初的方法以TCP传输开始的,也许增加了TCP缺乏的比如心跳等特性,后来试图在其他底层传输之上提供完全相同的行为。 
这种方法存在两个问题: 
  • 一,在特定的传输之上建立类TCP的封装实际上使得传输成为多余的了。如果这种方法的行为类似于TCP,那么为什么一开始就不用TCP呢?(性能特性排除在这个规则之外)
  • 二,许多传输实际上不可能硬塞到TCP模型里。比如,定义的IP广播发送数据给网络上所有的机器,而不是像TCP那样发送给特定的目标。
 

基于以上问题,ØMQ采用了不同的方法。底层的传输仍然保留它们本身的特性,向上不提供通用、所有都包含的接口。然而,提供最小化接口(尤其是:消息分割,消息分段和消息原子化),并且要求更高层能够统一地处理不同的底层传输的特性。

具体的来说,这意味着传输层上方的封包是相当简单的封包,比如消息分割协议(当封装TCP时),消息分段协议(分割长的消息为适宜于基于包传输的几个包)或者后续连接协议(当连接PGM(实际通用组播协议的)组播流时,丢弃你获得的消息的结尾部分):


 

建立拓扑和路由消息

网络栈里的每一层都是抽象了互联网络复杂性的某一部分。IP层抽象了查找目的主机路由这个需求。TCP层抽象了网络固有的可丢失这个事实而提供了可靠性保证。

ØMQ抽象了指定发送数据到特定网络位置这样需求。消息是被发送到拓扑的,而不是发送给特定的终端节点。重新调用哪个与特定商务逻辑紧密相连的拓扑意味着当你发送消息给拓扑的时候,你基本上已经请求所提供的特定的服务,比如调整图像亮度大小。实际接收消息的终端节点是在ØMQ的透明传输方式里选择的。

 

为了强化这个原则,ØMQ严格地分离拓扑的建立(zmq_bind,zmq_connect)和真实消息的传递(zmq_send,zmq_rev)。

前者同底层的传输地址协同工作,比如IP地址,而后者仅仅使用处理器(文件描述符)去定位具体的拓扑:

/* Topology establishment */
int s = zmq_socket (...);
zmq_connect (s, "tcp://192.168.0.111:5555");

/* Message routing */
const char data [] = "ABC";
zmq_send (s, data, sizeof (data), 0);

区分拓扑建立和消息路由严格地说不是不可缺少的。毕竟,混合这两个为一个单独的函数是很容易的:

zmq_send (s, "tcp://192.168.0.111:5555", data, sizeof (data), 0);
 
分离的理论基础即是技术的又是学术的。技术方面的争论包括: 
  • 当我们打算以异步的方式接收来自拓扑的消息的时候,我们无论如何都要连接到这个拓扑。同时没有理由不重复使用这个通道传送消息。
  • 拓扑建立和消息路由的分离很好地映射到BSD套接字API上(bind/connect和send/recv)。

现在,学术方面的争论甚至更加重要。使用ØMQ该做什么,不该做什么。

底层的协议,比如TCP,允许你发送数据给特定的终端节点。ØMQ构建在底层协议之上,它允许你发送数据给拓扑而不是给特定的终端节点。因此,如果你打算发送数据给特定的终端节点,那么你应当使用TCP或者类似的协议。如果你打算发送数据给拓扑,并且让拓扑决定目标,那么你应当使用ØMQ。

 

不幸的是,这个概念似乎相当难以掌握。结果是:要使人们相信ØMQ不能用来定位具体的终端节点以及这一点不是漏洞而是特性几乎是不可能的。

把拓扑建立和消息路由分离没有解决问题,不过使得真正的功能更加显明。未来给这个组合添加名字解析(参阅下面同名的章节)希望使得这个事实完全显明:

zmq_connect (s, "Brightness-Adjustment-Service");
zmq_send (s, data, sizeof (data), 0);
 

消息模式

当把拓扑当作路由消息的方式考虑的时候,对不同的拓扑使用不同的路由算法将变得清晰起来。当"纳斯达克股票报价“拓扑发布报价给这个拓扑里的所有客户时,"亮度调整“拓扑传输一个客户的图像到工作者之一,然后回传调整好的图像给起始客户。

ØMQ通过定义几个所谓的“消息模式”来展示这样事实。前者即股票报价拓扑食一个发布/订阅模式的例子,而后者,亮度调整拓扑是请求/应答模式的例子。
 

消息模式既定义了节点间通信使用的协议,还定义了单个节点的功能,比如它用来路由消息的的算法。因此不同模式的行为类似于不同的协议。你不能连接发布/订阅节点到请求/应答节点,就像你不能连接TCP终端节点到SCTP终端节点一样。因此每个拓扑仅仅实现了一个单独的消息模式-没有方法把两个不同的消息模式连接为一个单独的拓扑。

这么严格的分离要求拓扑作为一个整体的行为提供保证。只要你知道拓扑里的每个节点都遵循发布/订阅语义,那么你就能够提供诸如“消息将分发到这个拓扑里的所有节点”这样的保证。如果这个拓扑的一部分允许以负载均衡方式而不是广播方式发布,那么你将不能够做这样的保证。还有更糟糕的,由于消息模式是开放的,你将不得不希望节点以完全任意的方式行动,因此你根本不能提供任何保证。
 

下面是网络栈图。注意各个消息模式位于栈的同一层而且相互之间没有关联:

考虑到一些传统的消息系统选择提供通用的路由基础框架,这个框架实际上允许用户在其上构建任何路由算法(例如AMQP模型的交换中心,路由绑定和用户)而不是分发预打包的消息模式(例如 JMS的主题和队列),因此说明ØMQ选择后一选项的基本原理是很重要的。

 

首先,设计功能完整而且无漏洞的消息模式是一项艰巨的任务。即便把创建模式的责任推卸给用户,我们仍可以确定构建在消息系统之上的大多数应用在某种程度上说是错误的。即使这儿模式得到正确地实现,学习和开发的费用还是超过了多次使用预打包模式所花的费用。终究正如DNS设计一篇早期的文章所说:“[用户]打算使用而不是理解提供给他们的系统。”

其次,正式定义的模式允许增强两种模式不能共存在同一个拓扑里的要求。消息系统可核查双方是否实现了同样的消息模式,如果没有实现就拒绝连接。如果所实现的模式是用户任务,那么这样的检查就不做了。

 

第三、常用的路由框架不能自动发布(也称为自动联合)。这意味着只能采用简单的星形结构运行了,一旦你打算跨越这个模型的话,你不得不提供其他信息,也即是回答“什么是消息模式?”这个问题。看看由AMQP上的各种产品构建的联合机制。这儿“模式”位总是存在,不管是仅仅显式地或者是隐含地(支持一种模式)。

最后,我们对AMQP的经验是:即便它提供了丰富的各种可能的消息模式,人们仍然一而再再而三地在其上构建同样的一对模式,而且完全忽略了其中的其他模式。


 

互联网栈最具有独创性的特征之一就是对逐跳功能(IP)和端对端功能(TCP,UDP,SCP等等)的清晰划分。正是这种划分允许互联网生态系统得以发展。如果没有这样的划分,每个队端对端协议的微小更改都将如IPv4到IPv6转换一般痛苦。

理念是网络上的每个节点都实现了IP,然而,只有使用特定端对端协议比如TCP的终端节点可以意识到它。换句话说,中间节点比如路由器不需要理解位于IP上方的端对端协议层就可以以所希望的方式工作:

 

划分IP和TCP层的经验后来一“端对端论点”的形式得到了普及。端对端论点是这样描述的:如果功能不能由较低层正确地提供(我们例子中的逐跳层),也就是说这个时候它需要更高层的帮助才能按照所期望的运行,所以首先在较低层实现这个功能没有什么价值。

ØMQ遵循端对端原理,而且划分自身栈为逐跳层(以"X"开头的套接字类型笨拙地表示)和端对端层(不以“X”开头的套接字类型)。注意这与上面的TCP/IP图类似:

concepts9.png

类似于TCP/IP,逐跳层负责路由,而端对端层可以提供其他服务,比如可靠性,加密等。

 

不过,我们不应当继续太深入的比如为TCP/IP。不像互联网栈那样具有一个单独的逐跳协议(IP)和多个端对端协议(TCP,UDP,SCTP等),在ØMQ里每个端对端协议都拥有自己的逐跳协议。因此这个栈看起来如下:

具有这样结构的原因是(由逐跳层提供的)路由功能针对特定的消息模式,因此不能由多个模式共享。如果魔门仍然遇到了共享路由同一路由算法的两个消息模式,而且将来只有通过端对端功能才能区分,那么我们将能够模仿出一个单独的逐跳协议上几个端对端协义层的互联网栈模型。

最后,让我们看一看逐跳与端对端划分的一个具体的例子。

 

请求应答模式意味着客户端应用传递一割请求给一个工作者应用(在这条道路上运行负载均衡的),工作者应用然后处理这个请求,接着生成应答。然后回传应答给源客户端:

逐跳层不得不做的事情是发送每个请求给一个上游节点(执行负载均衡),然后发送应答给接收到的与其相关的请求发送的下游节点。

一切运行良好,直到处理请求的工作者失败或者可能整体拓扑由于网络失效而离线 。这种情况下,客户端将永远卡在这儿,来等待从来不会出现的应答。


 

为了解决这个问题,客户端可以等待特定数量的时间,如果到那时应答还没有到达,那么重新发送这个请求。这还不得不过滤掉延迟的重复的应答。

现在重新回想一下端对端的论点。如果没有终端节点的帮助,重发功能不可能实现,因此在逐跳层实现重发功能(保存各种可能的优化,比如在磁盘上保存这个请求,然后网失效的节点重新启动的时候重新发送这个请求)没有什么意义。

我们得到的最终结果是路由在逐跳层实现,可靠性在逐跳层上的端对端层里实现。

 

名字解析

 

现在,ØMQ不能为自己提供名字。为了加入到拓扑里,你不得不使用底层传输定义的地址,比如IP地址和TCP端口。

虽然现在是这样,但将来值得提供把拓扑名字(”ØMQ名字“)转换为底层传输地址的名字解析服务 。例如,字符串"Brightness-Adjustment-Service"可解析为"tcp://192.168.1.111:5555"。

关于这个问题还有许多要思考,不过主要问题似乎是拓扑是由多个节点组成的,而且名字解析服务选择这些节点之一。决定应当可能是基于管理标准的。例如,当连接到“纳斯达克股票报价"拓扑的时候,你需要名字服务连接你到本地的股票报价中心而不是连接到纳斯达克自身的报价中心,或者甚至最糟糕的情形,连接你到竞争者的股票报价中心。

 

技术上来说,名字解析服务应当使用DNS实现,因为DNS是唯一一个广泛可用的分布式数据库。而且这儿的名字解析服务的需求似乎完美的匹配DNS所提供的特性。

像DNS这样以松散地一致性分布式数据库方式存储名字的设计结果应当引起注意,比如拓扑应该是长期存活并且很少更改条目,以确保DNS缓冲机制不能损坏名字的解析等等。

 

附录:设计原则

 

附录总结了用来评估某个特定的消息模式是否是良好设计的原则。

统一性原则

 

统一性原则描述的是你连接应用到拓扑的哪一个节点应该是无关紧要的。提供的服务应当是相同的。

统一性原则听起来很明显,不过破坏它却非常容易。考虑一下ØMQ里目前已经实现的发布/订阅模式。 它允许一个拓扑里有多个发布者,这引入了非统一性:

在上图中,客户端C将看到不同的数据反馈,这取决于它连接到中间者1还是中间者2 。中间者1既提供发布者A的消息也提供发布者B的消息,而中间者2只提供来自发布者B的消息。

注意统一性原则是怎样成为互联网设计里的重要原则之一的:不管你把你笔记本插入到那个插头里,你使用的是哪种wi-fi或者给你提供互联网访问的是哪个ISP,你看到的世界总是相同的。

 

可扩展性原则

 

可扩展性原则描述的是当拓扑要么因为节点超负荷要么因为连接拥挤而不能处理负载时,通过给这个拓扑增加新的节点应当可能解决这个问题。而且增加的节点数是随着负载线性增长的。

让我们看一看违反可扩展性原则的模式。最简单的非可扩展性模式是分割一个应用为固定数目的功能块。想象一个由账务和人力资源功能组合的大应用。这儿,一个单独的盒子不能处理这样的负载,编程人员也许决定分割为账务功能和人力资源功能为独立执行部分,这样它们运行在两个盒子上:

这样的设计无法满足可扩展性测试。当两个盒子不能处理负载时,在不重写应用的情况下,没有办法增加第三个盒子。 

 
 

注意:ØMQ里的模式是由一对套接字来表示的。

非可扩展性模式的一个更复杂的例子是分布式日志记录:

随着记录日志的应用数目的增长,日志记录器的负载就增加,直到它不能处理负载为止。在应用与日志记录器之间增加中间节点不能真正地解决这个问题。无论是否有中间节点,所有的信息都不得不到达日志记录器,因此无论如何在某一刻日志记录器就会爆。

为了使这种“数据收集”的模式可扩展,中间节点不得不聚集信息,即发送固定数量的信息到下游,并且与上游应用的数目无关。这种聚集采用了计算总数、传递统计而不是像以上所说传递消息等。这种聚集模式在发布/订阅模式里用来前传订阅的。通过控制订阅请求的绝对数量使得发布者不会超负荷运行,请求会聚集到中间节点,并且只有增量的那部分才进一步发送给上游(这个算法的详细讨论请参考这儿)。

再次提醒,注意互联网是怎样遵循可扩展性原则的。可在任何时候增加新节点,不管是最终用户的盒子还是中间框架,并且不会损害整体互联网的功能和性能。

 

插入原则

 

插入原则描述的是向拓扑里插入中间节点应当不会更改终端节点的行为。(注意插入中间节点可用来扩展这个拓扑。具体的例子可以看看这儿

让我们看一个经常请求确定连接到一个特定终端节点的对等节点数的特性的例子。我们看一看下面的转换,这儿中间节点I是插入到这个拓扑的:

 

正如所看到的,终端节点A的行为在中间节点插入到这个拓扑的时候更改了。不是汇报三个对等节点,现在它汇报了两个对等节点。因此展示连接的对等节点数破坏了插入原则。

再次说明,插入原则对互联网运行的方式来说十分重要。如果在中间更改了拓扑-即当骨干网操作员增加了新的路由器- 将破环终端节点上的应用,那么互联网将以非常快的速度陷入到崩塌的状态之中。

 

结论

 

这篇文章里所描述的结构展示的是包含两个意外的ØMQ的当前设计,这儿的两个意外指的是正在开发的特性。希望这篇文章提供了分布式消息的简短介绍,并为这个领域里的将做的工作打好基础。

 

ZMQ 0MQ ZeroMQ

分享到:
评论加载中,请稍后...
创APP如搭积木 - 创意无限,梦想即时!
回到顶部