消息队列和ZeroMQ原理和应用

一、定义

消息队列(message queue)本质就是个队列,先进先出FIFO。
利用FIFO先进先出的特性,可以保证消息的顺序性。

主要用途:不同服务server、进程process、线程thread之间通信。


二、消息队列应用特性

2.1 消息可靠性处理
在实际应用中,我们可以通过以下要点的设计来确保消息的可靠性处理:

  1. ACK:消息收发确认ack,如果没有ack就超时,重传。
  2. 序列号:定义序列号,消息序号是连续的,中间发现消息不连续的时候就认为消息丢失(生产者消费模式不适用)。
  3. 备份:对数据进行备份
  4. 持久化:RocketMQ


2.2消息吞吐量
模式比较:
1)REQ-ACK模式(无法提高吞吐量)

  • 优点:低延时,确保消息可达
  • 缺点:无法提高吞吐量

2)多次send,一次ack模式(可以提高吞吐量)


2.3 消息队列—流量控制
流量控制也称为削峰。这也在消息队列设计的时候需要考虑的要素。
削峰,水位到一定的阶段,就不处理高出的数据,消息队列不接收数据,直接原路打回去,减轻后端的负载,比如12306现在响应繁忙,请稍后。
如果经常发生削峰机制,要考虑扩展后端的处理能力。


2.4 消息队列解耦
使用消息队列实现系统的解耦。下图为消息队列系统解耦示例。
在这里插入图片描述


2.5 消息持久化
消息的持久化,对于一些关键的核心业务来说是非常重要的,启动消息持久化后,消息队列宕机重启后,消息可以从持久化存储恢复,消息不丢失,可以继续消费处理。
在这里插入图片描述
批量持久化,批量应答。如果进行批量消息传送,吞吐量可以上去,但是时效性会差一些。ZeroMQ不支持持久化。


三、ZeroMQ

3.1 ZeroMQ可以解决以下传统网络问题

  • 调用的socket接口较多
  • TCP是一对一的连接
  • 编程需要关注很多socket细节问题
  • 不支持跨平台编程
  • 需要自行处理分包、组包问题
  • 流式传输时需处理粘包、半包问题
  • 需自行处理网络异常,比如连接异常中断、重连等
  • 服务端和客户端启动有先后
  • 自行处理IO模型
  • 自行实现消息的缓存
  • 自行实现对消息的加密


3.2 ZeroMQ模型

1)REQ/REP请求响应模型
客户端和服务端的启动先后顺序没有严格的要求,先启动哪个都可以。
在这里插入图片描述
服务端编码:

void *context = zmq_ctx_new ();
void *responder = zmq_socket (context, ZMQ_REP); //ZMQ_REP
int rc = zmq_bind (responder, "tcp://*:5555");  // 服务器要做绑定

char buffer [10];
int size = zmq_recv (responder, buffer, 10, 0);
zmq_send (responder, "World", 5, 0);

客户端编码:

void *context = zmq_ctx_new ();
void * requester = zmq_socket (context, ZMQ_REQ); //ZMQ_REQ
zmq_connect (requester, "tcp://localhost:5555");

zmq_send (requester, "Hello", 5, 0);
zmq_recv (requester, buffer, 5, 0);        // 收到响应才能再发

zmq_close (requester);
zmq_ctx_destroy (context);

