ZeroMQ:14---基础篇之(高水位标记)

  • 所有的套接字类型都可以使用标识。如果你在使用PUB和SUB套接字,其中SUB套接字为自己声明了标识,那么,当SUB断开连接时,PUB会保留要发送给SUB的消息。
  • 这种机制有好有坏。好的地方在于发布者会暂存这些消息,当订阅者重连后进行发送;不好的地方在于这样很容易让发布者因内存溢出而崩溃。
  • 如果你在使用持久化的SUB套接字(即为SUB设置了套接字标识),那么你必须设法避免消息在发布者队列中堆砌并溢出,应该使用阈值(HWM)来保护发布者套接字。发布者的阈值会分别影响所有的订阅者。
  • 我们可以运行一个示例来证明这一点,用第一章中的wuclient和wuserver具体,在wuclient中进行套接字连接前加入这一行:
zmq_setsockopt (subscriber, ZMQ_IDENTITY, "Hello", 5);
  • 编译并运行这两段程序,一切看起来都很平常。但是观察一下发布者的内存占用情况,可以看到当订阅者逐个退出后,发布者的内存占用会逐渐上升。若此时你重启订阅者,会发现发布者的内存占用不再增长了,一旦订阅者停止,就又会增长。很快地,它就会耗尽系统资源
  • 我们先来看看如何设置阈值,然后再看如何设置得正确。下面的发布者和订阅者使用了上文提到的“节点协调”机制。发布者会每隔一秒发送一条消息,这时你可以中断订阅者,重新启动它,看看会发生什么。
  • 以下是发布者的代码:
//
//  发布者 - 连接持久化的订阅者
//
#include "zhelpers.h"
 
int main (void) 
{
    void *context = zmq_init (1);
 
    //  订阅者会发送已就绪的消息
    void *sync = zmq_socket (context, ZMQ_PULL);
    zmq_bind (sync, "tcp://*:5564");
 
    //  使用该套接字发布消息
    void *publisher = zmq_socket (context, ZMQ_PUB);
    zmq_bind (publisher, "tcp://*:5565");
 
    //  等待同步消息
    char *string = s_recv (sync);
    free (string);
 
    //  广播10条消息,一秒一条
    int update_nbr;
    for (update_nbr = 0; update_nbr < 10; update_nbr++) {
        char string [20];
        sprintf (string, "Update %d", update_nbr);
        s_send (publisher, string);
        sleep (1);
    }
    s_send (publisher, "END");
 
    zmq_close (sync);
    zmq_close (publisher);
    zmq_term (context);
    return 0;
}
  • 下面是订阅者的代码:
//
//  持久化的订阅者
//
#include "zhelpers.h"
 
int main (void)
{
    void *context = zmq_init (1);
 
    //  连接SUB套接字
    void *subscriber = zmq_socket (context, ZMQ_SUB);
    zmq_setsockopt (subscriber, ZMQ_IDENTITY, "Hello", 5);
    zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "", 0);
    zmq_connect (subscriber, "tcp://localhost:5565");
 
    //  发送同步消息
    void *sync = zmq_socket (context, ZMQ_PUSH);
    zmq_connect (sync, "tcp://localhost:5564");
    s_send (sync, "");
 
    //  获取更新,并按指令退出
    while (1) {
        char *string = s_recv (subscriber);
        printf ("%s\n", string);
        if (strcmp (string, "END") == 0) {
            free (string);
            break;
        }
        free (string);
    }
    zmq_close (sync);
    zmq_close (subscriber);
    zmq_term (context);
    return 0;
}
  • 运行以上代码,在不同的窗口中先后打开发布者和订阅者。当订阅者获取了一至两条消息后按Ctrl-C中止,然后重新启动,看看执行结果:
$ durasub
Update 0
Update 1
Update 2
^C
$ durasub
Update 3
Update 4
Update 5
Update 6
Update 7
^C
$ durasub
Update 8
Update 9
END
  • 可以看到订阅者的唯一区别是为套接字设置了标识,发布者就会将消息缓存起来,待重建连接后发送。设置套接字标识可以让瞬时套接字转变为持久套接字。实践中,你需要小心地给套接字起名字,可以从配置文件中获取,或者生成一个UUID并保存起来。
  • 当我们为PUB套接字设置了阈值,发布者就会缓存指定数量的消息,转而丢弃溢出的消息。让我们将阈值设置为2,看看会发生什么:
