Skip to menu

服务器框架

作为一名linux下的c/c++后台服务器开发的工程师,最近才接触ZeroMQ,确实有点“相见恨晚”啊。不过“好饭不怕晚”,该好好的享受一下大餐了!

 

ROUTER/DEALER模式

常见的后台服务模型

作为一名C/C++后台服务器开发的工程师,一定很熟悉这样的服务框架:

一个IO线程负责监听和接受客户端的请求,接收完请求后,封装成一个任务(Task),然后丢入任务队列管理器(TaskManager)中,至于 怎么调度和管理这些任务(Task),任务管理器(TaskManager)就得采用一定的调度算法来管理和分派这些任务到线程池 (ThreadPool)中,然后由指定的工作线程(Worker)来完成这些任务。同时工作线程(Worker)和任务管理器 (TaskManager)还必须有一个通知机制,当Worker处理完Task的时候,能够高效的将结果给会到TaskManager,从而回复给请求 的客户端。

怎么样, 后台服务器开发还是很有技术含量的吧,但还是白菜价啊!自己手工实现上面的东东,怎么着都得捣鼓一阵子吧。无奈产品经理说,一阵子是多久,我要马上就好!工程师只能吓尿。。。

ZeroMQ搭建一个broker

但是,方法总比困难多。ZeroMQ来救场了。ROUTER/DEALER模式,就可以做一个broker,轻松解放生产力!

ROUTER其实指的是ZeroMQ里面的一种套接字类型( ZMQ_ROUTER类型)。这个套接字把接收到的请求(ZMQ_REQ)公平的排队分发,而且ROUTER还会标志客户的身份,从而确保能够将应答数据 给到客户端。这样就刚好可以解决了我们想要的“监听和接受客户端请求,并丢入任务管理器”。【这里的任务管理器指的是DEALER】

DEALER其实指的是ZeroMQ里面的一种套接字类型(ZMQ_DEALER)。这个套接字会把“任务”负载均衡的分发到后端的工作线程(或进 程)Worker的。并且,当DEALER接受到Worker的处理结果的时候,DEALER还会把处理结果数据传递给ROUTER,由ROUTER将应 答回给客户端。这样一来,DEALER也解决了我们想要的“任务管理调度和Worker分配工作的问题”。

 

ROUTER/DEALER的优点

没错,就是简单使用ZeroMQ提供的ROUTER/DEALER组合模式,可以轻松搭建一个高性能异步的C/C++后台服务框架。ROUTER可 以高效的接收客户端的请求,而DEALER可以负载均衡的调度后端Worker工作。当客户端的请求特别多,后端Worker处理不过来,需要增加 Worker的时候,也非常简单,新加入的Worker直接Connect到DEALER即可。如此运维起来也非常高效,后端可以非常简单的横向扩展!

 

友情提示

值的一提的是,ROUTER又叫做XREP,DEALER又叫做XREQ,刚开始学习的时候,看别人的代码,一头雾水。看了下zmq的api,才发现他们是一个东东。另外zmq的各个版本的api不兼容,这个得注意。我这次学习使用的是zeromq-4.0.4 版本。

/* Deprecated aliases */
#define ZMQ_XREQ ZMQ_DEALER
#define ZMQ_XREP ZMQ_ROUTER

 

搭建一个高性能的异步后台服务框架

框架图

router

 

 

ROUTER/DEALER模式搭建一个broker(Device)

这个Device很关键,起着承上启下的作用,没有它,那这个后台框架就不复存在了。虽然很关键,但是实现起来却非常的简单,简单的组合一下ZMQ_ROUTER和ZMQ_DEALER套接字即可。看一下代码吧:

【提示】这里暂且叫做deviceFrame.cpp吧。这里使用了zmq_poll来管理ZMQ_ROUTER和ZMQ_DEALER套接字的事 件,其实有更简单的api来帮助我们(后面再介绍)。不过zmq_poll的方式,对于我这个新手来说,类比epoll来理解,很合适。

