`

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-4.2.0.tar.zip

    标题中的"zeromq-4.2.0.tar.zip"是指ZeroMQ库的4.2.0版本,它被封装在一个ZIP压缩包中,而内部包含的文件是tar归档格式。ZeroMQ是一个开源的消息中间件,它提供了一个高级的消息队列模型,允许应用程序之间进行高效...

    ZeroMQ中文说明文档带目录

    3. **消息格式**:ZeroMQ不规定消息的内部结构,开发者可以自由定义消息内容。但为了提高互操作性,通常推荐使用标准的数据序列化格式,如JSON或Protocol Buffers。 4. **错误处理**:ZeroMQ的API会返回错误代码,...

    zeromq介绍

    - **与IPC相比**:ZeroMQ不仅支持单机内部的进程间通信,还支持跨多主机的通信。 - **与CORBA相比**:ZeroMQ不会强制使用复杂的消息格式。 - **与RPC相比**:ZeroMQ完全支持异步通信,参与者可以随时增加或删除。 #...

    ZeroMq封装源码

    `router-dealer`模式是ZeroMQ中的一种高级模式,用于构建灵活的多对多通信架构。`ROUTER`角色负责接收客户端的请求,并将它们路由到适当的`DEALER`角色。`DEALER`则扮演worker的角色,处理请求并返回结果。这个模式...

    ZeroMQ 云时代极速消息通信库.pdf

    ZeroMQ内部维护了一个消息队列,用于存储等待处理的消息。这个队列是基于内存的,因此具有非常高的效率。当消息从一个Socket传入时,会被放置到队列中,然后由另一个Socket从中取出并处理。 ##### 3. 多线程与多...

    zeroMQ guide

    ZeroMQ的核心理念是将网络通信抽象成一系列的插座(Socket),这些插座可以连接到不同的端点,如进程内部、同一台机器上的其他进程、甚至跨网络的其他机器。这种抽象使得开发者无需关注底层网络细节,只需像操作本地...

    ZeroMQ指导(目录完整版) 不要积分

    - **输入/输出线程**:介绍ZeroMQ内部线程模型,以及如何管理I/O操作以提高程序的响应性和效率。 - **消息模式**:深入分析了ZeroMQ支持的各种消息传递模式,包括但不限于请求-响应、发布-订阅等。 - **利用消息来...

    zeromq, zmq_init, 源码

    zeromq是一个强大的开源消息库,它提供了高效、灵活的异步消息传递机制,被广泛应用于分布式计算、微服务架构以及各种系统间通信场景。在zeromq中,`zmq_init`是一个至关重要的函数,它是初始化zeromq库的关键步骤。...

    c++的消息中间件zeromq

    在压缩包文件`zeromq-2.0.7`中,我们可以找到ZeroMQ的早期版本源代码,这对于理解其内部实现机制和进行定制化开发非常有帮助。通过对源码的学习,开发者可以更深入地了解ZeroMQ如何实现上述特性,并将其运用到自己的...

    zeromq-4.3.2.zip

    这个压缩包"zeromq-4.3.2.zip"包含了zeromq的4.3.2版本,它在互联网技术领域有着广泛的应用,尤其在分布式系统和微服务架构中,用于高效地处理和传递消息。 zeromq的核心概念是套接字(sockets),它提供了一种灵活...

    PX4源码开发人员文档(一)——软件架构.pdf

    PX4源码开发人员文档(一)——软件架构.pdf提供了PX4软件架构的详细介绍,包括软件堆栈结构、内部进程通信、安全和保护模型、PX4应用程序框架、节点句柄、发布和订阅等知识点,为开发人员提供了丰富的参考资源。

    A 100% native C# implementation of ZeroMQ for .NET.zip

    ZeroMQ,也被称为0MQ或ØMQ,是一个高性能、轻量级的消息队列库,它为分布式计算提供了灵活且高效的消息传递机制。这个压缩包文件包含的是一个专门为.NET平台定制的100%原生C#实现的ZeroMQ版本。在.NET环境中,这...

    fabric3-ftp-spi-1.9.6.zip

    Fabric3 是一个基于Java的分布式平台,它提供了服务发现、配置管理和容错等功能,适用于构建可扩展的微服务架构。FTP SPI(Service Provider Interface)则是Fabric3中用于FTP服务的接口,允许开发者通过插件化的...

    zer0mqXt:使用dotnet核心和C#键入安全的zeroMQ模式

    zer0mqXt是一个专为使用.NET Core和C#开发人员设计的库,它提供了对ZeroMQ消息传递库的安全、类型安全的访问。ZeroMQ,也称为ØMQ或0MQ,是一个高性能、轻量级的消息队列系统,适用于构建分布式应用程序。通过zer0...

    开源应用程序架构 二(The Architecture of Open Source Applications 2)

    本章深入探讨了GDB的工作原理、内部架构及其使用技巧。对于从事软件开发的人来说,了解一个强大的调试工具是非常重要的。 **5. The Glasgow Haskell Compiler (GHC)** - **作者:** Simon Marlow, Simon Peyton-...

    libzmq-master源码

    通过获取并研究这个源代码,开发者可以深入理解ZeroMQ内部的工作机制,包括其协议、线程模型、错误处理和性能优化等方面。这对于想要定制化ZeroMQ功能或者对其进行二次开发的开发者来说是非常宝贵的资源。 【标签】...

    Zguide文档中文翻译

    由于ZeroMQ在内部使用了大量的内存管理机制,因此了解如何检测和修复内存泄露对于维护高质量的应用程序至关重要。 **ZMQ多线程编程:** `mtserver.c`示例展示了如何在一个进程中使用多个线程来处理消息。这有助于...

    Load-Balancer:使用 zeromq 作为 MOM 的 NodeJS 负载均衡器实现

    zeromq是一个强大的开源库,它提供高性能、轻量级的消息队列机制,适用于多种编程语言,包括NodeJS。 首先,我们来看zeromq如何在负载均衡场景中工作。zeromq提供多种模式,如PUB/SUB(发布/订阅)、REQ/REP(请求/...

Global site tag (gtag.js) - Google Analytics