以ZeroMQ谈消息中间件的设计【译文】

本文主要是探究学习比较流行的一款消息层是如何设计与实现的程序员


      ØMQ是一种消息传递系统,或者乐意的话能够称它为“面向消息的中间件”。它在金融服务,游戏开发,嵌入式系统,学术研究和航空航天等多种环境中被使用。算法

      消息传递系统基本上像应用程序的即时消息同样工做。应用程序决定将事件传送到另外一个应用程序(或多个应用程序),它组装要发送的数据,点击“发送”按钮,消息传递系统负责其他的事情。然而,与即时消息传递不一样,消息传递系统没有GUI,而且在出现问题时,在端点处没有人可以进行智能干预。 所以,消息系统必须是容错的而且比常见的即时消息传送快得多。缓存

  1. ØMQ最初被构想用因而一个针对股票交易的极速的消息传递系统,因此重点是极端优化。该项目的第一年用于设计基准方法,并尝试定义一个尽量高效的架构。
  2. 后来,大约在第二年的发展时,重点转向了提供一个通用系统,该系统用于构建分布式应用程序和支持任意消息模式,多种传输机制,任意语言绑定等。
  3. 在第三年,重点主要是提升可用性和扁平化学习曲线。 咱们采用了BSD套接字API,试图清除单个消息模式的语义,等等。 
      本文将深刻了解上述三个目标如何转化为ØMQ的内部架构,并为那些正在努力解决相同问题的人提供一些提示或技巧。
      从第三年开始,ØMQ它的代码库已经增加地过大; 因此有一个倡议来标准化其使用的有线协议,以及在Linux内核中实验性地实现一个相似ØMQ的消息系统等。这些主题在这里就不涉及了。 可是,你能够获取在线资源( online resources)以获取更多详细信息。

Application vs. Library

      ØMQ是一个消息库,而不是一个消息服务器。咱们花了几年时间研究AMQP协议(一个金融行业尝试标准化企业消息传递的有线协议),为其编写参考实现并参与了好几个大规模的基于消息传递技术的大型项目,并最终意识到意识到使用经典客户端/服务器模型的智能消息传递服务器(代理)和哑消息传递客户端的方法有问题。服务器

      咱们首要关注的是性能:若是中间有一个服务器,每一个消息必须经过网络两次(从发送方到代理,从代理到接收方),这在延迟和吞吐量方面都会有必定代价。 此外,若是全部消息都经过代理传递,在某一时刻,服务器必然成为瓶颈。 网络

      次要关注的是大规模部署:当部署跨组织(如:公司等)时,管理整个消息流的中央受权的概念再也不适用。因为商业秘密和法律责任,没有公司愿意将控制权交给不一样公司的服务器。在实践中的结果是,每一个公司有一个消息服务器,用桥接器链接到其余公司的消息传递系统。整个系统所以严重分散,而且为每一个涉及的公司维护大量的桥接器不会使状况更好。为了解决这个问题,咱们须要一个彻底分布式的架构,该架构中每一个组件均可能由不一样的业务实体控制。考虑到基于服务器的架构中的管理单元是服务器,咱们能够经过为每一个组件安装单独的服务器来解决上述问题。在这种状况下,咱们能够经过使服务器和组件共享相同的进程来进一步优化设计。这样咱们最终获得一个消息库。 多线程

      ØMQ开始时,咱们有一个想法,即如何使消息工做没有中央服务器。 它须要将消息的整个概念颠倒过来,而且基于端到端原则,使用“智能端点,哑网络”架构来替换自主集中存储网络中心的消息的模型。 这个决定的技术将决定ØMQ从一开始就是是一个消息库,而不是一个应用程序。

      咱们已经可以证实这种架构比标准方法更高效(更低的延迟,更高的吞吐量)和更灵活(很容易构建任意复杂的拓扑,而不是限定为经典的hub-and-spoke模型)。架构

      其中一个出乎意料的结果是,选择库模型改善了产品的可用性。 一次又一次,用户因没必要安装和管理独立的消息服务器而感到开心。 事实证实,没有服务器是一个首选项,由于它下降了运营成本(不须要有一个消息服务器管理员),并加快上线时间(无需与客户协商是否运行服务器,以及管理或运营团队的问题) 。并发

