首页 > 代码库 > 【大话QT之九】ZMQ偏执海盗模型调研以及模拟实现网盘负载均衡间消息通讯
【大话QT之九】ZMQ偏执海盗模型调研以及模拟实现网盘负载均衡间消息通讯
应用需求:
由于网盘服务端既需要承载用户文件目录的监控又要负责文件的上传和下载,当某一时刻用户访问量较大或用户操作较为频繁是,单台文件监控服务器和文件传输服务器往往无法满足需求,极端情况下很可能造成服务器内存和CPU使用率爆表的情况,而且当Client与文件监控服务器间网络状况不好的情况下,很有可能造成用户操作序列的丢失,即用户在客户端的操作序列没有及时反映到服务端,造成用户本地目录和服务器端存储的文件不一致的情况。基于上述情况的考虑,必须要设计一套负载均衡系统,它能够满足在用户访问量增加或用户操作增加的情况下,能过通过在多台服务器之间调度,使用户的操作顺利进行。这篇文章不重点介绍负载均衡模型的架构(会在后面的文章中进行介绍),而是重点研究它的通信模型如何实现,即在分布式的情况下如何实现多节点之间可靠的消息传递。
调研方向:
通过调研,我们决定采用ZMQ来实现我们的网络层的通信。ZeroMQ以下简称ZMQ是一个简单好用的传输层,像框架一样的一个socket library,它使得socket编程更加简单、简洁和性能更高。它是一个消息处理队列库,可在多个线程、内核和主机之间弹性伸缩。它逐渐成为一套解决分布式环境中节点之间消息通信的标准。ZMQ并不像是一个传统意义上的消息队列服务器,事实上,它也根本不是一个服务器,它更像是一个底层的网络通讯库,在Socket API之上做了一层封装,将网络通讯、进程通讯和线程通讯抽象为统一的API接口。
ZMQ提供了三个基本的通信模型,分别是"Request-Reply","Publisher-Subscriber","Parallel Pipeline",其它的模型都是在这三类基本模型的基础上通过组合装配衍生出来的。下面我们就来详细地分析ZMQ中使用心跳来实现可靠通信的偏执海盗模式的通信原理,注意:该文不是简单的介绍ZMQ如何使用,所以如果你还没有对ZMQ有个清晰的了解,强烈建议你先对ZMQ的三个基本模型有了大致的了解之后再结合本文分析会更有益处。
偏执海盗模型通信原理分析:
1> 偏执海盗通信模型图
这里我们分三部分来简单描述下偏执海盗模式的实现:Client端、Queue服务器以及Worker工作者节点。
首先,Client是建立的ZMQ_REQ类型的socket,它主要负责与Queue(ROUTER类型的socket)节点建立连接,并将自己的任务发送给Queue节点(本质就是需要处理的数据)。这里需要提出的一点是Client端对可靠性做出的努力,当任务发送给Queue节点后,在特定的时间内如果没有响应,则Client端则认定上次的链接已经失效,它会将原有建立的socket关闭,然后重新建立一个socket尝试与Queue节点建立通信,并重新发送数据。
其次,Worker节点在启动后会向Queue节点建立通信连接,并发送READY的信息申请注册,Queue节点会将该Worker的标识(其实就是socket标识符)记录下来,保存在工作者队列中,等待Queue节点给它分配任务。同时,worker会定期向Queue节点发送心跳信息(它也会接收来自Queue的心跳信息),当worker长时间没有向Queue节点发送心跳信息,在Queue端就会认定该worker已经失效,会将它在工作者队列中删除掉,这样它就不会再接收到Queue节点发送给它的任务。当worker再次重新启动时,它还会向Queue发送注册信息,会重新加入工作者队列。
最后,Queue队列在启动后会等待worker的注册,以及客户端发送消息的请求。每当有worker发送READY信息过来时,Queue节点会将该worker的标识保存在队列中,并设置一个过期时间。在特定的时间间隔内,Queue节点会接收到来自worker的心跳包,每当接收到心跳包时,Queue节点会自动更新队列中对应的worker的过期时间。当Queue中设定的指定时间后,它会统一扫描工作者队列,并向其中的所有现存节点发送心跳信息,对针对过期的节点进行删除操作。同时,每当有Client端的任务发送过来时,它会从工作者队列中取出一个worker,并将该任务分配给worker,同时会将该worker从工作者队列中剔除。当worker完成分配给它的任务后,Queue会接收到来自worker的完成任务的信息,它会将返回结果回馈给Client端,同时再将该worker加入到工作者队列中,此时该worker又可以接收来自客户端的请求了。
2> 信封及多帧消息通信的相关认知
ZMQ与传统BSD的socket的很大一个不同在于传统socket建立的是1:1的通信,则ZMQ则可以建立N:M的通信,以某种REQ-ROUTER-DEALER-REP建立的多对多的通信模型来讲,某一个client将数据包发送给ROUTER,然后经过简单负载之后发送给某个REP,在处理完之后返回给ROUTER,然后ROUTER再将结果返回给那个client,这里要重点突出的是,当有多个客户端同时请求时,它可以记住某个数据包是来自哪个客户端,当那个数据包处理完之后返回时它可会返回给某个特定的client,而不是简单地从众多的client中随机选择一个。而实现这种功能的原理就是信封,其实信封只是一个称谓,本质上来看它就是SOCKET标识符,因为只有这样我们才可以发送消息给某个特定的客户端。
当ROUTER接收数据包的时候,它会自动的将该数据包的来源记录下来,形成一个信封套在数据的外面,例如在REQ端通过zmq_setsockopt显示地设置ZMQ_IDENTITY的值,然后再发送数据包,ROUTER会采用多帧接受的方式可以接收到3个数据帧(第一帧表示socket标识、第二帧为空,第三帧为实际的数据),其结构如下:
通过逐帧地接收数据,我们就可以将数据包拆解开,即可以通过第一帧解析出它的来源地址、第三帧解析出它携带的数据。偏执的海盗模式就是使用这种方式来记录某个worker的身份,以及来自哪个Client端的数据,从而可以实现一对一发送。而当ROUTER向外发送数据包时,它会将某个具体的socket标识组装到message的头部。
3> 偏执海盗通信步骤分析
偏执海盗通信模型中数据包发送的关系图如下(我们会逐步分析整个通信过程):
1) 过程一:worker启动后向ROUTER注册
worker启动,建立ZMQ_DEALER类型的socket,同时在启动时通过zmq_setsockopt显示地设置了socket标识ZMQ_IDENTITY,然后发送“READY”数据给router,此时router会接收到2帧的数据,这里要注意:为什么会是接收到两帧呢?你前面不是说的设置了socket标识之后就可以接收到3帧吗?通过我自己的实际代码判断,采用ZMQ_DEALER类型的socket设置标识符之后发送数据,在router端的确是接收到了2帧数据;而采用ZMQ_REQ类型的socket设置标识符之后发送数据在router端就可以接收到3帧数据,我猜测是因为socket类型不通导致的。2帧的数据包相比3帧的数据包少了中间那一个空帧,即接收到的READY的两帧的数据包如下:
router通过对第一帧解析就可以获取到worker的socket标识,然后将该socket标识保存在队列中,等有任务时就可以通过该socket给该worker分配任务了。
2) 过程二:worker与router之间发送心跳包
i. worker给router发送心跳包
worker给router发送心跳包和发送READY的注册包是一样的,在router端接收到的同样是2帧的数据,如上图,只是数据换成了“HEARTBEAT”(心跳)。
ii. router给worker发送心跳包
router主动向worker发送心跳包时,首先它会构造一个只包含“HEARTBEAT”数据的一个包,然后从工作者队列里面取出一个worker的标识符,然后将该标识符组合在上面数据包的头部,即如上图形成第一帧是某个worker的socket标识符号,第二针为"HEARTBEAT"数据的完整数据包,然后将该数据包发送出去。某worker接收到该数据包,会自动将其信封去掉(该过程我们观察不到),即接收到的就只有一帧,即数据“HEARTBEAT”。
3) 过程三:client向router发送任务,router将该任务专交给某个worker
同样,client启动时会建立ZMQ_REP类型的socket,然后通过zmq_setsockopt设置socket标识,然后给router发送数据。由于client是ZMQ_REQ类型的socket,因此在router端会接收到一个包含3帧的数据包(第一帧为client端的socket标识,第二帧为空帧,第三帧为数据帧),数据包格式如下:
router在接收到该数据包时会将该数据包转发给某个worker去处理,在发送给worker之前,它会从工作者队列中取出一个worker的socket标识符,然后将该标识符装配进数据包中,形成一个4帧的数据包(第一帧为某个worker的socket标识,第二帧为client端的socket标识,第三帧为空帧,第四帧为数据帧),其具体格式如下:
4) 过程四:worker处理某个具体的任务,然后返回给router,同时router将数据反馈给client
某个worker接收到来自router分配的任务,此时在worker接收到的是去除掉worker socket标识的三帧的数据包,从里面就可以取出来自客户端的数据,然后经过处理,通过保存的三帧数据重新拼成一个数据包发送给router,然后router接收到数据包后在转发给client即可。
以上就是整个通信模型的过程,其关键之处是理解多帧的组装。
实现偏执海盗模型中遇到的问题以及部分关键代码:
1. 在偏执海盗某型中由于会进行频繁的包的拆解与组装,因此它引入了GitHub中提供的一个ZMQ实现类,它能帮助我们很简单的完成包的组装和拆解。引入的两个文件分别为:zmq.hpp和zhelpers.hpp。
2. zmq.hpp中实现了消息封装的类,而在每次接收我们都要把它想象成一个类的实例,其实本质的操作还是通过多帧消息的操作,例如:zmq_msg_init,zmq_msg_data,zmq_msg_size等一些列的函数。而在每次发送前,它都会拼接成一个新的zmq_message_t发送出去。
关键代码,其实就是zmq.hpp的实现,里面实现了多帧的接收、封装、拆解等,自己感觉还不错,下面贴出几个关键函数:
1) recv接收
bool recv(zmq::socket_t & socket) { clear(); while(1) { zmq::message_t message(0); try { if (!socket.recv(&message, 0)) { return false; } } catch (zmq::error_t error) { std::cout << "E: " << error.what() << std::endl; return false; } ustring data = http://www.mamicode.com/(unsigned char*) message.data();>2) send发送
void send(zmq::socket_t & socket) { for (size_t part_nbr = 0; part_nbr < m_part_data.size(); part_nbr++) { zmq::message_t message; ustring data = http://www.mamicode.com/m_part_data[part_nbr];> 这两天的时间一直再弄这个模型,期间经历了很多波折,好在终于把它实现了出来。每天的生活就像一句名言:痛并快乐着!!