史上最快的消息队列:ZeroMQ

ZeroMQ

最近在公司实习使用到这个消息传输框架,发现传输效率真的不是一般的快,而且还非常任性地支持随机连接顺序,即可以client先连接、server后上线,在服务器的应用上还可以用于分布式扩展。这里向大家安利一波,也算是记录自己在ZMQ方面的学习的过程~

官方说法: ZMQ是一个简单好用的传输层,像框架一样的的一个socket library,它使得Socket编程更加简单、简洁和性能更高。它是一个消息处理队列库,可在多线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是“成为标准网络协议栈的一部分,之后进入Linux内核”。现在还未看到它们的成功,但是,它无疑是极具前景的、并且是人们更加需要的“传统”BSD套接字之上的一层封装。ZMQ让编写高性能网络应用程序极为简单和有趣。

特点

连接先后顺序无关、自动重连机制。ZeroMQ的socket有个特色就是对于谁先bind谁后connect之类的完全都不在乎,如果是connect先行,发现连接无法建立,ZMQ会自动重试,当bind也确立了,连线就自动接上了。

多人Socket连线、多位址绑定。ZMQ的socket之间可以多个互相连线,所以一个socket的另一端可能有N个结点连接,除此之外,同一个socket也可以绑在不同的位址上。

自动负载平衡。REQ端会将send的message用Round-robin的方式(这个又是什么?)分给所有远端连线,而你有多少个连线,他都会照一样的规则分配。

讯息传输。ZMQ可以提供你传送多段资料在一笔讯息里,因此我们在这看到的send_multipart和recv_multipart作用即为如此,有了这样的特性,就不同担心通讯协定的问题,只需专心处理讯息。

支援不同的通讯方式。TCP、IPC(目前只支援Linux下的domain socket)、基于UDP的广播通讯方式(节省大量的重复封包传送)、thread之间的通讯方式,故如果你的两个节点放在同一个process里,为了效能考量,你可以用这种协定让通讯效率最佳化。

更多的样式

REQ/REP样式:一个request一个response,而REQ端会对所有的连线做fairly queue,也就是会公平第把request塞给REP端。

PUB/SUB样式:观察者模式,特点是所有PUB发送的消息会广播给所有SUB的连线,而且SUB可以设定只要某段字串开头的讯息。

PUSH/PULL样式:当我们需要将资料往某个方向负载平衡地推送,就可以使用这种样式,PUSH会将负载分散给PULL端,而且只能由PUSH推往PULL。

PAIR样式:和一般的socket一样,一对一连线。

多样式参杂实现分散式服务器

使用ZMQ,使分散式系统的实现难度降低,因为通讯的部份由ZMQ来完成了,开发者只要专心考虑节点之间的拓扑与连接方式、通讯方式,以及讯息的处理。ZMQ的本意是用于即时处理大量的金融资料,效率更是ZMQ的金字招牌之一。

ZeroMQ只是一个网络编程的Pattern库,将常见的网络请求形式(分组管理,链接管理,发布订阅等)模式化、组件化,简而言之socket之上、MQ之下。对于MQ来说,网络传输只是它的一部分,更多需要处理的是消息存储、路由、Broker服务发现和查找、事务、消费模式(ack、重投等)、集群服务等。

为什么我希望用C而不是C++来实现ZeroMQ

​ ZeroMQ是需要长期连续不停运行的一个网络库,它应该永远不会出错,而且永远不能出现未定义的行为。因此,错误处理对于ZeroMQ来说至关重要,错误处理必须是非常明确的而且对错误应该是零容忍的。

​ 但是C++的异常处理机制却无法满足这个要求。C++的异常机制对于确保程序不会失败时非常有效的——只要将主函数包装在try/catch块中,然后你就可以在一个单独的位置处理所有的错误。然而,当你的目标是确保没有未定义行为发生时,噩梦就产生了。C++中引发异常和处理异常时松耦合的,这使得在C++中避免错误是十分容易的,但却使得保证程序永远不会出现未定义行为变得基本不可能。

在C语言中,引发错误和处理错误的部分是紧耦合的,它们在源代码中处于同一个位置,使得我们在错误发生时能很容易理解到底发生了什么。

ZeroMQ的层级模型

最顶层的是ZObject与IPollEvent。

ZObject是所有ZeroMQ体系中类的父类,它存在的意义是发送与接收命令(命令是指告诉ZeroMQ该做什么,需要做什么)。

IPollEvent则是一个接口,定义了若干操作,包括读操作,写操作,客户端请求连接,服务端应答连接,超时操作等共5个操作,其实现类包括Req、Rep等具体Socket,该接口的目的是顶你一终端间发生操作时的行为。

Ctx是一个上下文类,通常一个终端只需要创建一个上下文。

IOObject本身并没有太多的属性,主要是其内部维护了一个IOThread。

MailBox是一个重要的类,它被用作处理命令,包括命令的发送与接收,需要注意的是,这里的命令其实是本地发送的,不是端点间发送的。

Pipe用于处理接收到或者需要发送的数据,是实际存储待处理数据的数据结构,其内部是用队列的形式实现的。

LBFQ全名分别为“LoadBalance”和“FairQueue”,也就是负载均衡与公平排队分别用于处理要发送的数据与要接收的数据。

SocketBase是例如Req、Rep、Pull等包装后Socket 的父类。其内含有一对Pipe,用于在SocketBase与SessionBase之间传递消息,具体传递过程在接下去说明。

SessionBase是创建SocketChannel并与目标终端进行连接的地方,是与底层Poller最先进行交互的一层(通过StreamEngine进行交互)。具有超时重连,断线重连等功能。

Poller是整个ZeroMQ的核心,它实现了命令的发送与接收,数据的发送与接收。由他来真正的发送数据到其他终端,也是他处理来自其他终端的数据后交给SessionBase。

基于此层级模型的交互逻辑:

发送消息 Socket -> Session -> StreamEngine -> Poller

接收消息 Poller -> StreamEngine -> Session -> Socket