`

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中文说明文档带目录

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

    ZeroMq封装源码

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

    zeroMQ guide

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

    zeromq, zmq_init, 源码

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

    c++的消息中间件zeromq

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

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

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

    fabric3-ftp-spi-1.9.6.zip

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

    消息中间件之ActiveMQ视频课程

    消息中间件,已经成为互联网企业应用系统内部通信的核心手段,是目前企业内主流标配技术,它具有解耦、异步、削峰、签收、事务、流量控制、最终一致性等一系列高性能架构所需功能。 当前使用较多的消息中间件有...

    大数据技术分享 JStorm介绍 JStorm-分布式实时计算引擎 共40页.pptx

    JStorm作为一款分布式实时计算引擎,因其高度的定制化、优秀的稳定性和扩展性,成为阿里巴巴集团内部大规模应用的实时计算平台。它的出现弥补了开源Storm在处理阿里巴巴复杂业务需求时的不足,为实时大数据处理提供...

    局域网聊天

    标题中的“局域网聊天”指的是在同一个网络环境下,如办公室、家庭或学校内部网络,通过特定的软件或协议实现的即时通讯功能。这种通信方式无需依赖互联网,而是利用局域网(LAN)内的直接连接进行信息交换,因此...

    MQ之ActiveMQ.mmap

    它具有解耦、异步、削峰、签收、事务、流量控制、最终一致性等一系列高性能架构所需功能。 当前使用较多的消息中间件有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ等, 本次以Apache的ActiveMQ作为切入点...

    【SparkStreaming篇01】SparkStreaming之Dstream入门1

    在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。DStream提供了多种操作,如map、reduce、join、window等,可以对数据流进行处理和分析。 背压机制(Backpressure)是Spark ...

    Storm简介.pdf

    此外,Storm的部分核心内部是用Clojure编写,可能对不熟悉该语言的开发者造成一定学习曲线。 总的来说,Storm是应对实时计算需求的一个强大工具,它在实时数据处理领域提供了强大的功能和灵活性,广泛应用于实时...

    新浪kanyun安装步骤及其依赖包

    8. `pyzmq-2.2.0.1.zip`: PyZMQ是Python接口的ZeroMQ库,ZeroMQ是一个高性能的消息队列,Kanyun可能用它来实现异步通信和任务调度。 安装步骤通常包括以下几个阶段: 1. **环境准备**:确保系统已经安装了Python...

    zmq源码src

    zeromq,通常简称为ZMQ,是一种高性能的消息中间件,它提供了强大的异步消息传递机制,被广泛用于分布式计算和微服务架构中。本文将深入解析ZMQ的源码,探讨其在Linux环境下的实现原理,以帮助你更好地理解和应用ZMQ...

    消息队列及消息中间件

    通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等等功能,其作为分布式系统架构中的一个重要组件,有着举足轻重的地位。2.

    ruby中文档

    "Ruby Hacking Guide.pdf"可能是一本专注于Ruby内部机制和扩展编程的指南。Ruby的源代码是开源的,这使得开发者可以深入研究其工作原理,并根据需要自定义或扩展语言。这本书可能涵盖了解析器、编译器、垃圾回收、C...

    grenache:Bitfinex的基于DHT的高性能微服务框架

    Grenache是​​Bitfinex提供的基于DHT的高性能微服务框架。 它是去中心化的,并针对性能进行了优化。 因为它很简单,所以易于理解和设置。... ZeroMQ传输:超快速,非常适合内部网络 模式: PUT / GET:将数据

    游戏服务器框架

    Framework是整个游戏服务器的基础架构,它定义了服务的生命周期管理、错误处理、日志记录、配置管理等通用功能。一个良好的框架应提供清晰的扩展点,方便开发者根据需求进行定制。 8. **数据持久化**: 游戏数据...

    通俗易懂的消息中间件ActiveMQ教程(含配套资料)

    消息中间件已经成为互联网企业应用系统内部通信的核心手段,是目前企业内主流标配技术,它具有解耦、异步、削峰、签收、事务、流量控制、最终一致性等一系列高性能架构所需功能。当前使用较多的消息中间件有RabbitMQ...

Global site tag (gtag.js) - Google Analytics