函数说明:

  1. zmq_ctx_new ():创建一个新的ZMQ环境上下文。环境上下文是线程安全的,可以按需要尽可能多的被应用线程共享使用,而不需要添加线程锁。

  2. void *zmq_socket (void *context, int type):根据上下文创建一个 ZMQ套接字。type参数指明了要创建的socket的类型,这个类型决定了在进行传输时在socket上执行的语义。

    与传统套接字不同的关键:

  • 传统的套接字提出了一种同步接口(无论是面向连接的可靠字节流(SOCK_STREAM),还是无连接的不可靠数据报(SOCK_DGRAM)); ZMQ套接字提出了一种异步消息队列的抽象机制,基于socket类型的精确的队列语义。

  • 传统的套接字传输字节流或者离散的数据报,ZMQ套接字传输离散的消息。

  • ZMWQ套接字使用了异步机制,意思是说按照时间对实体的连接和断开、重连和有效的传输对用户来说是透明的,是由ZMQ自己进行控制的。进一步,消息可能被添加到无法到达的目的端上。

  • 传统的套接字值允许严格的一对一(两个对端)、多对一(多个客户端,一个服务端),或者有些时候的一对多(多播)。在ZMQ中,出了ZMQ_PAIR类型的套接字,ZMQ允许使用zmq_connect()函数链接到多个终结点,而同时还可以在使用zmq_bind()函数绑定的终结点上接收多个终结点发来的消息,这就允许多对多的传输关系。

    ZMQ不是线程安全的。应用进程不能在多个线程间使用同一个套接字,除非已经将一个套接字的内存空间从一个线程完全的迁移到另一个线程中。
    请求-响应模式用来从ZMQ_REQ客户端向一个或多个ZMQ_REP服务端发送请求,并且接收随后到来的对每一个发送端的回复。

    ZMQ_REQ
    ZMQ_REQ类型的套接字用来从一个客户端向一个服务端发送请求,并从这个服务端获取回复。这类套接字值允许一种交替的执行顺序:zmq_send(请求)和随后到来的zmq_recv(回复)调用。每一个发送出去的请求都会在所有的服务端循环,并且每一个收到的消息都会与自后发出的相匹配。
    当一个ZMQ_REQ套接字由于对所有的服务端都达到高水位而进入静默状态,或者如果没有可用的服务端,之后在设个套接字上的所有的zmq_send(3)操作都会进入阻塞状态,直到静默状态结束或者至少有一个可以的服务端变得可用;消息在此期间不会丢失
    在这里插入图片描述
    ZMQ_REP
    ZMQ_REP类型的套接字被服务端用来接收户端的请求并进行回复。这个套接字类型只允许按顺序交替进行zmq_recv(请求)和随后的zmq_send(回复)调用
    当一个ZMQ_REP套接字由于接受了太多而到达高水位并进入了静默状态,所有被发送的回复,如果对端的客户端出现了问题,将会被释放掉,直到静默状态结束。
    在这里插入图片描述

  1. int zmq_send (void *socket, void *buf, size_t len, int flags); 函数会根据buf参数指定的内存缓冲区和len参数指定的缓冲区数据长度创建一个消息,并将消息添加到消息队列中。flags参数是由下面的标志组合成的。

    ZMQ_DONTWAIT
    对于当socket不可使用就要执行阻塞方式的socket类型来说(DEALER,PUSH),该选项可以指定这个操作以非阻塞模式执行。如果无法添加消息到socket的消息队列上,zmq_send()函数将会执行失败并设置errno为EAGAIN。
    ZMQ_SNDMORE
    指出当前正在发送的消息是多帧消息,并且接下来还会发送更多的消息。

    成功的调用zmq_send()函数并不能说明消息已经被成功的发送到了网络上,只能说明消息被添加到了这个socket的消息队列上,并且ZMQ开始对其负责。

    一个ZMQ消息有一个或多个ZMQ消息帧组成。ZMQ保证自动交付消息:一个ZMQ的消息要么所有的帧消息都被接收,要么一个都不接收。一个消息中消息帧的总数没有限制,除非内存不够用。

    函数返回值:执行成功会返回发送的消息的字节数,否则返回-1。

    errno值
    EAGAIN—函数在使用非阻塞模式发送消息,并且当前socket不可用。
    ENOTSUP—socket的类型不支持zmq_send函数的操作。
    EFSM—zmq_send函数当前无法对这个socket进行操作,因为这个socket处在与此操作不适当的状态。这个错误可能出现在那些在几种状态之间进行切换的socket上,比如ZMQ_REP。请参照zmq_socket(3)函数部分的消息模式部分以获取更多信息。
    ETERM–与给定的socket向关联的context被终结了。
    ENOTSOCK—参数提供的socket不可用。
    EINTR—此次操作被系统信号中断了。
    EHOSTUNREACH—此消息不能被路由。

    //发送多帧消息example
    /* Send a multi-part message consisting of three parts to socket */
    rc = zmq_send (socket, "ABC", 3, ZMQ_SNDMORE);
    assert (rc == 3);
    rc = zmq_send (socket, "DEFGH", 5, ZMQ_SNDMORE);
    assert (rc == 5);
    /* Final part; no more parts to follow */
    rc = zmq_send (socket, "JK", 2, 0); assert (rc == 2);
    
  2. int zmq_recv (void *socket, void *buf, size_t len, int flags); zmq_recv()函数会从socket参数指定的socket上接收一个消息,并把这个消息存储在buf参数指定的内存空间中。超过len参数指定长度的任何数据都会被删去。如果在socket上没有消息可接收,zmq_recv()函数会进行阻塞,直到请求被满足为止。flag参数是由下面的选项组合而成的标志。

    ZMQ_DONTWAIT
    指明此函数在非阻塞模式下执行。如果在指定的socket中没有消息,zmq_recv()函数会执行失败,并返回值为EAGAIN的errno。

    一个ZMQ消息由1个或多个ZMQ消息帧组成。ZMQ保证自动交付消息:一个ZMQ的消息要么所有的消息帧都被接收,要么一个都不会接收。一个消息中消息帧的总数没有限制,除非内存不够用。

    一个应用进程想要接收多帧的ZMQ消息,必须在zmq_recv()函数执行完成后使用zmq_getsockopt(3)函数的ZMQ_RCVMORE标志选项进行检查,以确认是否还有更多的消息帧等待接收。

    函数返回值:执行成功,将会返回接收到的消息的字节数。注意这个值可能会超过len参数指定的值,以免消息被截断。如果执行失败,这个函数将会返回 -1。

    errno值
    EAGAIN–使用非阻塞方式接收消息的时候没有接收到任何消息。
    ENOTSUP–socket参数指定的socket类型无法使用zmq_recv()进行操作。
    EFSM–zmq_recv()函数当前无法对这个socket进行操作,因为这个socket处在与此操作不适当的状态。这个错误可能出现在那些在几种状态之间进行切换的socket上,比如ZMQ_REP。请参照zmq_socket(3)函数部分的消息模式部分以获取更多信息。
    ETERM–与制定的socket相关联的context被终结了。

    ENOTSOCK–制定的socket不可用。
    EINTR–在接接收到消息之前,这个操作被系统信号中断了。

  3. int zmq_close (void *socket); 关闭ZMQ socket。任何从网络接收了但没有被应用程序使用zmq_recv()接收的消息都会被丢弃。已经使用zmq_send()发送的消息,但是还没有被设备发送到网络中的消息的处理方式,将由socket的ZQM_LINGER值决定。
    默认的ZMQ_LINGER值通常是不会丢弃没有发送的消息;这样的话,当调用zmq_term()函数时可能会导致应用程序阻塞。更多细节请参见zmq_setsockopt(3) 和zmq_term(3)。

    函数返回值:执行成功,zmq_close()函数会返回0。其它情况则返回-1。

    errno值
    ENOTSOCK—提供的socket无法使用。

  4. int zmq_ctx_destroy (void *context); 销毁ZMQ环境上下文。

    环境上下文终结步骤:
    1) 当前进程,所有基于context创建的socket若在执行阻塞操作,则会立即返回并返回错误代码ETERM。除了zmq_close()函数之外,所有创建在context上的更深层次的操作都会失败,并返回错误代码ETERM。
    2) 在中断所有的阻塞调用后,zmq_ctx_destory()将阻塞,直到满足以下情况:所有基于context创建的socket都已经被zmq_close()关闭。
    3) 对于基于context创建的每一个socket,所有被应用程序调用zmq_send() 发送的消息都已经被真实的发送到了网络上,或者用ZMQ_LINGER参数设置的socket的执行周期已经期满。