学到的教训是,当开始一个新的项目时,若是可能的话应该选择库设计。从一个简单的程序调用库能够很容易建立一个应用程序; 然而,几乎不可能从现有的可执行文件建立库。 库模型为用户提供了更多的灵活性,同时节省了他们没必要要的管理工做。

 Global State

  全局变量不能很好地与库交互。 即便只有一组全局变量,库可能在进程中也会加载屡次。 图1显示了一个从两个不一样的独立库中使用的ØMQ库的状况。 而后应用程序使用这两个库的示例框架

 

 

 

 

  图1: ØMQ 库在两个不一样的独立库中被使用异步

  当这种状况发生时,ØMQ的两个实例访问相同的变量,致使竞态条件,奇怪的错误和未定义的行为。为了防止这个问题的出现,ØMQ库中没有全局变量。相反,库的用户负责显式地建立全局状态变量。包含全局状态的对象称为context。 虽然从用户的角度来看,context看起来或多或少像一个工做线程池,但从ØMQ的角度来看,它只是一个存储任何咱们碰巧须要的全局状态的对象。在上图中,libA有本身的context,libB也有本身的context。没有办法让他们中的一个破坏或颠覆另外一个。

 这里的教训很明显:不要在库中使用全局状态。若是你这样作,当它刚好在同一个进程中被实例化两次时,库极可能会被中断。


Performance

  当ØMQ项目启动时,其主要目标是优化性能。 消息传递系统的性能使用两个度量来表示:吞吐量 - 在给定时间内能够传递多少消息; 延迟 - 消息从一个端点到另外一个端点须要多长时间。 

  咱们应该关注哪一个指标? 二者之间的关系是什么? 不是很明显吗? 运行测试,将测试的总时间除以传递的消息数,获得的是延迟。 单位时间内的消息数是吞吐量。 换句话说,延迟是吞吐量的逆值。 简单,对吧?

  咱们花了几个星期详细评估性能指标而不是当即开始编码,从而发现吞吐量和延迟之间的关系远没有那么简单,并且是与直觉相反的。 

  想象A发送消息到B(参见图2)。 测试的总时间为6秒。 有5个消息已经过。 所以,吞吐量为0.83个消息/秒(5/6),延迟为1.2秒(6/5),对吗?

  图二:从A发送消息到B

  再看看图二。 每一个消息从A到B须要不一样的时间:2秒,2.5秒,3秒,3.5秒,4秒。 平均值是3秒,这与咱们原来计算的1.2秒相差很大。 这个例子显示了人们对性能指标直观倾向的误解。

  如今来看看吞吐量。 测试的总时间为6秒。 然而,对于A而言,它只须要2秒就能够发送完全部的消息。 从A的角度来看,吞吐量为2.5 msgs / sec(5/2)。 对于B而言,接收全部消息须要4秒。 因此从B的角度来看,吞吐量为1.25 msgs / sec(5/4)。 这些数字都不符合咱们原来计算的1.2 msgs / sec的结果。

  长话短说:延迟和吞吐量是两个不一样的指标; 这很明显。重要的是要了解二者之间的差别及其关系。延迟只能在系统中的两个不一样点之间度量; 单独在点A处没有延迟的概念。每一个消息具备其本身的延迟。你能够获得多个消息的平均延迟; 而消息流是没有延迟的。

  另外一方面,只能在系统的单个点处测量吞吐量。发送端有一个吞吐量,接收端有一个吞吐量,二者之间的任何中间点都有一个吞吐量,可是没有整个系统的总体吞吐量。而吞吐量只对一组消息有意义; 没有单个消息的吞吐量的概念。

  至于吞吐量和延迟之间的关系,事实证实真的有一种关系; 然而,公式涉及积分,咱们不会在这里讨论它。 有关更多信息,请阅读有关排队理论的文献。 在基准化消息系统中有不少的陷阱,咱们不会进一步深刻。 咱们应该把精力放在学到的教训上:确保你理解你正在解决的问题。 即便一个简单的问题,“让程序更快”也须要大量的工做才能正确理解。 更重要的是,若是你不理解这个问题,你可能会在你的代码中构建隐式假设和流行的神话,使得解决方案有缺陷,或者至少要复杂得多或者比可能的少。


 Critical Path

  咱们在优化过程当中发现三个因素对性能有相当重要的影响:

  1. 内存分配数
  2. 系统调用数
  3. 并发模型 

  然而,不是每一个内存分配或每一个系统调用对性能有相同的影响。咱们对消息传递系统感兴趣的性能是在给定时间内咱们能够在两个端点之间传输的消息数。或者,咱们可能感兴趣的是消息从一个端点到另外一个端点须要多长时间。

  然而,鉴于ØMQ是为具备长链接的场景设计的,创建链接所需的时间或处理链接错误所需的时间基本上是不相关的。这些事件不多发生,所以它们对总体性能的影响能够忽略不计。 

  一个代码库的反复频繁使用的部分被称为关键路径; 优化应该关注关键路径。

  让咱们看看一个例子:ØMQ并无在内存分配方面进行极大优化。例如,当操做字符串时,它一般为转换的每一个中间阶段分配一个新字符串, 可是,若是咱们严格查看关键路径(实际的消息传递),咱们会发现它几乎不使用内存分配。若是消息很小,则每256个消息只有一个内存分配(这些消息保存在一个大的分配的内存块中)。此外,若是消息流稳定,没有巨大的流量峰值,则关键路径上的内存分配数量将降至零(已分配的内存块不会返回到系统,而是重复使用)。