代码:deviceFrame.cpp

 

 

编译

[leoox@Study demo]$ g++ -o deviceFrame -I/home/leoox/local/zeromq-4.0.4/include/ deviceFrame.cpp /home/leoox/local/zeromq-4.0.4/lib/libzmq.a -lpthread -lrt

/home/leoox/local/zeromq-4.0.4/lib/libzmq.a(libzmq_la-ipc_listener.o): In function zmq::ipc_listener_t::set_address(char const*)':
/home/leoox/soft/zeromq-4.0.4/src/ipc_listener.cpp:127: warning: the use of
tempnam’ is dangerous, better use mkstemp'

[leoox@Study demo]$

 

这样就得到了一个可执行程序deviceFrame,不过编译过程中,还是遇到了warning的,ZeroMQ用到了不安全的函数“tempnam”,无关大局,忽略之。

 

运行

将这个Device运行起来。这里叫它Device,一则是ZeroMQ里面有Device的说法,二则是确实像一个设备一样,高效的解放了劳动力。

从日志可以看到,我们的Device已经正常启动了,不过目前暂时没有请求进来,所以都是zmq_poll超时醒来(rc = 0,意味着没有任何事件到来。)

[leoox@Study demo]$ ./deviceFrame 127.0.0.1 9090 127.0.0.1 9091
Current 0MQ version is 4.0.4
===========================================

[1428333949] router bind tcp://127.0.0.1:9090 ok.
[1428333949] dealer bind tcp://127.0.0.1:9091 ok.
[1428333950] zmq_poll rc = 0
[1428333951] zmq_poll rc = 0
[1428333952] zmq_poll rc = 0

 

 

REP搭建Worker工作进程

为了方便演示和学习,这里采用另外一个进程的方式。而且这样的好处是,可以启动任意多个Worker,连接到上面的Device的DEALER中。

ZMQ_REP套接字,其实就是一个“应答”,即,把应答数据回复给ZMQ_REQ,他们是严格的一问一答的方式。不过组合上 ZMQ_ROUTER,ZMQ_DEALER模式后,后台Worker不再是服务的死穴,可以通过横向扩展多个Worker来提高处理ZMQ_REQ的能 力。(当然加上zmq_poll这种reactor多路复用机制性能更佳!)

 

代码:repWorker.cpp

 

 

编译:

[leoox@Study demo]$ g++ -o repWorker -I/home/leoox/local/zeromq-4.0.4/include/ repWorker.cpp /home/leoox/local/zeromq-4.0.4/lib/libzmq.a -lpthread -lrt
/home/leoox/local/zeromq-4.0.4/lib/libzmq.a(libzmq_la-ipc_listener.o): In function
zmq::ipc_listener_t::set_address(char const*)’:
/home/leoox/soft/zeromq-4.0.4/src/ipc_listener.cpp:127: warning: the use of tempnam' is dangerous, better use mkstemp’

 

这样,我们就得到了工作进程repWorker了。

 

运行:

既然我们手中有资源,那我们就多启动几个repWorker来处理客户端的请求吧。先启动两个吧,repWorker的工作编号为1000和2000。后面方便观察。

 

启动工作编号为1000的工作进程:

[leoox@Study demo]$ ./repWorker 127.0.0.1 9091 1000
Current 0MQ version is 4.0.4
===========================================

worker zmq_connect tcp://127.0.0.1:9091 done!

 

启动工作编号为2000的工作进程:

[leoox@Study demo]$ ./repWorker 127.0.0.1 9091 2000
Current 0MQ version is 4.0.4
===========================================

worker zmq_connect tcp://127.0.0.1:9091 done!

 

随着工人到位,整个后台服务框架就这么简单的搞点了。可以开门大吉了!

 

REQ发送请求到后端

诚如上面所说的,ZMQ_REQ是“问”的套接字,它需要“答”套接字(ZMQ_REP)。不过目前ZMQ_REP已经被 ROUTER/DEALER华丽的包装成高富帅了。ZMQ_REQ只能通过联系ROUTER,然后由这个ROUTER/DEALER组成的DEVICE帮 忙传递“爱意”到达ZMQ_REP了,然后再默默的期待ROUTER传递ZMQ_REQ的“答复”。

 

代码:reqClient.cpp

 

 

编译:

[leoox@Study demo]$ g++ -o reqClient -I/home/leoox/local/zeromq-4.0.4/include/ reqClient.cpp /home/leoox/local/zeromq-4.0.4/lib/libzmq.a -lpthread -lrt
/home/leoox/local/zeromq-4.0.4/lib/libzmq.a(libzmq_la-ipc_listener.o): In function zmq::ipc_listener_t::set_address(char const*)':
/home/leoox/soft/zeromq-4.0.4/src/ipc_listener.cpp:127: warning: the use of
tempnam’ is dangerous, better use `mkstemp’
[leoox@Study demo]$

 

这样我们就顺利的得到请求客户端reqClient了。可以激动的将请求发往我们的高性能的异步后台了。哈哈!

 

运行:

我们的高性能异步后台已经饥渴难耐的等待客户端的请求了,来见证奇迹的时刻吧!把请求发往端口为9090的ROUTER吧。。。。

为了方便观察,我们把这个客户端的编号设置为1,看看效果!

 

[leoox@Study demo]$ ./reqClient 127.0.0.1 9090 1
Current 0MQ version is 4.0.4
===========================================

client zmq_connect tcp://127.0.0.1:9090 done!
[1428335643] send request(index=1&cmd=hello&time=1428335643) to server, rec = 33, request.len = 33
[1428335644] recv reply(worker=1000&result=world&time=1428335644) from server, rec = 40, reply.len = 40
[1428335644] ————————–

[1428335647] send request(index=1&cmd=hello&time=1428335647) to server, rec = 33, request.len = 33
[1428335647] recv reply(worker=2000&result=world&time=1428335647) from server, rec = 40, reply.len = 40
[1428335647] ————————–

[1428335650] send request(index=1&cmd=hello&time=1428335650) to server, rec = 33, request.len = 33
[1428335650] recv reply(worker=1000&result=world&time=1428335650) from server, rec = 40, reply.len = 40
[1428335650] ————————–

[1428335653] send request(index=1&cmd=hello&time=1428335653) to server, rec = 33, request.len = 33
[1428335653] recv reply(worker=2000&result=world&time=1428335653) from server, rec = 40, reply.len = 40
[1428335653] ————————–

 

哈哈,就是这么简单,我们的客户端正常的发请求到后端,并且得到了应答!

 

Device的运行情况:

截取一段日志,可见Device调度得很顺畅!

【思考】为什么router会传递3帧消息到DEALER呢?明明在客户端代码里面ZMQ_REQ只send了一帧数据呀!

[1428335906] zmq_poll rc = 0
[1428335907] zmq_poll rc = 0
[1428335908] zmq_poll rc = 0
[1428335908] zmq_poll rc = 1
[1428335908] zmq_poll catch one router event!
[1428335908] router deliver request to dealer. rc = 5, more = 1
[1428335908] router deliver request to dealer. rc = 0, more = 1
[1428335908] router deliver request to dealer. rc = 33, more = 0
[1428335908]———-DoRouter———-

[1428335908] zmq_poll rc = 1
[1428335908] zmq_poll catch one dealer event!
[1428335908] dealer deliver reply to router. rc = 5, more = 1
[1428335908] dealer deliver reply to router. rc = 0, more = 1
[1428335908] dealer deliver reply to router. rc = 40, more = 0
[1428335908]———-DoDealer———-

 

repWorker的工作情况:

截取一段日志,可见repWorker正在热火朝天的工作中,只需要专注的做自己的工作就可以了,其他的如何应答客户端,完全不用担心!

[1428336028] recv request(index=1&cmd=hello&time=14283360288336022) from client, rec = 33, request.len = 40
[1428336028] send reply(worker=1000&result=world&time=1428336028) to client, rec = 40, reply.len = 40
[1428336028]————————

[1428336034] recv request(index=1&cmd=hello&time=14283360348336028) from client, rec = 33, request.len = 40
[1428336034] send reply(worker=1000&result=world&time=1428336034) to client, rec = 40, reply.len = 40
[1428336034]————————

 

升级版的Device

在上面的示例中(deviceFrame.cpp),我们使用zmq_poll来自己实现了一个Device。虽然代码也很简洁明了,但其实你发现 我们在处理zmq_poll的就绪事件的时候,代码是大同小异的(其实就一个样)。所以这部分代码,zmq已经用一个API帮我们代劳了,进一步释放劳动 力!

这个API就是:

ZMQ_EXPORT int zmq_proxy (void *frontend, void *backend, void *capture);

来看一下升级版的Device的代码,为了区别开来,这里叫做deviceProxy.cpp

 

代码:deviceProxy.cpp

 

 

编译:

g++ -o deviceProxy -I/home/leoox/local/zeromq-4.0.4/include/ deviceProxy.cpp /home/leoox/local/zeromq-4.0.4/lib/libzmq.a -lpthread -lrt

 

运行和使用方式,与deviceFrame一模一样,这里就不累赘了。

 

总结

第一次接触ZeroMQ,这么简单的代码就写出了一个高性能的异步后台服务框架,不得不说相当给力。不过作为一个队技术有追求的工程师,还是要探索一下ZeroMQ的ROUTER/DEALER模式的。

1、客户端(ZMQ_REQ)发送请求到ROUTER后,ROUTER是会对客户端进行身份表示的,正式因为有这个身份标示,所以ROUTER才有能力正确的把应答数据准确的传递到来源的客户端。

现在可以回答一下上文的一个思考题了—-ROUTER传递的3帧数据到底是什么数据:

A、第一帧是ROUTER自己加上的消息,这个是ROUTER对ZMQ_REQ所做的一个身份标识。说到身份标识,这里就引入到两种套接字。

一种叫做临时套接字,另外一种叫做永久套接字,他们的区别仅仅是是否使用ZMQ_IDENTITY。

没使用的即默认为临时套接字,我的这个文章里面的例子就是一个临时套接字。对于临时套接字,ROUTER会为它生成一个唯一的UUID,所以可以看到第一帧的长度为5,正是这个UUID。

而使用如下方式设定的套接字,则称为永久套接字。如果这样设置,那第一帧收到的消息长度就是13,而ROUTER也会直接使用www.leoox.com这个身份来标识这个客户端。

zmq_setsockopt(req, ZMQ_IDENTITY, “www.leoox.com”, 13);

 

B、第二帧是一个空帧,这是由REQ加上去的。可以理解为一个分隔符,当ROUTER遇到这个空帧后,就知道下一帧就是真正的请求数据了,这在多种组合模型里面尤其有用。

 

C、第三帧显然就是真正的请求数据了。这里的例子比较简单,复杂的例子,客户端可能会通过ZMQ_SNDMORE来发送多帧数据。如果是这样,那ROUTER还会继续收到第四帧,第五帧。。。数据的。

 

2、REQ到达ROUTER,ROUTER会公平排队,并公平的将REQ发往后端。但当后端能力不足的时候,导致ROUTER派发太慢的时候,ROUTER进入高水位的时候,ROUTER会丢弃掉消息的。所以这个得注意监控后台服务的性能。

 

3、DEALER会负载均衡的将任务派发后连接到它的各个工作Worker。至于这个负载均衡的算法,目前猜测应该是简单的轮询方式吧。知道的网友,欢迎指导!

 

 

from:

向上