2)发布订阅模型
在这里插入图片描述
服务端源码如下:

void *context = zmq_ctx_new ();
void *publisher = zmq_socket (context, ZMQ_PUB); //ZMQ_PUB
int rc = zmq_bind (publisher, "tcp://*:5556");

while(1){
    
    
char update [20];
sprintf (update, "%05d %d %d", zipcode, temperature, relhumidity);
s_send (publisher, update);  //封装接口,内部调用的是zmq_send函数
}

客户端源码如下:

void *context = zmq_ctx_new ();
void *subscriber = zmq_socket (context, ZMQ_SUB); //ZMQ_SUB
    int rc = zmq_connect (subscriber, "tcp://localhost:5556");

const char *filter = (argc > 1)? argv [1]: "10001 ";
rc = zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE,
                         filter, strlen (filter));  // 字符匹配的方式
char *string = s_recv (subscriber);  //封装接口,内部调用的是zmq_recv函数

函数说明:

  1. **int zmq_setsockopt (void socket, int option_name, const void option_value, size_t option_len); 设置ZMQ socket的属性。
    设置的属性由option_name参数指定,属性值由参数option_value指定。option_len参数指定属性值的数据存储空间的大小。

    ZMQ_SNDHWM:对向外发送的消息设置高水位(最大缓存量)。它会限制每一个与此socket相连的在内存中排队的未处理的消息数目的最大值。0值代表着没有限制。如果已经到达了规定的限制,socket就需要进入一种异常的状态,表现形式因socket类型而异。socket会进行适当的调节,比如阻塞或者丢弃已发送的消息。(ZMQ并不能保证可以接收像ZMQ_SNDHWM参数一样多的消息,而实际的限制可能在60~70%以下,这取决于这个socket上的消息流。)
    ZMQ_AFFINITY:设置I/O线程关联。关联指明了和scoket相关的context上的哪个线程池中的哪个线程来处理新创建的链接。0值表示没有关联,这意味着工作将在线程池中平等的向所有进程进行分发。对于非0值,最低位对应线程1,第二低位对应线程2,等等。

    ZMQ_SUBSCRIBE创建消息过滤标志。可以使用这个属性来建立最初的消息过滤项。一个option_value的长度是0的过滤属性会订阅所有的广播消息。一个非空的option_value值会只订阅所有以option_value的值为前缀的消息。一个ZMQ_SUB类型的socket可以附加多个过滤条件,只要一个消息符合过滤条件中的任何一个就会被接受
    ZMQ_UNSUBSCRIBE移除消息过滤选项。需要被移除的过滤选项必须是已经使用ZMQ_SUBSCRIBE选项建立了的。如果添加了几个相同的过滤项,ZMQ_UNSUBSCRIBE选项只会移除其中的一个,其它的可以继续使用。

    ZMQ_IDENTITY设置socket 身份ID。socket的身份ID只会能在请求/回复模式中使用。可以使用ROUTER类型的socket进行socket串联,根据给定的身份ID可以对消息进行路由。身份ID至少有1 B的长度,最多有255 B的长度。以0位开始的身份ID由ZMQ保留使用。

    ZMQ_RATE设置广播数据的频率

    ZMQ_RECOVERY_IVL设置多路广播恢复时间。恢复时间决定了,当一个接收端从一个广播组退出后再连接上来之前,在数据丢失,导致数据不可恢复前的最大时间间隔,时间以毫秒为单位。(注意:当设置较大的恢复时间时,这会非常耗内存。比如,传输速率是1Gbps时,1分钟长的回复时间,会导致7GB的内存消耗。)

    ZMQ_SNDBUF设置内核传输缓冲区大小。会对socekt参数指定的socket设置底层内核的传输缓存大小,以B为单位进行设置。设置的属性值是0,则意味着使用OS的默认值。

    ZMQ_RCVBUF设置内核传输缓冲区大小。会对socekt参数指定的socket设置底层内核的传输缓存大小,以B为单位进行设置。设置的属性值是0,则意味着使用OS的默认值。

    ZMQ_LINGER为socket关闭设置停留时间。停留时间指定了在对一个socket调用zmq_close(3)函数之后,这个socekt上即将被发送但还没有被发送到对端的消息在内存中继续停留的时间。这个属性进而还会影响到与这个scoket相关联的context上zmq_term(3)操作的进行终结的结果。
    属性值 -1表示无限的停留时间:还没被发送的消息在socket调用zmq_close()操作之后不会被丢弃。
    属性值0 表示没有停留时间:当使用zmq_close()函数将socket关闭的时候,所有没有被发送呃消息都会被丢弃。

    ZMQ_RECONNECT_IVL:设置重连间隔。当与对端的链接断开后,尝试再次进行连接时的ZMQ周期。属性值是 -1表示不进行重连。

    ZMQ_BACKLOG:设置向外发起的连接队列的最大长度。
    ZMQ_MAXMSGSIZE:可接受的最大进入消息的大小。
    ZMQ_MULTICAST_HOPS:多播数据包时候的最大网络中继
    ZMQ_RCVTIMEO:在一个recv操作返回EAGAIN错误前的最大时间
    ZMQ_SNDTIMEO:在一个发送操作返回EAGAIN之前等待的最大时间
    ZMQ_IPV6:在socket上允许使用IPv6
    ZMQ_IPV4ONLY:在socket上只使用IPv4
    ZMQ_IMMEDIATE:队列消息只作用于已完成的链接
    ZMQ_ROUTER_HANDOVER:处理ROUTER socket上的每一个身份ID名冲突
    ZMQ_ROUTER_MANDATORY:在ROUTER socket上只接收可以进行路由的消息
    ZMQ_ROUTER_RAW:转换ROUTER socket到原始模式
    ZMQ_PROBE_ROUTER:ROUTER socket 的引导连接
    ZMQ_XPUB_VERBOSE:提供XPUB socket上的所有订阅消息
    ZMQ_REQ_CORRELATE:匹配请求回复
    ZMQ_REQ_RELAXED:放松请求和回复之间严格的轮换
    ZMQ_TCP_KEEPALIVE:代替SO_KEEPALIVE属性
    ZMQ_TCP_KEEPALIVE_IDLE:代替TCP_KEEPCNT(有些操作系统上是TCP_KEEPALIVE)
    ZMQ_TCP_KEEPALIVE_CNT:代替TCP_KEEPCNT socket属性
    ZMQ_TCP_KEEPALIVE_INTVL:代替TCP_KEEPINTVL socket 属性
    ZMQ_TCP_ACCEPT_FILTER:为新连上来的TCP连接分配过滤器
    ZMQ_PLAIN_SERVER:设置PLAIN 服务的角色
    ZMQ_PLAIN_USERNAME:设置PLAIN安全方式的用户名
    ZMQ_PLAIN_PASSWORD:设置PLAIN安全方式的密码
    ZMQ_CURVE_SERVER:设置CURVE服务端角色
    ZMQ_CURVE_PUBLICKEY:设置CURVE公钥
    ZMQ_CURVE_SECRETKEY:设置CURVE密钥
    ZMQ_CURVE_SERVERKEY:设置CURVE服务端码
    ZMQ_ZAP_DOMAIN:设置RFC 27认证域
    ZMQ_CONFLATE:只保留最后到来的消息

    函数返回值:执行成功,zmq_setsockopt()函数会返回0。否则会返回 -1。

    error值:
    EINVAL–请求的属性中,属性名称为止,或者属性长度或者属性值不可用。
    ETERM–和指定socket相关的context已经被终结了
    ENOTSOCK–参数提供的socket不可用
    EINTR–本次操作被信号中断了

    注意:除了以下属性,其他属性均需要在对socket进行bind/connect操作之前设置:
    ZMQ_SUBSCRIBE, ZMQ_UNSUBSCRIBE, ZMQ_LINGER, ZMQ_ROUTER_HANDOVER,
    ZMQ_ROUTER_MANDATORY,ZMQ_PROBE_ROUTER,ZMQ_XPUB_VERBOSE, ZMQ_REQ_CORRELATE, ZMQ_REQ_RELAXED
    特别的,安全的属性也可以在bind/connect操作之后生效,并且可以随时进行修改并影响之后的bind/connect操作。

