ZeroMQ

作者:Martin Sústrik

原文链接:http://aosabook.org/en/zeromq.html

ØMQ是一个消息系统,或者如果你愿意“面向消息中间件”。它用于各种场景,比如金融服务、嵌入式系统、学术研究及航天航空。

消息系统主要为应用程序传递即时消息。应用程序决定向另一个(或多个)应用程序发送一个消息,它组装要发送的数据,点击“发送”按钮,这就好了——消息系统负责余下部分。

不像即时消息,消息系统没有GUI,并假设在出错时,在末端没有人类的干预。因此,消息系统必须容错并且比普通的即时消息要更快。

ØMQ一开始构想为用于股票交易的超快消息系统,因此关注点在于极致优化。项目的第一年花在了设计基准测试方法上,并尝试定义一个尽可能高效的架构。

随后,大约在开发的第二年,关注点转移到为构建分布式应用程序提供一个通用系统,支持任意消息模式、各种传输机制、任意语言绑定等。

在第三年,关注点主要在于提高可用性及降低学习曲线。我们采用了BSDSockets API,尝试澄清各个消息模式的语义,等等。

希望本章能给出上面三个目标如何转换为ØMQ内部架构的一个深刻理解,并为面临相同问题苦苦挣扎的人们提供一些启示。

自其第三年起,ØMQ的代码库已不再合适;有一项计划是标准化它所使用的有线协议,并在Linux内核里实验性地实现一个类ØMQ的消息系统。本书不讨论这些话题。不过,更多细节你可以参考网络资源:http://www.250bpm.com/conceptshttp://groups.google.com/group/sp-discuss-grouphttp://www.250bpm.com/hits

24.1.应用程序与库

ØMQ是库,而不是消息服务器。我们花费了数年研究AMQP协议,金融行业标准化商业消息有线协议的一次尝试——编写了一个参考实现,并参与几个深度基于消息技术的大型项目——认识到智能消息服务器(中介,broker)与哑消息客户端的经典客户端/服务器模型中存在问题。

彼时我们的主要关注点是性能:如果在中间有一个服务器,每条消息必须两次经过网络(从发送者到中介,从中介到接受者),导致了就时延与吞吐率而言的一个性能损失。另外,如果所有的消息都通过中介,在某个时刻它一定会成为瓶颈。

第二个关注点关于大规模部署:在部署跨越组织边界时,整个消息流的中央集权管理不再适用。没有公司愿意受制于其他公司的服务器;存在商业秘密与法律责任。实践中,结果是每公司一个消息服务器,使用手写的桥将它连接到其他公司的消息系统。因此,整个生态系统严重碎片化,并且为每个涉及的公司维护大量的桥并不能改善这个情形。为了解决这个问题,我们需要一个完全分布式的架构,一个每个公司都可能受制于另一个商业实体的架构。鉴于在基于服务器的架构中,管理单元是服务器,我们可以通过为每个部分安装一个独立的服务器来解决这个问题。在这样的情形里,我们可以使服务器与相应部分共享相同的进程来进一步优化设计。最终我们得到一个消息库。

在我们有了一个如何无需中央服务器使消息工作的想法时,ØMQ启动了。它要求将消息传递的整个概念翻转过来,将在网络中心自发集中存储消息的模型替换为基于点到点原则的“智能终点,哑网络”的架构。这个决策的技术成果是,ØMQ从一开始就是库,而不是应用程序。

同时,我们已经能够证明这个架构更高效(更低时延、更高吞吐率)以及更灵活(它很容易构建复杂的拓扑,而不是拘泥于传统的轮辐模型,hub-and-spokemodel)。

不过,一个意料外的结果是,选择库模型提高了产品的可用性。对无需安装及管理一个单独的消息服务器,用户再三表达了他们的喜悦。事实证明没有服务器是一个最佳方案,因为它减少了操作成本(无需管理消息服务器),并改进了上市时间(无需与客户、管理团队或运营团队协调服务器运行)。

经验教训是,在启动一个新项目时,如果可能,你应该选择库设计。通过从一个平凡程序调用库,创建应用程序相当容易;不过,从一个现存的可执行文件里创建库是几乎可不能的。库为用户提供大得多的灵活性,同时节省了他们可观的管理工作。