经验教训:优化产生显著差别的地方。优化不在关键路径上的代码段是是无效的。


Allocating Memory

  假设全部基础设施都已初始化,而且两个端点之间的链接已创建,则在发送消息时只须要为一个东西分配内存:消息自己。所以,为了优化关键路径,咱们必须研究如何为消息分配内存并在堆栈中上下传递。

  在高性能网络领域中的常识是,经过仔细平衡消息分配内存的成本和消息复制的成本(例如,对小,中和大消息的不一样处理)来实现最佳性能。对于小消息,复制比分配内存要代价小。根本不分配新的存储器块,而是在须要时将消息复制到预分配的存储器是有意义的。另外一方面,对于大消息,复制比内存分配代价大。将消息分配一次,并将指针传递到分配的块,而不是复制数据是有意义的。这种方法称为“零拷贝”。

  ØMQ以透明的方式处理这两种状况。 ØMQ消息由不透明句柄表示。 很是小的消息的内容直接编码在句柄中。 所以,复制句柄实际上复制了消息数据。当消息较大时,它被分配在单独的缓冲区中,而且句柄仅包含指向缓冲区的指针。建立句柄的副本不会致使复制消息数据,这在消息是兆字节长时是有意义的(图3)。 应当注意,在后一种状况下,缓冲器被引用计数,使得其能够被多个句柄引用,而不须要复制数据。

  图三:消息拷贝(或没有消息拷贝)

经验教训:在考虑性能时,不要假设有一个单一的最佳解决方案。可能发生的是,存在问题的多个子类(例如,小消息 vs. 大消息),每一个都具备其本身的最佳算法。


 Batching

  已经提到,消息系统中的必定系统调用的数量可能致使性能瓶颈。其实,这个问题比那个更广泛。 遍历调用堆栈相关时会有不小的性能损失,所以,当建立高性能应用程序时,避免尽量多的堆栈遍历是明智的。

  考虑图4.要发送四个消息,你必须遍历整个网络栈四次(ØMQ,glibc,用户/内核空间边界,TCP实现,IP实现,以太网层,NIC自己和从新备份栈)。

  图四:发送四个消息

  可是,若是您决定将这些消息合并到单个批消息中,则只有一次遍历堆栈(图5)。对消息吞吐量的影响多是很是显著的:高达两个数量级,特别是若是消息很小,而且其中几百个能够打包成一个批消息时。

  图五:Batching messages

  另外一方面,批量化会对延迟产生负面影响。让咱们举个例子,知名的Nagle算法,在TCP中实现。它将出站消息延迟必定量的时间,并将全部累积的数据合并到单个数据包中。显然,分组中的第一消息的端到端等待时间比最后一个的等待时间多得多。所以,对于须要得到一致的低延迟来关闭Nagle算法的应用程序来讲,这是很常见的。甚至经常在堆栈的全部层次上关闭批量化(例如,NIC的中断合并功能)。可是没有批量化意味着大量遍历堆栈并致使低消息吞吐量。咱们彷佛陷入了权衡吞吐量和延迟的困境。 

  ØMQ尝试使用如下策略提供一致的低延迟和高吞吐量:当消息流稀疏而且不超过网络堆栈的带宽时,ØMQ关闭全部批量化以提升延迟。这里的权衡在某种程度上是会使CPU使用率变高(咱们仍然须要常常遍历堆栈)。 然而,这在大多数状况下不被认为是问题。

  当消息速率超过网络栈的带宽时,消息必须排队(存储在存储器中),直到栈准备好接受它们。排队意味着延迟将增加。若是消息在队列中花费了一秒钟,则端到端延迟将至少为1秒。 更糟糕的是,随着队列的大小增长,延迟将逐渐增长。若是队列的大小没有限制,则延迟可能会超过任何限制。

  已经观察到,即便网络堆栈被调到尽量低的延迟(Nagle的算法被关闭,NIC中断合并被关闭,等等),因为排队效应,延迟仍然多是使人沮丧的,如上所述。

  在这种状况下,大量开始批量化处理是有意义的。没有什么会丢失,由于延迟已经很高。另外一方面,大量的批处理提升了吞吐量,而且能够清空未完成消息的队列 - 这反过来意味着等待时间将随着排队延迟的减小而逐渐下降。一旦队列中没有未完成的消息,则能够关闭批量化处理,以进一步改善延迟。

  另外一个观察是,批量化只应在最高层次进行。 若是消息在那里被批量化,则较低层不管如何都不须要批处理,所以下面的全部分批算法不作任何事情,除了引入附加的等待时间。

