虚妄

ZeroMQ理论基础

    笔记     CPP·翻译

  1. 1. 介绍
  2. 2. 拓扑结构
  3. 3. 传输
  4. 4. 拓扑结构的建立 VS. 消息路由
  5. 5. 消息传输模式
  6. 6. Hop-by-Hop VS. End-to-End
  7. 7. 命名解析
  8. 8. 附录:设计模式
    1. 8.1. 一致性原则
    2. 8.2. 扩展性原则
    3. 8.3. 新增插入原则
  9. 9. 总结

介绍

不像其他(中心式)基于容易理解的理论基础的消息传输系统。 几乎没有关于通用式分布式消息传输系统的资源,尤其是读者特别感兴趣的ØMQ。

这篇论文的目的是解释ØMQ架构的基本元素,他们是如何相互协调以及他们这样设计的理由。

拓扑结构

拓扑结构是ØMQ的主要概念。除非你真正理解“拓扑结构”是什么意思,否则其中出现的一些概念导致混淆以及难于理解,甚至导致设计不当。

作为一个非正式的定义,我们可以把“拓扑结构”理解成参与到业务逻辑的相同的方面的一组应用程序。

例如:考虑一个图片转换服务,将图片调整到所需的大小和分辨率。所有的应用程序都提供了转换服务,所有的应用程序都使用服务。所有的中间节点,比如负载均衡,形成了一个拓扑结构。

从技术层面来说,拓扑结构有以下属性:

  • 拓扑是个图,图中的节点是应用程序,格子(lattices)是应用程序之间的数据通道。
  • 所有的应用程序在业务逻辑方面都有一致的路线协议。
  • 这个图是紧密连接的。也就是:任意两个节点要不是直接连通,要不就是通过其他中间节点间接连通。

第一点很明显。有一点需要指出的是,“通道(channel)”一词是故意使用,用来替代“连接(connection)”,来描述这个模型,甚至底层是无连接的传输,比如IP多播或UDP。

第二点说的是,在拓扑结构中,所有的应用程序都有一致消息传输(比如“图片需要调整大小”或“这是一张调整过大小的图片”),一致的消息序列(在应用程序中以状态机实现),实际的数据编码(图片数如何序列化的?RGB?CMYK?)等等。

第三点说的是,即使是有两个相同业务逻辑的部署,他们形成的也是两个拓扑结构,除非他们通过数据通道进行相互连接。

topology1

为了直观理解拓扑结构概念,重要的是理解fuzzy概念。同样fuzzy的是面向对象编程中的类。存在一个正式的定义解释类是数据成员和方法的集合。但是没有定义解释哪一部分的业务逻辑应该形成一个类,哪一部分不应该。完全依赖于程序员去决定哪个业务概念需要封装成类,哪个不需要。程序员可能会犯错,将所有的业务逻辑放到一个类中,因此几乎回避了面向对象设计;或者将逻辑分布到千万个小的类中,这就将程序员置于难以理解的相互关联的混乱之中。

同样地,没有一个单一的正确方式将业务逻辑划分到拓扑结构中。唯一的经验法则是,拓扑是扩展的原子单元。你可以将拓扑结构作为整体进行扩展,而不能只是扩展其某一方面。因此,如果你期待在将来需要将功能A与功能B相互独立,你应该直接为A单独创建一个拓扑结构,为B单独创建一个拓扑结构。

我们拿一个例子来阐述说明上述的概念:
在我们的图片转换程序中,有两个基本功能:调整图片大小和调整图片的亮度。我们可以选择将这两个功能创建一个拓扑,或者为每个功能创建“大小调整”拓扑和“亮度调整”拓扑。

前面这种场合,我们需要定义某种路线传输方式来传达我们想要的功能。比如说,消息的第一个字节为1代表“大小调整”,2代表“亮度调整”。同时我们应该注意的是,这种设计导致这两个功能是紧密耦合的。如果在将来我们打算增加更多的处理节点,他们的每一个都应该实现这两个功能。