24.2.全局状态

全局变量不适用库。在进程中,一个库可能被载入几次,但即使这样,只有一组全局变量。图24.1展示了被两个不同、独立库使用的一个ØMQ库。应用程序使用了这两个库。


图24.1:被不同库使用的ØMQ

在发生这种情况时,ØMQ的两个实例访问相同的变量,导致竞争条件、奇怪的失败以及未定义的行为。

为了防止这个问题,ØMQ库没有全局变量。相反,库的使用者负责显式地创建全局状态。全局状态中包含的对象被称为上下文(context)。而在用户看来,上下文看起来就像一个工作者线程池,在ØMQ看来,它只是一个保存任意我们恰好需要的全局状态的对象。在上图中,libA有自己的上下文,libB也有自己的上下文。其中一个没有办法破坏或颠覆另一个。

这里的教训相当明显:不要在库里使用全局状态。如果你做了,在同一个进程里恰好实例化两次时,这个库很可能会垮掉。

24.3.性能

在ØMQ项目启动时,其主要目标是优化性能。消息系统的性能使用两个维度衡量:吞吐率——在给定时间内可以传递多少消息;时延——消息从一个终点到另一端需要多长时间。

我们应该关注哪个度量呢?两者间有什么关系?这不明显吗?运行测试,将测试时间除以传递的消息数,你得到的是时延。消息数除以时间,你得到的是吞吐率。换而言之,时延是吞吐率的倒数。很简单,是吧?

不是立即开始写代码,我们花了几周来细致调查性能度量,我们发现吞吐率与时延间的关系比这微妙得多,通常这些度量是相当违反直觉的。

设想A向B发送消息(参考图24.2)。测试的总体时间是6秒。传递了5条消息。因此,吞吐率是0.83msgs/sec(5/6),时延是1.2sec(6/5),对吧?


图24.2:从A到B发送消息

再看一下这张图。每条消息从A到B花了不同的时间:2秒,2.5秒,3秒,3.5秒,4秒。平均是3秒,这与我们原来计算的1.2秒有相当的差距。这个例子显示了人们在直觉上倾向于误解性能度量。

现在看一下吞吐率。测试的总体时间是6秒。不过,在A只花了2秒发出所有的消息。从A看来,吞吐率是2.5msgs/sec(5/2)。在B,花了4秒接受所有的消息。因此,从B看来,吞吐率是1.25msgs/sec(5/4)。这些数字都不符合我们原来计算的1.2msgs/sec。

长话短说,时延与吞吐率是两个不同的度量;这是显然的。重要的是理解两者间的差异以及它们相互关系。仅可以在系统的两个不同点之间测量时延;在点A没有像时延这样的东西。每条消息有自己的时延。你可以平均多条消息的时延;不过,不存在消息流时延这样的东西。

另一方面,可以在系统的一个点测量吞吐率。在发送者存在一个吞吐率,在接受者存在一个吞吐率,在两者间的任意中间点存在一个吞吐率,但不存在整个系统总体吞吐率这样的东西。仅对一组消息,吞吐率才有意义;没有单条消息吞吐率这样的东西。

至于吞吐率与时延间的关系,已经证明确实存在关系;不过,公式涉及积分,我们不在这里讨论它。更多信息,参考排队理论的文献。

在消息系统基准测试中,存在许多我们不准备深入的陷阱。重点应该是经验教训:确保你理解要解决的问题。即使如“使得它快”这样简单的问题,要正确理解,也会花费大量的时间。另外,如果你不理解这个问题,你很可能在代码里构建隐含的假设与流行的迷思,使得解决方案要么有缺陷,或者至少比可能的要复杂得多或者用处少得多。

24.4.关键路径

我们发现在优化过程中,三个因素对性能有关键的影响:

·        分配的内存数

·        系统调用数

·        并发模型

不过,每个内存分配或系统调用对性能的影响都不相同。在消息系统中,我们感兴趣的性能是,在给定时间内,在两个终点间可以传输的消息数。另外,我们可能对消息从一端到另一端需要多长时间感兴趣。

