`

ZeroMQ的内部架构(一)

阅读更多

部分翻译自www.zeromq.org/whitepapers:architecture

 

概述

想要了解ZMQ内部结构的人越来越多,大量关于代码库的讨论经常提到的问题是缺乏一个可以让新人快速了解代码结构的架构文档。

本文的目的便是提供一种这样的文档。本文会逐步覆盖整个代码库,但是不会去关注太多的细节问题。因为随着时间的推移,细节部分可能与文档脱节。如果想要获得详细信息,你应该查看相关部分的源码。

首先需要提醒读者的是代码库是复杂的。从代码行数(也许是意大利面式的代码行数,我想ZMQ应该不是意大利面)的角度来看代码库并不复杂(目前有10000行)。想法,由于ZMQ要考虑大量不同的组合,因此是很复杂的。比如,它需要在超过10个操作系统的不同版本上运行;需要运行在许多不同的指令体系结构,从ARMItanium;可以由不同的编译器编译,从gccMSVCSunStudio;可以与20多种不同语言绑定进行交互;可以使用不同的底层传输协议,不同的进程间消息传递机制并支持可靠多播;支持不同的消息模式:远程过程调用,数据分发,并行流水线等;每个socket可以连接或被连接到对等socket,或者同时建立双向连接;两个节点之间的通信失败可能是由于底层连接的中断也可能是暂时性的网络问题等等。所有这些选项是相互正交的,需要考虑到上千种可能的组合。

以上意思是说代码即使看起来很简单,很容易上手,但实际上很复杂。到目前为止,代码大约花费了10人年的工作量,每一行的代码上的花费大约是两个小时,包括像“i + +;”这样的代码。因此,在对代码做任何更改之前要小心仔细的了解代码在做什么和为什么要这样做。

全局状态

在库中使用全局变量绝对是一个搬起石头砸自己的脚的最佳方式。一切都工作良好除非库被链接到可执行文件两次(见图片),而这时你就会遇到古怪的错误和崩溃。为了防止这类问题, libzmq 没有使用全局变量,而是由使用者负责显式的创建全局状态。包含全局状态的对象被称为上下文”(context)。从使用者的角度来看 context 或多或少像是供ZMQ socket使用的I/O线程池,在libzmq的观点来看context只是一个存储libzmq所需要的全局状态的对象。例如,进程内部使用的可用端点(endpoint)列表(已经关闭的但仍然逗留在内存中的ZMQ socket列表,之所以逗留在内存中是由于在上下文中有需要发出的消息)就存储在上下文中。

 

上下文由类 ctx_t 实现。

 


 

并发模型

 

表面来看ZMQ的并发模型的可能会让人感到费解。令人费解的原因是,我们使用自己独特的消息传递方式来实现并发性和可扩展性。这样设计的好处是,在多线程环境中,ZMQ的使用者不必使用互斥锁,条件变量或信号等用来同步并行处理的东西。ZMQ中的每个对象只会在自己所在的线程上执行,其他线程只能通过发送消息(以下称之为命令,以区别于使用者发送给ZMQ的消息)与对象进行交互而不能直接使用对象(这就是为什么是不需要互斥体的原因),同时对象间也可以相互发送命令进行交互(实际上线程在ZMQ中也是一种对象)。

 

对象由类object_t定义,类中定义了发送命令和处理命令的接口。如果想要在自定义对象之间传递命令,对象类只需要简单的派生自object_t,并定义处理命令的程序就可以了。

 

命令由类command_t 实现,command_t 中定义了所有可用的命令。比如有一个带有单参数'linger'“term”(销毁)命令,以参数linger=100发送term命令到对象p中,可以像下面这样调用

send_term(p,100);

另一方面,如果你想给对象定义一个处理“term”命令的程序,可以这样做:

 

void my_object_t::process_term(int linger) 
{ 
    // 处理动作在这里实现 
}

需要注意的是以上实现只有当应用类派生自object_t类才有效!

 

对于大多数命令,ZMQ可以保证命令在传递过程中目标对象不会消失,也就是说命令可以保证投递。(关于如何实现这一保证,可以查看本文中关于对象树模型的相关描述,对象数模型将异步对象绑定至树状的层次结构。)不过对于少数跨越对象树的命令需要额外的操作来实现保证投递:对于这类命令,发送者在向接收方发送命令之前需要调用接收方的 inc_seqnum() 方法来获得这一保证。inc_seqnum()增加接收方中的计数 sent_seqnum。当接收方处理命令时,会增加另一个计数processed_seqnum。在接收方将要销毁时,如果发现processed_seqnum小于sent_seqnum,就说明有正在传送而没有处理的命令,这时接收方就不会继续执行销毁动作。销毁操作的逻辑对 object_t own_t 来说是透明的,命令发送方和接收方只需要发送和接收命令,而无需关心命令的具体序列号seqnum

备注:事实上,有些数据被限制在临界区中。使用临界区遵循以下两个规则:

1          在任何时候,数据对每个线程都是可访问的,(例如,前面提到的同一进程内的端点列表)。

2         在消息传递的过程中不应使用临界区中的数据。

线程模型

 

从操作系统来看,ZMQ中只有两种线程,应用线程和I/O线程。应用线程在ZMQ外部创建,访问ZMQAPII / O线程在ZMQ内部创建,用于在后台发送和接收消息。thread_t是系统级线程的抽象,可以以OS无关的方式创建线程。

 

而从ZMQ的观点来看,线程只是一个拥有邮箱(mailbox_t)的对象。邮箱存储发送给居住在当前线程上所有对象的信件(命令command_t),所有这些对象公用线程上的邮箱。线程从邮箱中按序获取命令并交给其上的对象进行处理。

 

目前ZMQ内部使用两种不同类型的线程(拥有邮箱的对象):I/O线程(io_thread_t)socket(socket_base_t)

 

I / O线程很容易理解,每个 I/O 线程与一个系统级线程一一对应。I / O线程运行在自己的系统线程上,并且拥有独立的获取命令的邮箱。

 

socket在某种程度上显得复杂一些。每个ZMQ socket拥有自己的接收命令的邮箱,因此socket可被ZMQ视为分离线程。而实际上,一个应用程序线程可以创建多个套接字,也就是说多个ZMQ socket被映射到同一个系统线程。更加复杂的是,ZMQ socket可以在系统线程之间迁移。例如,Java语言绑定可以在单线程中使用ZMQ socket,而当线程结束时,ZMQ socket会传递给垃圾回收线程,并在垃圾回收线程上销毁。

 

I / O线程

I / O线程(io_thread_t)ZMQ异步处理网络IO的后台线程。它的实现非常简洁。io_thread_t实现继承object_t ,并实现 i_poll_events 接口,其内部包含一个邮箱(mailbox_t)和一个poller对象(poller_t)

 

继承object_t使得io_thread_t能够发送和接收command(如 stop 命令,当收到该命令时,I / O线程将被终止)。

 

i_poll_events 接口定义了文件描述符和计时器事件就绪时的回调处理函数(in_event/out_event/timer_event)。io_thread_t 实现此接口(in_event)来处理来自mailbox的事件。当mailbox_t事件触发时,io线程从mailbox中获取命令,并让命令的接收者进行处理。

 

mailbox_t 用来存储发送给任何居住在io_thread_t 上的object_t 的命令,每个io_thread_t 上有多个对象,这些对象公用同一个邮箱,邮箱的收件人就是对象。mailbox_t本质是一个具有就绪通知功能的存储命令的队列。就绪通知机制由signaler_t提供的文件描述符实现。队列是由ypipe_t实现的无锁无溢出队列。

 

poller_t 是从不同操作系统提供的事件通知机制中抽象出来的概念,用来通知描述符和计时器事件,poller_t 通过 typedef定义为操作系统首选的通知机制(select_t/poll_t/epoll_t )。所有运行在 io_thread_t上的对象都继承自辅助类 io_object_t,该类实现了向io_thread_t注册/删除文件描述符 (add_fd/rm_fd)和计时器(add_timer/cancel_timer)事件的功能,同时io_object_t 还继承了 i_poll_events 接口来实现事件回调功能。

 

 

 

对象树模型

ZMQ库的内部,对象在大多数情况下被组织成树状层次结构。树的根节点只能是供应用使用的ZMQ socket (socket_base_t)


 

树中的每个对象可以处在不同的线程上。想要将子节点束缚在根节点对应的线程上是不可行的,因为根节点(ZMQ socket)处于应用程序线程上,而其他节点则处在I/O 线程上:


 

对象树模型产生的主要目的是为了实现一致性的销毁机制(对象销毁)。经验做法是在对象销毁之前,向其所有子对象发送销毁请求命令,当收到所有孩子对销毁请求的确认时对象才会真正被销毁。

由于命令处理是按序的,销毁请求和销毁确认机制能够刷新在对象之间送中的命令。这就是为什么大多数命令(在对象树内传递的命令)不需要使用命令序列号(见上文)来保证当有正在传送的消息时对象不会销毁的原因。

当孩子对象决定销毁自身而父对象没有向它发出销毁请求时,销毁过程会变得更加复杂,例如TCP连接断开时回话对象会销毁。我们必须仔细处理由父对象向子对象发起销毁请求,子对象自身发起销毁请求,以及二者同时发生时的情况。最后发现只要子对象销毁自身时向其父对象发出销毁子对象的请求就可以同一处理以上三种情况。下面是以上三种情况的时序图。与销毁有关的有三种命令:父对象要求子对象销毁的term命令,子对象向父对象的销毁确认term_ack命令,子对象想销毁自身时向父对象发送的term_req 命令。

 

 

在上图的最后一种情况下term_req 命令被父对象简单地丢弃。因为父对象已经向子对象发出了销毁请求(term),所以重新发送没有任何意义。如果父对象发送两次term请求,第二次请求到达时孩子已被释放,将会造成segmentation fault错误或覆盖现有的内存。

对象树机制通过类 own_t 来实现,own_t 定义了对象树中的节点。own_t 继承自 object_t 使得对象树中的每一个节点可以发送和接收命令(在销毁阶段需要接收和发送命令)。需要注意的是,并不是每个对象都在对象树中。有些对象可以发送和接收命令,但不属于对象树(如管道端点)。

 

回收线程

 上一小节描述了与销毁相关的机制。而销毁任何一个指定的对象(包括socket)消耗的时间是不确定的。然而,我们希望close有类似于POSIX的行为:当关闭TCP套接字时,即使在后台还有没有完全发出的数据,调用也会立即返回。

所以,应用程序线程调用 close 时,ZMQ应该关闭对应的套接字,但是,我们不能依赖应用线程来完成socket子对象的销毁(销毁可能需要多次命令交互)。同时应用线程调用zmq_close以后不会在继续使用该socket甚至可能永远不会再调用 ZMQ库函数。因此,ZMQ socket应该从应用线程迁移到一个工作线程来处理销毁的逻辑。一个可能的解决方案是将socket迁移到某个后台的I/O线程上去,然而ZMQ可以初始化为具有零个I / O线程(适用于只在进程间通信的情况),因此,我们需要一个专门的回收线程来执行销毁任务。

回收线程由类reaper_t实现。socket通过(send_reap)向回收线程发送回收命令,回收线程收到命令后会将socket从应用线程迁移到回收线程上,这样socket就可以在回收线程上处理命令(term/term_ack),直到socket的所有子对象都成功销毁时,socket就会在回收线程上销毁。实际上回收线程只是待回收对象驻留的线程,对象的处理逻辑仍然由对象自身处理。

 

未完待续...

  • 大小: 2.8 KB
  • 大小: 5.7 KB
  • 大小: 3.8 KB
  • 大小: 5.6 KB
  • 大小: 35.5 KB
分享到:
评论

相关推荐

    基于ZeroMQ的一对一网络类

    偶尔在云风的博客看到了ZeroMQ这个网络库,想到我自己刚好要做课程设计,于是便载下来粗粗看了一下。 因为我做的是KTV系统的点播端与播放端间的通信,所以是一对一模式,只需要Request-Reply模式就可以了。 客户端...

    zeromq的windows版本安装包

    “ZMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是...

    ZMQ/ZeroMQ使用手册

    像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程 间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、 发布-订阅、任务分发、请求-应答等。ZMQ 的快速足以胜任集群...

    ZeroMQ一个强大的Socket库

    “ZMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是...

    消息队列zeromq学习的安装包之一libsodium

    消息队列zeromq学习的安装包之一libsodium,为学习zeromq安装部署的同学们提供帮助,zeromq是消息队列中的一种技术,安装过程中要注意版本对应。

    ZeroMQ(java)window库

    ZeroMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。(摘自百度百科) ZMQ官方网址http://zeromq.org/ ZMQ本身只提供了C++版本的下载,...

    中文版ZeroMQ文档

    中文版ZeroMQ文档

    zeromq-2.1.7

    zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-2.1.7zeromq-...

    zeromq-4.1.4.zip

    ZeroMQ是一个开源的消息队列系统,按照官方的定义,它是一个消息通信库,帮助开发者设计分布式和并行的应用程序。 首先,我们需要明白,ZeroMQ不是传统的消息队列系统(比如ActiveMQ、WebSphereMQ、RabbitMQ等)。...

    zeromq安装包

    “ZMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是...

    ZeroMQ指南.pdf

    ZeroMQ指南.pdf

    zeromq教程

    ZeroMQ是一个很有个性的项目,它原来是定位为“史上最快消息队列”,所以名字里面有“MQ”两个字母,但是后来逐渐演变发展,慢慢淡化了消息队列的身影,改称为消息内核,或者消息层了。从网络通信的角度看,它处于...

    zeromq_java.rar_java zeromq_libzmq.lib _zeromq_zeromq java

    ZeroMQ学习资料,java项目。有一些简单的例子,可以好好的学习下。

    zeromq-4.1.3.zip

    ZeroMQ是一个开源的消息队列系统,按照官方的定义,它是一个消息通信库,帮助开发者设计分布式和并行的应用程序。 首先,我们需要明白,ZeroMQ不是传统的消息队列系统(比如ActiveMQ、WebSphereMQ、RabbitMQ等)。...

    ZEROmq实例

    ZEROmq实例,简单的zeromq的例子,vs下可以运行

    zeromq guide

    zeromq guide zeromq指南

    zeromq-4.3.2.zip

    zeroMQ 4.3.2 源码包,开源的一个消息中间件。可以下载下之后在各平台编译。 号称是可以替换socket的接口。

    ngx_zeromq, 针对 Nginx的ZeroMQ传输.zip

    ngx_zeromq, 针对 Nginx的ZeroMQ传输 ngx_zeromq 是一个传输模块,它允许 nginx 在与上游服务器通信时使用面向ZeroMQ的传输层。它是 7层协议不可知的,这意味着它可以与任何性能良好的上游模块( 。proxy,fastcgi,...

    zeromq+qt通信源码

    zeromq与qt的client与server通信源码

    ZeroMQ 2.1.11 类图

    ZeroMQ 2.1.11 类图

Global site tag (gtag.js) - Google Analytics