经验教训:为了在异步系统中得到最佳吞吐量和最佳响应时间,请关闭堆栈的最底层上的批量化算法而且在在最高层次进行批量化。只有当新数据的到达速度比可处理的数据快时才进行批量化处理。


 Architecture Overview

  到目前为止,咱们专一于使ØMQ快速的通用原则。如今,让咱们看看系统的实际架构(图6)。 

  图六:ØMQ architecture

  用户使用所谓的“sockets”与ØMQ交互。 它们很是相似于TCP套接字,主要的区别是每一个套接字能够处理与多个对等体的通讯,有点像未绑定的UDP套接字。

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

  在工做线程中存在各类对象。每一个对象都由一个父对象拥有(全部权由图中的简单实线表示)。父对象能够在与子对象不一样的线程中。大多数对象直接由套接字拥有; 然而,有几种状况下,对象由套接字拥有的对象所拥有。 咱们获得的是一个对象树,每一个套接字有一个这样的树。 这种树在关闭期间使用; 没有对象能够本身关闭,直到它关闭全部的子对象。 这样咱们能够确保关机过程按预期工做; 例如,等待的出站消息被推送到网络优先于结束发送过程。

  大体来讲,有两种异步对象:在消息传递中不涉及的对象和另一些对象。前者主要作链接管理。例如,TCP侦听器对象侦听传入的TCP链接,并为每一个新链接建立引擎/会话对象。相似地,TCP链接器对象尝试链接到TCP对等体,而且当它成功时,它建立一个引擎/会话对象来管理链接。 当此类链接失败时,链接器对象尝试从新创建链接。 

  后者是正在处理数据传输自己的对象。 这些对象由两部分组成:会话对象负责与ØMQ套接字交互,引擎对象负责与网络通讯。 只有一种会话对象,可是对于ØMQ支持的每一个底层协议有不一样的引擎类型。 所以,咱们有TCP引擎,IPC(进程间通讯)引擎,PGM引擎(可靠的多播协议,参见RFC 3208)等。引擎集是可扩展的 (在未来咱们能够选择实现 WebSocket引擎或SCTP引擎)。 

  会话与套接字交换消息。 有两个方向传递消息,每一个方向由管道对象处理。每一个管道基本上是一个优化的无锁队列,用于在线程之间快速传递消息。 

  最后,有一个context对象(在前面的部分中讨论,但没有在图中显示),它保存全局状态,而且能够被全部的套接字和全部的异步对象访问。