不过,鉴于ØMQ是针对长期存活连接设计的,它建立一个连接的时间或者处理一个连接错误所需的时间基本上是无关的。这些事件非常罕见,因此它们对总体性能的影响可以忽略。

频繁使用的代码库部分,称为关键路径,优化应该聚焦在关键路径。

让我们看一个例子:ØMQ没有就内存分配极度优化。例如,在操作字符串时,通常对转换的每个中间步骤分配一个新字符串。不过,如果我们认真看关键路径——实际消息传递——我们将发现它几乎不使用内存分配。如果消息是小的,仅每256个消息一次内存分配(这些消息保存在单个大的内存分配块中)。另外,如果消息流是平稳的,没有大的起伏,关键路径上的内存分配降低为零(分配的内存块不会返回给系统,而是重复使用)。

经验教训:进行有效的优化。优化不在关键路径上的代码片段是白费劲。

24.5.内存分配

假设所有的基础设施初始化了,且两个终点间的一个连接已经建立,在发送一条消息时,仅有一个东西要分配:消息本身。因此,要优化关键路径,我们必须调查消息如何分配,并在栈上下传递。

在高性能网络领域的常识是,通过仔细衡量消息分配代价与消息拷贝代价获取最好的性能(例如http://hal.inria.fr/docs/00/29/28/31/PDF/Open-MX-IOAT.pdf:参考“小”、“中”及“大”消息的不同处理)。对小消息,拷贝比分配内存代价小得多。完全避免分配新内存,在需要时将消息拷贝到预先分配的内存是合理的。另一方面,对于大消息,拷贝代价远高于内存分配。分配内存一次,然后传递该分配块的指针,而不是拷贝数据,是合理的。这个做法称为“零拷贝”。

ØMQ以透明的方式处理这两个情形。一条ØMQ消息由一个不透明(opaque)句柄表示。非常小消息的内容在句柄里直接编码。因此,拷贝句柄实际上拷贝了消息数据。在消息更大时,它分配在一个独立的缓冲里,句柄只是包含指向该缓冲的指针。拷贝句柄不会导致拷贝消息数据,这在消息是数百万字节长时,是合理的(图24.3)。应该注意到在后者,缓冲是引用计数的,因此它可以被多个句柄援引,而无需拷贝数据。


图24.3:消息拷贝(或不拷贝)

经验教训:在考虑性能时,不要假设存在一个最好的解决方案。可能存在该问题的几个子集(比如,小消息与大消息),每个有自己的最优算法。

24.6.批处理

已经提到,在一个消息系统中,系统调用的绝对数量会导致性能瓶颈。实际上,这个问题还要更普遍。存在一个与调用栈遍历相关的非平凡性能损失,因此,在创建高性能应用程序时,尽可能避免栈遍历是明智的。

考虑图24.4。要发送4条消息,你必须遍历整个网络栈4次(即,ØMQ、glibc、用户/内核空间边界、TCP实现、IP实现、以太网层、NIC本身,然后又沿着栈回来)。


图24.4:发送4条消息

不过,如果你决定将这4条消息合并为单个批次,将仅有栈的一次遍历(图24.5)。对消息吞吐率的影响会是颠覆性的:最多两个数量级,特别是如果消息是小的,且单个批次可以封装数以百计的消息。


图24.5:批处理消息

另一方面,批处理对时延有负面影响。让我们以实现在TCP里的著名的Nagle算法为例。它推迟外出消息一定时间,将所有累积数据合并到单个包中。显然,在封包中第一条消息的端到端时延比最后一条要差得多。因此,对需要一致性低时延的应用程序,关闭Nagle算法是常见的。甚至关闭栈每一级上的批处理也是常见的(即,NIC的中断合并功能)。

不过,再次的,没有批处理意味着栈的大量遍历,导致低的消息吞吐率。看起来我们落入了吞吐率、时延的两难境地。

ØMQ尝试使用以下策略来实现一致性的低时延与高吞吐率:在消息流是分散且不超过网络栈的带宽时,ØMQ关闭所有的批处理以改进时延。这里的代价是所谓更高的CPU使用率—我们不得不频繁地遍历栈。不过,在大多数情形里这不视为一个问题。

在消息速率超过网络栈的带宽时,消息必须排队—保存在内存中,直到栈准备好接收它们。排队意味着时延的增长。如果消息在队列中花了1秒,端到端时延将至少增加1秒。更糟的是,随着队列长度的增加,时延将逐渐增加。如果队列大小不受限制,时延可以任意大。

已经观察到,即使网络栈调整为最低可能时延(关闭Nagle算法,关闭NIC中断合并功能等),因为上述的排队效应,时延仍然会是令人沮丧的。

在这样的情形里,激进地启动批处理是合理的。因为时延已经很高,没有损失什么。另一方面,激进批处理提升了吞吐率,可以清空等待消息的队列——这反过来意味着随着排队时延的降低,时延将逐步降低。一旦队列中没有未完成的消息,可以关闭批处理以进一步改善时延。

另外一个经验是,批处理仅能在最顶层完成。如果消息在那里批处理,底下各层没有东西可批处理,因此下面的所有批处理算法将无事可做,除了引入额外的时延。

经验教训:在一个异步系统中,要得到最优的吞吐率与最优的响应时间,在栈的低层关闭所有的批处理算法,在最顶层批处理。仅在新数据的进入超出了处理速度,才进行批处理。

24.7.架构概览

到目前为止,我们关注在使得ØMQ快的一般性原则。从现在开始,我们将看一下该系统的实际架构(图24.6)。


图24.6:ØMQ架构

用户使用所谓的“套接字”与ØMQ交互。它们与TCP套接字相当类似,主要差别在于每个套接字可以处理与多个对端的交流,有点像非绑定的UDP套接字。

套接字对象存在于用户线程里(参考下一节对线程模型的讨论)。除此之外,ØMQ运行处理通讯异步部分的多个工作者线程:从网络读取数据、消息排队、接受进入的连接等。

在工作者线程里有各种对象。每个这样的对象由一个父对象拥有(所有权在图中由实线表示)。父亲可以存在与孩子不同的线程里。大多数对象由套接字直接拥有;不过,存在几个情形,其中对象由套接字拥有的对象拥有。我们得到的是一棵对象树,每个套接字一棵树。在关闭时使用这棵树;所有对象在关闭了所有的孩子后才能关闭。这样我们可以保证关闭的过程如预期工作;例如,在终止发送过程前,未处理的外出消息被推送到网络。

大致说来,有两种异步对象;不涉及消息传递的对象与涉及消息传递的对象。前者主要与连接管理相关。例如,TCP监听对象监听进入的TCP连接,对每个新连接创建一个engine/session对象。类似的,TCP连接器对象尝试连接到TCP对端,在成功时,它创建一个engine/session对象来管理该连接。在这样的连接失败时,连接器对象尝试重建它。

后者是处理数据传输的对象。这些对象由两部分组成:负责与ØMQ套接字交互的session对象,负责网络通讯的engine对象。只有一种session对象,但对ØMQ支持的每个协议存在不同的engine对象。因此,我们有TCPengine、IPCengine(进程间通讯)、PGMengine(一个可靠多播协议,参考RFC3208)等。Engine集是可扩展的——在将来我们可能选择实现,比如,一个WebSocketengine或者一个SCTPengine。

Session与套接字交换消息。有两个方向传入消息,每个方向由一个管道对象处理。每个管道是为了在线程间传递消息优化的无锁队列。

最后,存在保存全局状态,并可被所有套接字以及所有异步对象访问的上下文对象(在前面章节里讨论,但没有显示在图中)。

24.8.并发模型

ØMQ的一个要求是要利用多核;换而言之,与可用CPU数成正比地扩展吞吐率。

我们之前的消息系统经验表明,以经典的方式(临界区、semaphor等)使用多线程不会产生可观的性能提升。实际上,一个消息系统的多线程版本可能比单线程版本更慢,即使在多核系统上测试。单个线程在等待其他线程上花费了太多时间,与此同时,诱发了拖慢系统的大量的上下文切换。

鉴于这些问题,我们决定选择另一个模型。目的是完全避免锁,让每个线程全速运行。以线程间的异步消息传递来提供线程间的通讯。业内人士知道,这是经典的actor模型。

想法是每CPU核启动一个工作者线程——让两个线程共享同一个核只是意味着大量上下文切换,而没有特别的好处。每个ØMQ内部对象,比如说,TCPengine,将紧密绑定到一个特定的工作者线程。这反过来意味着无需临界区、互斥量、semaphor及类似的东西。另外,这些ØMQ对象将不会在CPU核间迁移,因此避免了缓存(cache)污染对性能的负面影响(图24.7)。


图24.7:多个工作者线程

这个设计使得大量传统的多线程问题消失了。不过,存在在许多对象间共享工作者线程的需要,这反过来意味着必须有某种类型的多任务协作。这意味着我们需要一个调度器;对象需要是事件驱动的,而不是由整个事件循环控制;我们必须小心事件的任意次序,即使非常罕见的;我们必须确保没有对象占用CPU太长时间,等等。

简而言之,整个系统必须是完全异步的。没有对象能承受一个阻塞操作,因为这不仅阻塞它自己,还有所有共享同一个工作者线程的其他对象。所有对象必须成为状态机,不管显式还是隐式。数以百计或千计的状态机并行运行,你必须小心它们之间所有可能的交互以及——最重要的——关闭过程。

已经证明以干净的方式关闭一个完全异步系统是令人生畏的复杂任务。尝试关闭数千个活动部分,其中一些在工作、一些空闲、一些在初始化过程中、一些已经自己关闭了,很容易出现各种竞争条件,资源泄露及类似情形。Shutdown子系统绝对是ØMQ最复杂的部分。快速检查bug跟踪器表明,大约30-50%报告的bug都与以某种方式关闭有关。

经验教训:在追求极致的性能和可扩展性的时候,考虑actor模型;它是这种情况下几乎唯一的玩家。不过,如果你不使用像Erlang或ØMQ这样一个特定系统,你将不得不手工编写和调试大量的基础设施。另外,从一开始,考虑关闭该系统的过程。它将是代码库中最复杂的部分,如果你没有如何实现它的清晰的想法,你可能应该首先重新考虑使用actor模型。

24.9.无锁算法

无锁算法最近很流行。它们是用于线程间通讯的简单机制,它们不依赖于内核提供的同步原语,比如互斥量或semaphor;它们使用原子化的CPU操作进行同步,比如原子化的比较与交换(CAS)。应该理解的是,它们并不是完全无锁——相反,锁定在硬件层面的幕后进行。

ØMQ在管道对象里使用一个无锁队列,在用户线程与ØMQ的工作者线程间传递消息。ØMQ如何使用无锁队列有两个有趣的方面。

首先,每个队列正好有一个写线程与一个读线程。如果需要1对N通讯,创建多个队列(图24.8)。鉴于这个方式,队列无需关心写线程(仅有一个写线程)或读线程(仅有一个读线程)的同步,它可以一个极高效的方式实现。


图24.8:队列

其次,我们认识到,尽管无锁算法比传统的基于互斥量的算法要高效,原子CPU操作仍然是代价相当高的(特别是CPU核间存在竞争时),为每条消息的写或读进行原子操作会比我们愿意接受的速度要慢。

再次,加速它的方法是批处理。设想你有10条消息写入队列。例如,它可以发生在当你收到一个包含10条小消息的网络包时。接收一个包是一个原子事件;你不能拿一半,这个原子事件导致需要向无锁队列写10条消息。对每条消息执行一次原子操作没有道理。相反,你可以在由写线程单独访问的队列的“预写”部分积累消息,然后使用一个原子操作冲刷它们。

这同样适用从队列读。设想上面的10条消息已经冲刷到队列。读线程可以使用原子操作从队列提取每条消息。不过,它是多余的;相反,它可以将所有等待处理的消息,使用一个原子操作移到队列的“预读”部分。随后,它可以从“预读”缓冲一个一个地提取消息。“预读”仅由读线程所有及访问,因此在该阶段无需同步。

图24.9中的箭头展示了如何通过修改指针,将预写缓冲冲刷入队列。右边的箭头展示了队列的整个内容,仅通过修改另一个指针,可以被转移到预读区。


图24.9:无锁队列

经验教训:无锁算法很难创造,难以实现,而且几乎不可能进行调试。如果可能的话,使用现有的已被证明的算法,而不是发明你自己的算法。在要求极致性能时,不要仅依赖无锁算法。尽管它们很快,在它们之上进行聪明的批处理会显著提升性能。

24.10.API

用户接口是任何产品最重要的部分。它是你程序对外可见的唯一部分,如果你做错了,全世界都会恨你。在终端用户产品中,它要么是GUI或命令行接口。在库,它是API。

在ØMQ的早期版本中,API基于AMQP的交换与排队模型(参考AMQP规范)。从历史的角度来看,尝试使AMQP与消息无中介模型一致的2007年的白皮书是有趣的。2009年底,我使用BSD套接字API几乎从头重写了它。这是转折点;从那时起,ØMQ的接受率飙升。在此之前,它是一群信息专家使用的小众产品,之后它成为了众人方便的常用工具。一年左右,社区扩大了10倍,实现了大约20个不同语言的绑定。

用户接口定义了一个产品的认知。基本上不改功能——只是改动API——ØMQ从一个“企业消息”产品改变为一个“网络”产品。换而言之,认知从“用于大银行的一个复杂基础设施的部分”改变为“嘿,这有助于我从应用A向应用B发送我的10字节长的消息”。

经验教训:理解你希望你项目成为什么,并相应设计用户接口。拥有一个与项目愿景不一致的用户界面100%保证失败。

转移到BSD套接字API的一个重要方面是,它不是一个革命性的新发明的API,而是一个现有且著名的接口。实际上,BSD套接字接口是今天仍在活跃使用的最老的API之一;它追溯到1983年的4.2BSDUnix。它已经被广泛使用并维持了几十年稳定。

上面的事实带来许多好处。首先,它是一个众所周知的API,所以学习曲线是非常平坦的。即使你从未听过ØMQ,由于能够重用BSD套接字知识,你可以在几分钟内构建第一个应用程序。

其次,使用广泛实现的API使得ØMQ能够与现有技术集成。例如,将ØMQ对象展示为“套接字”或“文件描述符”允许在相同的事件循环里处理TCP、UDP、管道、文件以及ØMQ事件。另一个例子:将类似ØMQ功能带入Linux内核的实验性项目,被证明相当容易实现。通过共享相同的概念框架,它可以重用许多已有的基础设施。

第三且可能最重要的,尽管有无数次替换它的尝试,BSD套接字API存活了几乎30年的事实,意味着在设计中存在着某种本质上正确的东西。BSD套接字API设计——不管是故意还是偶然——做出了正确的设计决策。采纳这个API,我们自然共享了这些设计决策,甚至不知道它们是什么,以及它们解决了什么问题。

经验教训:虽然很久以前已经提倡代码重用,且随后加入了模式重用,以一个更一般的方式思考重用是重要的。在设计一个产品时,看一下类似的产品。看一下哪些失败,哪些成功;从成功的项目学习。不要屈服于“没有发明”综合症。重用思想、API、概念性的框架,任何你觉得合适的东西。这样做可以允许用户重用他们现有的知识。同时你也可能避免了在当时没有意识到的技术陷阱。

24.11.消息模式(messaging patterns)

在任何消息系统中,最重要的设计问题是如何为用户提供一种方法来指定将哪些消息路由到哪些目的地。主要有两种做法,我相信这种二分法相当普遍,且基本上适用于在软件领域遇到的任何问题。

一个做法是采取Unix的“只做一件事,且把它做好”的哲学。这意味着问题域应该被人为地局限在小的且被良好理解的区域。程序应该以正确、详尽的方式解决这个受限的问题。在消息领域这样做法的一个例子是MQIT。它是一种将消息分发给一组消费者的协议。它不能用于其他(比如RPC),但它容易使用,且分发消息良好。

另一个做法是关注通用性,提供一个强大且高度可配置的系统。AMQP是这样系统的一个例子。其队列与交换模型为用户提供了以编程方式定义他们能想到的几乎任何路由算法的方法。当然,代价是要权衡很多选项。

ØMQ选择了前一个模型,因为它生成的产品基本上可为任何人使用,而通用模型要求消息专家来使用。为了展示这点,让我们看一下模型如何影响API的复杂性。下面是在一个通用系统(AMQP)上的RPC客户端的实现:

connect (“192.168.0.111”)

exchange.declare(exchange=”requests”, type=”direct”, passive=false,

durable=true,no-wait=true, arguments={})

exchange.declare(exchange=”replies”, type=”direct”, passive=false,

durable=true,no-wait=true, arguments={})

reply-queue =queue.declare (queue=””, passive=false, durable=false,

exclusive=true,auto-delete=true, no-wait=false, arguments={})

queue.bind(queue=reply-queue, exchange=”replies”,

routing-key=reply-queue)

queue.consume(queue=reply-queue, consumer-tag=””, no-local=false,

no-ack=false,exclusive=true, no-wait=true, arguments={})

request= new-message (“Hello World!”)

request.reply-to= reply-queue

request.correlation-id= generate-unique-id ()

basic.publish(exchange=”requests”, routing-key=”my-service”,

mandatory=true,immediate=false)

reply = get-message ()

另一方面,ØMQ将消息传递场景分为所谓的“消息传递模式”。模式的例子有“发布/订阅”、“请求/响应”或者“并行化管道”。每个消息模式与其他模式完全正交,可视为一个独立的工具。

下面是使用ØMQ的请求/响应模式重新实现上面的应用。注意,所有的选项调整被简化为在单个步骤里选择合适的消息模式(REQ):

s= socket (REQ)

s.connect(“tcp://192.168.0.111:5555”)

s.send(“Hello World!”)

reply = s.recv ()

到目前为止,我们认为特定的解决方案比一般的解决方案要好。我们希望我们的解决方案尽可能地特定。不过,同时我们也希望向我们的用户提供尽可能广泛的功能。我们如何解决这个明显的矛盾呢?

答案包含两步:

1.       定义栈的一层来处理一个特定的问题领域(即,传输、路由、展示等)。

2.       提供该层的多个实现。每个用例应该有一个单独的非交叉的实现。

让我们看一下互联网栈中传输层的例子。它意味着在网络层(IP)之上,提供诸如传输数据流、应用流控、提供可靠性等的服务。这通过定义多个非交叉的解决方案来实现:TCP用于面向连接的可靠流传输,UDP用于无连接、不可靠包传输,SCTP用于多流的传输,DCCP用于不可靠连接,以此类推。

注意每个实现都是完全正交的:UDP终点不能与TCP终点交谈。SCTP终点也不能与DCCP终点交谈。这意味着在任何时刻都可以添加新实现而不影响栈的现有部分。相反,失败的实现可以被遗忘、丢弃,而不会影响这个传输层的整体活力。

同样的原则也适用于由ØMQ定义的消息模式。消息模式构成了传输层(TCP及其伙伴)上的一层(所谓的“可扩展层,scalabilitylayer”)。它们严格正交——发布/订阅终点不能与请求/响应终点交谈。模式间的严格分离反过来意味着新模式可以按需加入,新模式失败的实验不会损害现有的模式。

经验教训:在解决一个复杂、多方面的问题时,可能会发现一个单一的通用解决方案可能不是最好的方法。相反,我们可以把问题域视为一个抽象层,并提供这个层的多个实现,每个关注一个特定的良好定义的用例。在这样做时,仔细描述用例。要确定什么在范围内,什么不在。太激进地限制用例,你软件的应用可能会受限。不过,如果你太宽泛地定义问题,产品可能变得太复杂、定位不清并让用户困惑。

24.12.结论

随着我们的世界充斥着许多通过互联网连接的小电脑——手机、RFID阅读器、平板与手提电脑、GPS设备等——分布式计算的问题不再是学术领域的领域,而是成为每个开发人员日常面临的问题。不幸的是,解决方案主要是领域定义的技巧。本文总结了我们以系统的方式构建大规模分布式系统的经验。它关注的是从软件架构的角度来看很有趣的问题,我们希望这对开源社区的设计者和程序员是有用的。

本项工作在Creative CommonsAttribution 3.0 Unported许可下提供。详情请参考许可的完整描述


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值