ZMQ的模式之Pipeline
管道模式:
这种模式描述的场景是数据被散布到以管道方式组织的各个节点上。管道的每一步都连接一个或多个节点,连接多个节点时数据以RR方式往下流。注意是流,意味着数据跟发布模式一样是单向的。这个模式对应的socket是ZMQ_PUSH和ZMQ_PULL
官方例子:
[外链图片转存失败(img-MmJues20-1565859719447)(/home/yzz/Documents/pipeline.png)]
- ventilator:一台呼吸机,可以产生可以并行完成的任务
- workers:一组工人处理任务
- sink:接收器从工作进程收集结果
代码:
ventilator:
//
// Task ventilator in C++
// Binds PUSH socket to tcp://localhost:5557
// Sends batch of tasks to workers via that socket
//
// Olivier Chamoux <olivier.chamoux@fr.thalesgroup.com>
//
#include <zmq.hpp>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#define within(num) (int) ((float) num * random () / (RAND_MAX + 1.0))
int main (int argc, char *argv[])
{
zmq::context_t context (1);
// Socket to send messages on
zmq::socket_t sender(context, ZMQ_PUSH);
sender.bind("tcp://*:5557");
std::cout << "Press Enter when the workers are ready: " << std::endl;
getchar ();
std::cout << "Sending tasks to workers...\n" << std::endl;
// The first message is "0" and signals start of batch
zmq::socket_t sink(context, ZMQ_PUSH);
sink.connect("tcp://localhost:5558");
zmq::message_t message(2);
memcpy(message.data(), "0", 1);
sink.send(message);
// Initialize random number generator
srandom ((unsigned) time (NULL));
// Send 100 tasks
int task_nbr;
int total_msec = 0; // Total expected cost in msecs
for (task_nbr = 0; task_nbr < 100; task_nbr++) {
int workload;
// Random workload from 1 to 100msecs
workload = within (100) + 1;
total_msec += workload;
message.rebuild(10);
memset(message.data(), '\0', 10);
sprintf ((char *) message.data(), "%d", workload);
sender.send(message);
}
std::cout << "Total expected cost: " << total_msec << " msec" << std::endl;
sleep (1); // Give 0MQ time to deliver
return 0;
}
workers:
//
// Task worker in C++
// Connects PULL socket to tcp://localhost:5557
// Collects workloads from ventilator via that socket
// Connects PUSH socket to tcp://localhost:5558
// Sends results to sink via that socket
//
// Olivier Chamoux <olivier.chamoux@fr.thalesgroup.com>
//
#include "zhelpers.hpp"
#include <string>
int main (int argc, char *argv[])
{
zmq::context_t context(1);
// Socket to receive messages on
zmq::socket_t receiver(context, ZMQ_PULL);
receiver.connect("tcp://localhost:5557");
// Socket to send messages to
zmq::socket_t sender(context, ZMQ_PUSH);
sender.connect("tcp://localhost:5558");
// Process tasks forever
while (1) {
zmq::message_t message;
int workload; // Workload in msecs
receiver.recv(&message);
std::string smessage(static_cast<char*>(message.data()), message.size());
std::istringstream iss(smessage);
iss >> workload;
// Do the work
s_sleep(workload);
// Send results to sink
message.rebuild();
sender.send(message);
// Simple progress indicator for the viewer
std::cout << "." << std::flush;
}
return 0;
}
sink:
//
// Task sink in C++
// Binds PULL socket to tcp://localhost:5558
// Collects results from workers via that socket
//
// Olivier Chamoux <olivier.chamoux@fr.thalesgroup.com>
//
#include <zmq.hpp>
#include <time.h>
#include <sys/time.h>
#include <iostream>
int main (int argc, char *argv[])
{
// Prepare our context and socket
zmq::context_t context(1);
zmq::socket_t receiver(context,ZMQ_PULL);
receiver.bind("tcp://*:5558");
// Wait for start of batch
zmq::message_t message;
receiver.recv(&message);
// Start our clock now
struct timeval tstart;
gettimeofday (&tstart, NULL);
// Process 100 confirmations
int task_nbr;
int total_msec = 0; // Total calculated cost in msecs
for (task_nbr = 0; task_nbr < 100; task_nbr++) {
receiver.recv(&message);
if (task_nbr % 10 == 0)
std::cout << ":" << std::flush;
else
std::cout << "." << std::flush;
}
// Calculate and report duration of batch
struct timeval tend, tdiff;
gettimeofday (&tend, NULL);
if (tend.tv_usec < tstart.tv_usec) {
tdiff.tv_sec = tend.tv_sec - tstart.tv_sec - 1;
tdiff.tv_usec = 1000000 + tend.tv_usec - tstart.tv_usec;
}
else {
tdiff.tv_sec = tend.tv_sec - tstart.tv_sec;
tdiff.tv_usec = tend.tv_usec - tstart.tv_usec;
}
total_msec = tdiff.tv_sec * 1000 + tdiff.tv_usec / 1000;
std::cout << "\nTotal elapsed time: " << total_msec << " msec\n" << std::endl;
return 0;
}
代码分析:
- 工人上游连接到呼吸机,下游连接到水槽。这意味着您可以任意添加工作人员。如果工作人员绑定到他们的端点,则需要(a)更多端点和(b)每次添加工作人员时修改呼吸机和/或接收器。我们说呼吸机和水槽是我们建筑的稳定部分,工人是它的动态部分。
- 我们必须将批处理的开始与所有正在运行的工作程序同步。这是ZeroMQ中相当常见的问题,并没有简单的解决方案。 zmq_connect方法需要一定的时间。因此,当一组工作人员连接到呼吸机时,第一个成功连接的工作人员将在短时间内收到大量消息,而其他人也在连接。如果不以某种方式同步批处理的开始,系统将根本不会并行运行。
- 呼吸机的PUSH插座将任务分配给工人(假设它们在批量开始外出之前全部连接)均匀分配。
- 水槽的PULL插座均匀地收集工人的结果。这称为公平排队。
管道模式容易出现的问题:
管道模式也表现出“慢连接”综合症,导致指责PUSH插座不能正确地进行负载平衡。如果你正在使用PUSH和PULL,并且你的一个工作人员得到的消息多于其他人,那是因为PULL套接字加入的速度比其他人快,并且在其他人设法连接之前抓取了很多消息。如果您需要适当的负载平衡,您可能希望查看高级请求 - 应答模式中的负载平衡模式。
更多问题请参见ZMQ官方网站:http://zguide.zeromq.org/page:all#Programming-with-ZeroMQ