uint64_t hwm = 2;
zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm));
  • 运行程序,中断订阅者后等待一段时间再重启,可以看到结果如下:
$ durasub
Update 0
Update 1
^C
$ durasub
Update 2
Update 3
Update 7
Update 8
Update 9
END
  • 看仔细了,发布者只为我们保存了两条消息(2和3)。阈值使得ZMQ丢弃溢出队列的消息。
  • 简而言之,如果你要使用持久化的订阅者,就必须在发布者端设置阈值,否则可能造成服务器因内存溢出而崩溃。但是,还有另一种方法。ZMQ提供了名为交换区(swap)的机制,它是一个磁盘文件,用于存放从队列中溢出的消息。启动它很简单:
// 指定交换区大小,单位:字节。
uint64_t swap = 25000000;
zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap));
  • 我们可以将上面的方法综合起来,编写一个既能接受持久化套接字,又不至于内存溢出的发布者:
//
//  发布者 - 连接持久化订阅者
//
#include "zhelpers.h"
 
int main (void) 
{
    void *context = zmq_init (1);
 
    //  订阅者会告知我们它已就绪
    void *sync = zmq_socket (context, ZMQ_PULL);
    zmq_bind (sync, "tcp://*:5564");
 
    //  使用该套接字发送消息
    void *publisher = zmq_socket (context, ZMQ_PUB);
 
    //  避免慢持久化订阅者消息溢出的问题
    uint64_t hwm = 1;
    zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm));
 
    //  设置交换区大小,供所有订阅者使用
    uint64_t swap = 25000000;
    zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap));
    zmq_bind (publisher, "tcp://*:5565");
 
    //  等待同步消息
    char *string = s_recv (sync);
    free (string);
 
    //  发布10条消息,一秒一条
    int update_nbr;
    for (update_nbr = 0; update_nbr < 10; update_nbr++) {
        char string [20];
        sprintf (string, "Update %d", update_nbr);
        s_send (publisher, string);
        sleep (1);
    }
    s_send (publisher, "END");
 
    zmq_close (sync);
    zmq_close (publisher);
    zmq_term (context);
    return 0;
}
  • 若在现实环境中将阈值设置为1,致使所有待发送的消息都保存到磁盘上,会大大降低处理速度。这里有一些典型的方法用以处理不同的订阅者:
    • 必须为PUB套接字设置阈值,具体数字可以通过最大订阅者数、可供队列使用的最大内存区域、以及消息的平均大小来衡量。举例来说,你预计会有5000个订阅者,有1G的内存可供使用,消息大小在200个字节左右,那么,一个合理的阈值是1,000,000,000 / 200 / 5,000 = 1,000。
    • 如果你不希望慢速或崩溃的订阅者丢失消息,可以设置一个交换区,在高峰期的时候存放这些消息。交换区的大小可以根据订阅者数、高峰消息比率、消息平均大小、暂存时间等来衡量。比如,你预计有5000个订阅者,消息大小为200个字节左右,每秒会有10万条消息。这样,你每秒就需要100MB的磁盘空间来存放消息。加总起来,你会需要6GB的磁盘空间,而且必须足够的快(这超出了本指南的讲解范围)。
  • 关于持久化订阅者:
    • 数据可能会丢失,这要看消息发布的频率、网络缓存大小、通信协议等。持久化的订阅者比起瞬时套接字要可靠一些,但也并不是完美的。
    • 交换区文件是无法恢复的,所以当发布者或代理消亡时,交换区中的数据仍然会丢失。
  • 关于阈值:
    • 这个选项会同时影响套接字的发送和接收队列。当然,PUB、PUSH不会有接收队列,SUB、PULL、REQ、REP不会有发送队列。而像DEALER、ROUTER、PAIR套接字时,他们既有发送队列,又有接收队列。
    • 当套接字达到阈值时,ZMQ会发生阻塞,或直接丢弃消息。
    • 使用inproc协议时,发送者和接受者共享同一个队列缓存,所以说,真正的阈值是两个套接字阈值之和。如果一方套接字没有设置阈值,那么它就不会有缓存方面的限制。

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/106882111