Concurrency Model

      ØMQ的要求之一是利用计算机的多核; 换句话说,能够根据可用CPU内核的数量线性扩展吞吐量。  

  咱们之前的消息系统经验代表,以经典方式使用多个线程(临界区,信号量等)不会带来不少性能改进。 事实上,即便在多核上测量,消息系统的多线程版本可能比单线程版本慢。 单独的线程花费太多时间等待对方,同时引起了大量的上下文切换,从而使系统减速。
  考虑到这些问题,咱们决定采用不一样的模式。 目标是避免彻底锁定,让每一个线程全速运行。 线程之间的通讯是经过在线程之间传递的异步消息(事件)提供的。 这正是经典的Actor模型。

  这个想法的思想是为每一个CPU核心启动一个工做线程(有两个线程共享同一个核心只会意味着不少上下文切换没有特别的优点)。每一个内部ØMQ对象,好比说,一个TCP引擎,将绑定到一个特定的工做线程。 这反过来意味着不须要临界区,互斥体,信号量等。 此外,这些ØMQ对象不会在CPU核心之间迁移,从而避免高速缓存污染对性能的负面影响(图7)

  图七:Multiple worker threads

  这个设计使不少传统的多线程问题消失了。 然而,须要在许多对象之间共享工做线程,这反过来意味着须要某种协做多任务。 这意味着咱们须要一个调度器; 对象须要是事件驱动的,而不是控制整个事件循环。 也就是说,咱们必须处理任意事件序列,即便是很是罕见的事件,咱们必须确保没有任何对象持有CPU太长时间; 等等 

  简而言之,整个系统必须彻底异步。 没有对象能够作阻塞操做,由于它不只会阻塞自身,并且会阻塞共享同一个工做线程的全部其余对象。 全部对象必须成为状态机,不管是显式仍是隐式。 有数百或数千个状态机并行运行,你就必须处理它们之间的全部可能的交互,而且最重要的是关闭过程。

  事实证实,以干净的方式关闭彻底异步系统是一个很是复杂的任务。 试图关闭一千个移动部件,其中一些工做,一些空闲,一些在启动过程当中,其中一些已经自行关闭,容易出现各类竞态条件,资源泄漏和相似状况。 关闭子系统绝对是ØMQ中最复杂的部分。 对Bug跟踪器的快速检查代表,大约30%-50%的报告的错误与以某种方式关闭相关。

得到的经验:在努力实现最佳性能和可扩展性时,请考虑actor模型; 它几乎是这种状况下惟一的方法。 可是,若是你不使用像Erlang或ØMQ这样的专用系统,你必须手工编写和调试大量的基础设施。 此外,从一开始,想一想关闭系统的过程。 它将是代码库中最复杂的部分,若是你不清楚如何实现它,你应该能够从新考虑使用actor模型。 


Lock-Free Algorithms

  无锁算法最近一直流行起来。 它们是线程间通讯的简单机制,它不依赖于内核提供的同步原语,例如互斥体或信号量; 相反,它们使用原子CPU操做(诸如原子compare-and-swap(CAS))来进行同步。 应当理解,它们不是字面上无锁的,而是在硬件级别的幕后进行锁定。

  ØMQ在管道对象中使用无锁队列在用户的线程和ØMQ的工做线程之间传递消息。 ØMQ如何使用无锁队列有两个有趣的方面。

  首先,每一个队列只有一个写线程和一个读线程。 若是须要1对N通讯,则建立多个队列(图8)。 考虑到这种方式,队列没必要关心同步写入器(只有一个写入器)或读取器(只有一个读取器),它能够以额外的高效方式实现。

 

  图八:Queues

  第二,咱们意识到虽然无锁算法比传统的基于互斥的算法更高效,但原子CPU操做仍然代价较高(尤为是在CPU核心之间存在争用时),而且对每一个写入的消息和/或每一个消息执行原子操做读的速度比咱们能接受的要慢。 

  加快速度的方法是再次批量处理。 想象一下,你有10条消息要写入队列。 例如,当收到包含10条小消息的网络包时,可能会发生这种状况。 接收分组是原子事件; 因此你不会只获得一半。 这个原子事件致使须要向无锁队列写入10条消息。 对每条消息执行原子操做没有太多意义。 相反,能够在队列的“预写”部分中累积消息,该部分仅由写入程序线程访问,而后使用单个原子操做刷新它。 

  这一样适用于从队列读取。 想象上面的10个消息已经刷新到队列。 阅读器线程可使用原子操做从队列中提取每一个消息。 然而,它是超杀; 相反,它可使用单个原子操做将全部未决消息移动到队列的“预读”部分。 以后,它能够逐个从“预读”缓冲区检索消息。 “预读”仅由读取器线程拥有和访问,所以在该阶段不须要任何同步。

  图9左侧的箭头显示了如何经过修改单个指针能够将预写缓冲区刷新到队列。 右边的箭头显示了队列的整个内容如何能够经过不作任何事情而修改另外一个指针来转移到预读。 

  图九:Lock-free queue

得到的教训:无锁算法很难发明,麻烦执行,几乎不可能调试。 若是可能,请使用现有的成熟算法,而不是发明本身的。 当须要最佳性能时,不要仅依赖无锁算法。 虽然它们速度快,但经过在它们之上进行智能批处理能够显着提升性能。 


 API

  用户接口是任何产品的最重要的部分。 这是你的程序中惟一能够看到的外部世界。 在最终用户产品中,它是GUI或命令行界面。 在库中它是API。

  在早期版本的ØMQ中,API基于AMQP的交换和队列模型。 (参见AMQP specification。)从历史的角度看,有趣的是看看2007年的白皮书(white paper from 2007),它试图权衡AMQP与无代理的消息模型。 我花了2009年年末重写它几乎从零开始使用BSD套接字API。 这是转折点; ØMQ从那时起就被快速采用。 虽然以前它是一个被一群消息专家使用的niche产品,后来它成为任何人的一个方便的常见工具。 在一年多的时间里,社区的规模增长了十倍,实现了约20种不一样语言的绑定等。

  用户接口定义产品的感知。 基本上没有改变功能 - 只是经过更改API - ØMQ从“企业消息传递系统”产品更改成“网络消息传递系统”产品。 换句话说,感受从“大型银行的一个复杂的基础设施”改变为“嗨,这有助于我将个人10字节长的消息从应用程序A发送到应用程序B”。

得到的经验:了解您想要的项目是什么,并相应地设计用户接口。 不符合项目愿景的用户接口是100%要失败的。

  迁移到BSD Sockets API的一个重要方面是,它不是一个革命性的新发明的API,而是一个现有的和知名的。 实际上,BSD套接字API是今天仍在使用的最古老的API之一; 它可追溯到1983年和4.2BSD Unix。 它被普遍稳定了使用几十年。 

  上述事实带来了不少优势。 首先,它是一个你们都知道的API,因此学习曲线很是短。 即便你历来没有据说过ØMQ,你能够在几分钟内构建你的第一个应用程序,由于你可以重用你的BSD套接字知识。

  此外,使用普遍实现的API能够实现ØMQ与现有技术的集成。 例如,将ØMQ对象暴露为“套接字”或“文件描述符”容许在同一事件循环中处理TCP,UDP,管道,文件和ØMQ事件。 另外一个例子:实验项目给Linux内核带来相似ØMQ的功能,实现起来很简单。 经过共享相同的概念框架,它能够重用许多已经到位的基础设施。

  最重要的是,BSD套接字API已经存活了近三十年,尽管屡次尝试更换它意味着在设计中有一些固有的合理的地方。 BSD套接字API设计者已经(不管是故意仍是偶然) 作出了正确的设计决策。 经过采用这套API,咱们能够自动共享这些设计决策,甚至能够不知道他们是什么,他们要解决什么问题。 

经验教训:虽然代码重用已经从好久前获得重视而且模式重用在后来被加以考虑,但重要的是以更通用的方式考虑重用。 在设计产品时,请看看相似的产品。 检查哪些失败,哪些已成功; 从成功的项目中学习。 Don't succumb to Not Invented Here syndrome。 重用思想,API,概念框架,以及不管你以为合适的东西。 经过这样作,能够作到容许用户重用他们现有的知识。 同时,可能会避免目前还不知道的技术陷阱。


Messaging Patterns

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

  一种方法是采用UNIX的“作一件事,并作好”的哲学。 这意味着,问题领域应该被人为地限制在一个小的而且易于理解的区域。 而后程序应该以正确并详尽的方式解决这个限制的问题。 消息传递领域中的这种方法的示例是MQTT。 它是一种用于向一组消费者分发消息的协议。 它不能用于任何其余用途(好比说RPC),但它很容易使用,而且用作消息分发很好。 

  另外一种方法是关注通用性并提供强大且高度可配置的系统。 AMQP是这样的系统的示例。 它的队列和交换的模型为用户提供了几乎任何路由算法定义的方法。 固然,权衡,须要关心不少选项。  

  ØMQ选择前一个模型,由于它容许基本上任何人使用最终产品,而通用模型须要消息传递专家使用它。 为了演示这一点,让咱们看看模型如何影响API的复杂性。 如下是在通用系统(AMQP)之上的RPC客户端的实现:

 1 connect ("192.168.0.111")
 2 exchange.declare (exchange="requests", type="direct", passive=false,
 3     durable=true, no-wait=true, arguments={})
 4 exchange.declare (exchange="replies", type="direct", passive=false,
 5     durable=true, no-wait=true, arguments={})
 6 reply-queue = queue.declare (queue="", passive=false, durable=false,
 7     exclusive=true, auto-delete=true, no-wait=false, arguments={})
 8 queue.bind (queue=reply-queue, exchange="replies",
 9     routing-key=reply-queue)