3)PUSH/PULL推拉模型
在这里插入图片描述
Ventilator—taskvent主要源码如下:

void *context = zmq_ctx_new ();

void *sender = zmq_socket (context, ZMQ_PUSH); 	//push给worker
zmq_bind (sender, "tcp://*:5557");

void *sink = zmq_socket (context, ZMQ_PUSH); 	//push给sink
zmq_connect (sink, "tcp://localhost:5558");

char string [10]=123;
s_send (sink, "0");
s_send (sender, string);

zmq_close (sink);
zmq_close (sender);
zmq_ctx_destroy (context);

Worker—taskworker主要源码如下:

void *context = zmq_ctx_new ();
void *receiver = zmq_socket (context, ZMQ_PULL);	 //从taskvent拉数据
zmq_connect (receiver, "tcp://localhost:5557");

void *sender = zmq_socket (context, ZMQ_PUSH);	 //push数据给sink
zmq_connect (sender, "tcp://localhost:5558");

char *string = s_recv (receiver);
free (string);
s_send (sender, "");

zmq_close (receiver);
zmq_close (sender);
zmq_ctx_destroy (context);

Sink—tasksink主要源码如下:

void *context = zmq_ctx_new ();
void *receiver = zmq_socket (context, ZMQ_PULL);  //拉数据
zmq_bind (receiver, "tcp://*:5558");