后面这种场合,这两个功能是不相交的。在消息中,没有必要设置特殊的字段来代表选择的功能,在“大小调整”拓扑中,所有的请求都是请求调整图片大小,在“亮度调整”拓扑中,所有的请求都是请求调整图片亮度。在这个设计中,我们可以独立扩展每个拓扑结构而不影响其他的拓扑。假设我们想设计一个专用的FPGA来调整图片大小,我们可以简单地将它们连接到“大小调整”拓扑结构,而不影响“亮度调整”拓扑。如下图。

topology2

客户端可以请求调整大小(通过拓扑A)和调整亮度(通过拓扑B),worker1只能调整图片大小,worker3只能调整图片亮度,而worker2可以提供调整图片大小和调整图片亮度两个功能。

最后关于拓扑结构要注意的是,由于很清晰地在拓扑结构之间进行了划分,就可以映射到底层的传输协议的某一方面,比如TCP端口。这就允许底层的网络按照业务规范其行为。比如,测量特定拓扑结构的带宽消耗(和特定的业务逻辑,比如调整图片大小服务的带宽消耗与调整图片亮度服务的带宽消耗)。可以基于拓扑结构进行流量调整。比如减少调整图片大小服务的带宽,用来增加调整图片亮度服务的带宽等等。

传输

经常会出现在不同的传输机制的上层,除了TCP之外,会要求运行一层消息层,比如InfiniBand(处于性能考虑),IP多播或者SCTP。
原生的方法是开启一个TCP传输,然后在此之上增加一些TCP缺少的特性,比如心跳。在其他的底层传输之上提供一个相同的行为。
这个方法有几个问题:

  • 首先,在特定的协议之上,构造一个类TCP的包装器会变得很冗余。如果它的行为类似TCP,那为何不一开始就用TCP。(这条规则排除因为性能的原因)
  • 其次,一些协议不能硬塞进TCP模型中。例如:IP广播。

基于上述特定的问题,ØMQ采用了不同的方案。底层的传输保持了各自的原生特性而不用提供一个共通的东西,在上层进行了一层封装。不同的是,ØMQ提供了一组最小的接口(消息界定,消息分割和消息原子性),同时要求上层足够的通用来处理底层各个不同的传输模式。
实际中,它意味着在传输层之上有一层很”薄”的封装。例如消息界定协议(当封装TCP的时候)、消息分割协议(将长的消息分割成多个基于包传输的数据包)或late-joiner协议(当加入PGM多播流的时候,丢弃接收到消息的最后一部分)

topology3

拓扑结构的建立 VS. 消息路由

网络栈的每一层都将网络传输中的一部分复杂进行了抽象。IP层抽象掉了需要查找到路由的目标主机。TCP抽象掉了网络内在的丢包属性,提供了 可靠性保证。
ØMQ抽象掉了将要发送数据的需要指定的特定网络位置。消息被发送到拓扑结构中,而不是特定的一个端点。回忆一下,拓扑结构是和特定业务逻辑绑定的,这也就意味着,你从一个拓扑结构发送消息,你是要求这个拓扑结构给你提供特定的服务,比如是图片调整大小或者明亮度调整。ØMQ会以用户透明的方式选择一个真正的端点来接收消息。

为了落实这一原则,ØMQ严格区分了拓扑结构的建立(zmq_bind,zmq_connect)和实际消息的传输(zmq_send,zmq_recv)。
前者处理底层的传输地址,比如IP地址,后者使用一个句柄(fd)去访问特定的拓扑结构。