10 queue.consume (queue=reply-queue, consumer-tag="", no-local=false,
11     no-ack=false, exclusive=true, no-wait=true, arguments={})
12 request = new-message ("Hello World!")
13 request.reply-to = reply-queue
14 request.correlation-id = generate-unique-id ()
15 basic.publish (exchange="requests", routing-key="my-service",
16     mandatory=true, immediate=false)
17 reply = get-message ()

  另外一方面,ØMQ将消息传递分为所谓的“消息模式”。 模式的示例是“发布/订阅”,“请求/回复”或“并行化流水线”。 每一个消息模式与其余模式彻底正交,而且能够被认为是一个单独的工具。

  如下是使用ØMQ的请求/回复模式从新实现上述应用程序。 注意如何将全部选项调整减小到选择正确的消息模式(“REQ”)的单一步骤:

1 s = socket (REQ)
2 s.connect ("tcp://192.168.0.111:5555")
3 s.send ("Hello World!")
4 reply = s.recv ()

  到目前为止,咱们认为具体的解决方案比通用解决方案更好。咱们但愿咱们的解决方案尽量具体。然而,同时,咱们但愿为咱们的客户提供尽量普遍的功能。咱们如何才能解决这个明显的矛盾?

答案包括两个步骤:

  1. 定义堆栈的层以处理特定问题区域(传输,路由,呈现等)。
  2. 提供该层的多个实现。对于每一个用例应该有一个单独的不相交的实现。

  让咱们来看看Internet栈中传输层的例子。它意味着在网络层(IP)的顶部上提供诸如传送数据流,应用流控制,提供可靠性等的服务。它经过定义多个不相交解决方案:TCP面向链接的可靠流传输,UDP无链接不可靠数据包传输,SCTP传输多个流,DCCP不可靠链接等。

  注意每一个实现是彻底正交的:UDP端点不能说TCP端点。 SCTP端点也不能与DCCP端点通讯。这意味着新的实现能够在任什么时候候添加到堆栈,而不会影响堆栈的现有部分。相反,失败的实现能够被忘记和丢弃而不损害做为总体的传输层的可行性。

  相同的原则适用于由ØMQ定义的消息模式。消息模式在传输层(TCP和朋友)之上造成层(所谓的“可伸缩性层”)。单独的消息模式是该层的实现。它们是严格正交的 - 发布/订阅端点不能说请求/回复端点等。模式之间的严格分离意味着能够根据须要添加新模式,而且失败的新模式的实验赢得“不利于现有模式。

得到的经验:在解决复杂和多方面的问题时,可能会发现单一通用解决方案可能不是最好的解决方法。相反,咱们能够将问题区域看做一个抽象层,并提供该层的多个实现,每一个集中在一个特定的定义良好的用例。在这样作时,请仔细描述用例。确保范围,什么不在范围内。太明显地限制用例,应用程序可能会受到限制。然而,若是定义的问题太宽泛,产品可能变得太复杂,模糊,并使用户产生混淆。


Conclusion

  随着咱们的世界变得充满了许多经过互联网链接的小型计算机 - 移动电话,RFID阅读器,平板电脑和笔记本电脑,GPS设备等 - 分布式计算的问题再也不是学术科学的领域,而且成为常见的平常问题 为每一个开发者解决。 不幸的是,解决方案主要是具体领域的hacks。 本文总结了咱们系统地构建大规模分布式系统的经验。 关注从软件架构的角度来看有趣的问题,但愿开源社区的设计师和程序员会发现它有用。


MartinSústrik是消息传递中间件领域的专家。 他参与了AMQP标准的建立和参考实施,并参与了金融行业的各类消息传递项目。 他是ØMQ项目的创始人,目前正在致力于将消息传递技术与操做系统和Internet栈进行集成。 本文摘自并修改自《The Architecture of Open Source Applications: Volume II》。

 原文连接:ZeroMQ: The Design of Messaging Middleware

相关文章
相关标签/搜索