char *string = s_recv (receiver);  //数据从taskvent push过来,表示已经启动
free (string);

char *string = s_recv (receiver); //数据从worker过来
free (string);

zmq_close (receiver);
zmq_ctx_destroy (context);

4)Router/Dealer模型
在这里插入图片描述

REQ—rrclient主要源码如下:

void *context = zmq_ctx_new ();

//  Socket to talk to server
void *requester = zmq_socket (context, ZMQ_REQ);
zmq_connect (requester, "tcp://localhost:5559");

int request_nbr;
for (request_nbr = 0; request_nbr != 10; request_nbr++) {
    
    
    s_send (requester, "Hello");
    char *string = s_recv (requester);
    printf ("Received reply %d [%s]\n", request_nbr, string);
    free (string);
}
zmq_close (requester);
zmq_ctx_destroy (context);

ROUTER—rrbroker主要源码如下:

void *context = zmq_ctx_new ();
void *frontend = zmq_socket (context, ZMQ_ROUTER);
void *backend  = zmq_socket (context, ZMQ_DEALER);
zmq_bind (frontend, "tcp://*:5559");
zmq_bind (backend,  "tcp://*:5560");

//  Initialize poll set
zmq_pollitem_t items [] = {
    
    
    {
    
     frontend, 0, ZMQ_POLLIN, 0 },
    {
    
     backend,  0, ZMQ_POLLIN, 0 }
};
//  Switch messages between sockets
while (1) {
    
    
    zmq_msg_t message;
    zmq_poll (items, 2, -1);
    if (items [0].revents & ZMQ_POLLIN) {
    
    
        while (1) {
    
    
            //  Process all parts of the message
            zmq_msg_init (&message);
            zmq_msg_recv (&message, frontend, 0);
            int more = zmq_msg_more (&message);
            zmq_msg_send (&message, backend, more? ZMQ_SNDMORE: 0);
            zmq_msg_close (&message);
            if (!more)
                break;      //  Last message part
        }
    }
    if (items [1].revents & ZMQ_POLLIN) {
    
    
        while (1) {
    
    
            //  Process all parts of the message
            zmq_msg_init (&message);
            zmq_msg_recv (&message, backend, 0);
            int more = zmq_msg_more (&message);
            zmq_msg_send (&message, frontend, more? ZMQ_SNDMORE: 0);
            zmq_msg_close (&message);
            if (!more)
                break;      //  Last message part
        }
    }
}
//  We never get here, but clean up anyhow
zmq_close (frontend);
zmq_close (backend);
zmq_ctx_destroy (context);

REP—rrworker主要源码如下:

void *context = zmq_ctx_new ();
void *responder = zmq_socket (context, ZMQ_REP);
zmq_connect (responder, "tcp://localhost:5560");
while (1) {
    
    
        //  Wait for next request from client
        char *string = s_recv (responder);
        free (string);

        //  Do some 'work'
        sleep (1);

        //  Send reply back to client
        s_send (responder, "World");
    }
 
    zmq_close (responder);
    zmq_ctx_destroy (context);

猜你喜欢

转载自blog.csdn.net/locahuang/article/details/111037619