1
2
3
4
5
6
/* 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);

将拓扑结构的建立和消息的路由分开严格上说不是不可或缺的。毕竟,结合两个单一的函数很简单。

1
zmq_send (s, "tcp://192.168.0.111:5555", data, sizeof (data), 0);

区分开既有技术上的又有教育意义上的理由。技术上的理由是:

  • 当我们想以异步的方式从拓扑结构中接收消息时,无论如何我们必须连接到它。没有理由不继续利用这个通道进行消息的发送。
  • 将拓扑结构的建立和消息路由分开可以很好地映射到BSD的socket API(bind/connect VS. send/recv)

教育意义上的观点甚至更重要。它涉及到ØMQ是什么以及它不是什么。
底层的协议,例如TCP,允许用户向特定的端点发送数据。ØMQ是构建在此基础之上,允许用户向特定的拓扑结构发送数据而不是特定的端点。因此,如果你想要向特定的端点发送数据,你应该使用TCP或者类似的协议。如果你想要将数据发送到拓扑结构,让拓扑结构来决定数据的最终端点,你应该使用ØMQ。

不幸的是,这个概念似乎很难去理解。其实,几乎不可能去说服用户ØMQ不能用于处理特定的端点,这个不是个bug而是个特性。
将拓扑结构的建立和消息的路由分开没有解决这个问题,只是将实际的功能变得更显而易见。在这个特性中增加名称解析,希望能让事实变得更清晰。

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

消息传输模式

当谈及到拓扑结构作为消息路由的一种手段时,在不同的拓扑结构中的路由算法的区别就变得很清晰。“NASDAQ股票报价”拓扑结构将报价分布到拓扑中的每个消费者,“图片明亮度调整”拓扑结构将从客户端接收到的图片交给一个worker去转换,然后将调整过的图片发回给客户端。
ØMQ通过定义几种不同的“消息传输模式”来反应这个事实。前面这个股票报价拓扑结构,就是发布/订阅模式的一个例子。后面这个图片明亮度调节拓扑结构就是一个请求/回复模式的一个例子。

消息传输模式既定义了用于节点间的通信协议,又定义了各个独立节点的功能。e.g.用于路由消息的算法。因此,不同的模式行为看上去像不同的协议。你不能将发布/订阅节点和请求/回复节点进行连接,就好像将TCP端点不能连接到SCTP端点一样。每一个拓扑结构都只实现了一个单一的消息传输模式。不存在一种方法将两个不同的消息传输模式的拓扑结构连接成一个拓扑结构。
这个严格的区分对于拓扑结构作为一个整体来保证其行为是有必要的。只要你知道在拓扑结构中的每个端点都坚持提供发布/订阅语义,你就可以提供类似“消息会被投递在拓扑结构中的每个端点”的保证。如果拓扑结构的某一部分允许负载均衡而不是广播,你就不能做出上述那样的保证。更糟的是,消息传输模式是开放式的,你可能需要以一种完全任意的方式去扩展一个端点的行为,因此你没办法做出任何保证。
下面是网络栈的示意图。需要注意的是,不同的消息传输模式都位于栈的同一层且相互间是独立的。

topology4

考虑到一些传统的消息传输系统选择提供通用的路由基础设施允许用户在此之上基本可以构建任意的路由算法(例如AMQP),而不是提供预包装的消息传递模式。所以有必要解释一下ØMQ选择后者的理由。

  • 第一,设计一个功能齐全又没有bug的消息传输模式是很困难的任务。通过将创建模式的责任转交给用户,我们可以保证基于此消息传输系统构建的大部分应用在某些方面是有问题的。即使是这个模式实现是正确的,学习和开发的开销的成本将会超过使用预包装的消息传递模式的成本的数倍。毕竟,在DNS设计方面的一篇早期论文中提及:“[用户]想要的是使用这个系统所提供的功能,而不是理解。”
  • 第二,正式定义的模式允许执行一些需求,比如两个不同的模式不能在同一个拓扑结构中存在。消息传输系统可以检查对方是否也实现了相同的消息传输模式,如果不是则直接拒绝连接。如果由用户实现此类的模式,类似这样的检查很可能就不会存在。
  • 第三,通用的路由基础设施不能自动地支持分布式(别名:federated),这意味着只在简单的中心辐射型(hub-and-spoke)架构下才能工作,一旦想要超出这个模型,就必须要提供额外的信息。也就是回答类似“这个系统的消息传输模型是什么?”的问题。看一下基于AMQP的各类产品实现的“集群”机制。“模式”的特性依旧还存在,或者显示或者隐示(通过只支持一种模式)。
  • 最后,基于我们在AMQP的经验,尽管它提供了大量丰富的可能的消息传输模式,人们一次又一次的基于它实现相同的几个模式,而忽略了其他东西。

Hop-by-Hop VS. End-to-End

在网络栈中最巧妙的一个特性就是就是清晰的hopby-hop功能(IP)和end-to-end功能(TCP,UDP,SCTP等),也就是这个特性,将整个网络栈生态切分开,独立发展。如果没有这种功能的切分,end-to-end协议的每一个小改动对于IPv4和IPv6传输来说都是一种痛苦。
这个想法的背后就是在网络传输过程中的每个节点都实现IP协议,然而,只有终端才会实现特殊的end-to-end协议,比如TCP。换句话说,中间节点,比如路由器,不需要知道在IP层之上的end-to-end协议。

topology5

将IP和TCP层切分的经验之后推广,形成了end-to-end argument形式。end-toend argument说的是,如果它的功能不能够被底层正确地提供(在我们的情况中就是hop-by-hop),也就是,它想要正常工作需要上层(end-to-end层)的协助,那么起初在底层实现也就没多大意义了。

ØMQ坚持end-to-end协议,将栈切分到hop-by-hop层(将节点用”X”开头表示)和end-to-end层(没有用”X”开头表示),这个和上述TCP/IP的拓扑图很像。

topology6

和TCP/IP同样的是,hop-by-hop层负责消息路由,end-to-end层可以提供附加的服务,比如可靠性,加密等。

然后我们不应该将TCP/IP的比喻用在此太过了,不像网络栈中的单hop-by-hop协议(IP)和多end-to-end(TCP,UDP,SCTP等),在ØMQ的每个end-to-end协议中都有它自己的hop-by-hop协议支撑。看起来像这样。

topology7

这样安排是因为每个消息模式的的路由功能(hop-by-hop提供)都是不一样的,不能再消息模式间共享。如果在未来碰到两个消息模式可以共享一个路由算法,而只在end-to-end的协议不同,那么我们也将会在单个hop-by-hop层之上,混合多个end-to-end协议。

最终,让我们来看下hop-by-hop VS. end-to-end的具体例子。
REQ/REP模式指的是,客户端向多个worker服务端发送一个request请求,worker中的一个会处理这个请求然后产生一个reply,然后将reply发送给客户端。

topology8

hop-by-hop层要做的是将每个request请求发送到上游节点中的某一个(load-balancing),之后将reply发送给下游的接收到request的节点。

一切运行良好,直到worker处理request失败,或者因为网络的原因,整个拓扑结构的一部分脱离了整体。在这种情况下,客户端将会被阻塞,等待一个永远都不可能的reply。

为了解决这个问题,客户端需要设置一个超时,如果没有在超时时间内没有收到reply,就重发这个request。客户端也应该有能力过滤掉重复的reply。

现在回忆下end-to-end argument,重发功能没有端点的支持是不可能完成的,因此这个不太可能在hop-by-hop层进行实现。

最终我们得到的是,路由功能在hop-by-hop层实现,在此之上的end-to-end层实现可靠性。

命名解析

在写这篇文章的时间点,ØMQ不提供命名服务。为了加入到拓扑结构中,需要指定一个地址,比如IP地址和TCP端口。

在未来,我们可能会提供命名服务,将一个提供到拓扑结构的名字转换到底层的传输地址。比如,”Brightness-Adjustment-Service”可能被解析成”tcp://192.168.0.1111:5555”。

这里面需要注意的一个是命名解析服务的选择。比如当连接到”NASDAQ stock quotes”拓扑结构,用户想的是连接到本地的一个股票报价中心而不是NASDAQ自己,或者更糟的是,到竞争对手的股票报价中心。

topology9

通常情况下,命名解析服务都是通过DNS来实现。DNS是唯一个全球可用的分布式数据库。

附录:设计模式

[请参考原文]

一致性原则

扩展性原则

新增插入原则

总结

这篇文章反应的架构是当前ØMQ的设计,在未来可能会存在变数。希望通过这篇文章能够介绍一些分布的消息传输,并且在这个领域的未来可以形成一些基础方面的共识。

页阅读量:  ・  站访问量:  ・  站访客数: