ZeroMQ 消息传递:多 Topic 共用与独立 IPC 的对比分析

0. 概要

在 ZeroMQ PUB/SUB 模型中,多个 topic 是共用一个 IPC 地址,还是为每个 topic 定义独立的 IPC 地址,是一个设计和性能的权衡问题。
下面我将从架构设计性能影响使用场景等方面进行深入分析,并给出这两种方法的优劣对比。

1. 架构设计

1.1 多个 topic共用一个IPC` 地址

在这种方法中,PUB 端通过一个 IPC 地址发送所有的消息,SUB 端根据订阅的 topic 对消息进行过滤。例如,每个消息以 topic 的字符串前缀开头,SUB 端根据这个前缀来决定是否处理该消息。

优点:
  • 管理简便PUB 端只需要维护一个 IPC 地址,所有消息通过相同的 IPC 传输,减少了管理多个 IPC 地址的复杂性。
  • 扩展性好:当需要添加新的 topic 时,只需在 PUB 端和 SUB 端修改相应的过滤规则,而不需要修改传输通道。
  • 网络连接数量少:因为所有消息都使用同一个 IPC 地址,SUB 端只需要维护一个连接,降低了连接管理的开销。
缺点:
  • 消息流共享:多个 topic 的消息会在同一个 IPC 通道上传输,所有订阅者都会接收所有的消息,然后在本地进行过滤。这会带来一些额外的网络开销,尤其是当消息量较大时,非目标 SUB 端也会接收到不相关的消息。
  • 潜在的瓶颈:如果一个 topic 产生大量消息,可能会占用大量带宽,导致其他 topic 的消息传输受到影响。

1.2 每个 topic 使用独立的 IPC 地址

在这种方法中,每个 topic 都有一个独立的 IPC 地址,PUB 端为每个 topic 绑定不同的 IPC 地址,SUB 端连接到各自需要的 IPC 地址。

优点:
  • 消息隔离:每个 topic 的消息完全隔离在不同的 IPC 通道上,SUB 端只会接收到它关心的 topic 消息,这避免了不必要的消息过滤。
  • 性能稳定:因为消息流是独立的,一个 topic 产生的大量消息不会影响其他 topic 的消息传输,提升了整体系统的性能和稳定性。
  • 更高的并发性:不同 IPC 地址可以独立处理不同的连接,更适合高并发的场景。
缺点:
  • 管理复杂:每个 topic 都需要一个独立的 IPC 地址,增加了配置和管理的复杂性,尤其是当 topic 数量较多时。
  • 资源消耗:每个 IPC 地址都需要独立的 socket 连接,会增加连接的数量和资源消耗,尤其是当 topic 数量较多时,连接和上下文的管理会变得更加复杂。

2. 性能对比理论分析

为了更直观地理解这两种方法的性能差异,可以从带宽利用率延迟资源消耗三个维度进行对比:

2.1 带宽利用率

  • 多个 topic 共用一个 IPC 地址:所有订阅者都会接收到所有 topic 的消息,哪怕他们只对某个特定 topic 感兴趣。这意味着会有额外的带宽开销,因为无关的消息也会被传输到 SUB 端,增加了不必要的网络负载。
  • 每个 topic 使用独立的 IPC 地址:每个 SUB 端只接收它所订阅的 topic 消息,带宽利用率更高,减少了不必要的数据传输。

2.2 延迟

  • 多个 topic 共用一个 IPC 地址:由于所有消息共享同一个通道,延迟可能会受到高流量的影响,特别是在某个 topic 消息量很大的情况下,其他 topic 的消息可能会因为队列积压而产生延迟。
  • 每个 topic 使用独立的 IPC 地址:消息流是独立的,各个 topic 互不干扰,因此延迟更加稳定,适合对实时性要求较高的场景。

2.3 资源消耗

  • 多个 topic 共用一个 IPC 地址PUBSUB 端的 socket 数量较少,资源消耗相对较低,尤其是在 topic 数量较多时,这种方法在资源消耗上有一定优势。
  • 每个 topic 使用独立的 IPC 地址:每个 topic 都需要独立的 socket 和连接,会增加系统的资源消耗,特别是当 topic 数量较多时,系统的文件描述符、内存和 CPU 负载会有所增加。

3. 使用场景对比分析:

3.1 多个 topic 共用一个 IPC 地址:

  • 适用场景
    • topic 数量较多且消息量不大。
    • 不同的 topic 之间没有明显的隔离需求。
    • 对系统资源要求比较敏感(减少连接数量)。
    • 适合小型项目或轻量级的消息传递场景。

3.2 每个 topic 使用独立的 IPC 地址:

  • 适用场景
    • topic 数量较少,且每个 topic 的消息量较大。
    • 不同 topic 的消息有明显的隔离需求(比如不同服务模块之间的消息隔离)。
    • 需要较高的并发性和更好的实时性。
    • 适合中大型项目或高吞吐量、低延迟的消息传递场景。

4. 性能测试

为了设计一个针对 ZeroMQ PUB/SUB 模式的性能压测代码,我们可以考虑对两种情况进行比较:多个 topic 共用一个 IPC 地址每个 topic 使用独立的 IPC 地址。通过测量两种方式下的消息吞吐量延迟以及资源消耗,我们可以获得对比数据。

4.1 设计思路:

  1. 使用多个 topic,两种情况下压测
    • 多个 topic 共用一个 IPC:所有 topic 的消息通过同一个 IPC 通道传输。
    • 每个 topic 使用独立 IPC:每个 topic 都使用独立的 IPC 地址传输。
  2. 消息生成
    • PUB 端按固定频率发送不同 topic 的消息(可以用线程或异步方式)。
  3. 消息接收
    • SUB 端订阅特定 topic,并统计接收到的消息数以及接收的延迟。
  4. 性能测量
    • 测量总消息吞吐量(单位时间内接收的消息数)。
    • 测量消息的延迟(从发送到接收到的时间差)。
    • 使用 CPU、内存监控工具(如 htop)手动观察系统的资源消耗。

4.2 代码结构:

PubSubBenchmark
  • 负责管理 PUBSUB 的 socket 连接。
  • 提供不同的测试模式(共用一个 IPC 和使用独立 IPC)。
ZmqPublisherZmqSubscriber
  • ZmqPublisher:负责发送 topic 消息。
  • ZmqSubscriber:负责接收并统计每个 topic 的消息。

4.3 实现代码:

#include <atomic>
#include <chrono>
#include <cstring>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <zmq.hpp>

#define DEBUG 0  // 将此值设为0以禁用调试信息

#if DEBUG
#define DBG_PRINT(msg) std::cout << "[DEBUG] " << msg << std::endl
#else
#define DBG_PRINT(msg) \
  do {                 \
  } while (0)
#endif

class PubSubBenchmark {
 public:
  PubSubBenchmark(size_t num_topics, bool use_separate_ipc)
      : num_topics_(num_topics),
        use_separate_ipc_(use_separate_ipc),
        context_(1),
        message_count_(num_topics),
        running_(true) {
    for (size_t i = 0; i < num_topics_; ++i) {
      message_count_[i].store(0);
    }
  }

  void Run() {
    auto start_time = std::chrono::high_resolution_clock::now();

    std::thread pub_thread(&PubSubBenchmark::ZmqPublisher, this);

    std::vector<std::thread> sub_threads;
    for (size_t i = 0; i < num_topics_; ++i) {
      sub_threads.emplace_back(&PubSubBenchmark::ZmqSubscriber, this, i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(10));
    Stop();

    pub_thread.join();
    for (auto& thread : sub_threads) {
      thread.join();
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end_time - start_time;

    std::cout << "Benchmark completed in " << elapsed.count() << " seconds.\n";

    for (size_t i = 0; i < num_topics_; ++i) {
      std::cout << "Topic " << i << " received " << message_count_[i].load() << " messages." << std::endl;
    }
  }

  void Stop() {
    running_.store(false);
    DBG_PRINT("Stop() called, setting running_ to false.");
  }

 private:
  zmq::context_t context_;
  size_t num_topics_;
  bool use_separate_ipc_;
  std::vector<std::atomic<size_t>> message_count_;
  std::atomic<bool> running_;

 private:
  void ZmqPublisher() {
    if (use_separate_ipc_) {
      std::vector<zmq::socket_t> publishers;
      for (size_t i = 0; i < num_topics_; ++i) {
        zmq::socket_t pub(context_, zmq::socket_type::pub);
        std::string ipc_address = "ipc:///tmp/topic" + std::to_string(i) + "_ipc";
        pub.bind(ipc_address);
        DBG_PRINT("Publisher bound to " << ipc_address);
        publishers.emplace_back(std::move(pub));
      }

      std::this_thread::sleep_for(std::chrono::seconds(1));  // 增加等待时间,确保订阅者连接

      while (running_.load()) {
        for (size_t i = 0; i < num_topics_; ++i) {
          if (!running_.load()) {
            DBG_PRINT("Publisher stopping...");
            break;
          }
          std::string message = "Message from topic " + std::to_string(i);
          zmq::message_t msg(message.c_str(), message.size());

          // 使用异步发送
          if (publishers[i].send(msg, zmq::send_flags::dontwait)) {
            DBG_PRINT("Published message to topic " << i << ": " << message);
          } else {
            DBG_PRINT("Failed to publish message to topic " << i << ": " << message);
          }

          std::this_thread::sleep_for(std::chrono::milliseconds(5));  // 增加延迟
        }
      }

      DBG_PRINT("Publisher exiting...");
    } else {
      zmq::socket_t publisher(context_, zmq::socket_type::pub);
      std::string ipc_address = "ipc:///tmp/my_zmq_ipc";
      publisher.bind(ipc_address);
      DBG_PRINT("Publisher bound to " << ipc_address);

      std::this_thread::sleep_for(std::chrono::seconds(1));  // 增加等待时间,确保订阅者连接

      while (running_.load()) {
        for (size_t i = 0; i < num_topics_; ++i) {
          std::string topic = "topic" + std::to_string(i);
          std::string message = "Message from " + topic;
          zmq::message_t topic_msg(topic.c_str(), topic.size());
          zmq::message_t msg(message.c_str(), message.size());

          // 使用异步发送
          if (publisher.send(topic_msg, zmq::send_flags::sndmore) && publisher.send(msg, zmq::send_flags::none)) {
            DBG_PRINT("Published message to topic " << i << ": " << message);
          } else {
            DBG_PRINT("Failed to publish message to topic " << i << ": " << message);
          }

          std::this_thread::sleep_for(std::chrono::milliseconds(5));
        }
      }

      DBG_PRINT("Publisher exiting...");
    }
  }

  void ZmqSubscriber(size_t topic_index) {
    zmq::socket_t subscriber(context_, zmq::socket_type::sub);
    std::string ipc_address;

    if (use_separate_ipc_) {
      ipc_address = "ipc:///tmp/topic" + std::to_string(topic_index) + "_ipc";
    } else {
      ipc_address = "ipc:///tmp/my_zmq_ipc";
    }

    subscriber.connect(ipc_address);
    DBG_PRINT("Subscriber connected to " << ipc_address);


    int timeout = 5;  // milliseconds
    subscriber.setsockopt(ZMQ_RCVTIMEO, &timeout, sizeof(timeout));
    if (!use_separate_ipc_) {
      std::string topic = "topic" + std::to_string(topic_index);
      subscriber.setsockopt(ZMQ_SUBSCRIBE, topic.c_str(), topic.size());

      DBG_PRINT("Subscriber subscribed to " << topic);
    } else {
      subscriber.setsockopt(ZMQ_SUBSCRIBE, "", 0);
    }

    while (running_.load()) {
      zmq::message_t topic_msg;
      zmq::message_t msg;

      // 使用非阻塞方式接收消息
      zmq::recv_result_t result = subscriber.recv(topic_msg, zmq::recv_flags::dontwait);

      if (result) {
        std::string received_topic(static_cast<char*>(topic_msg.data()), topic_msg.size());
        DBG_PRINT("Received message for topic " << topic_index << ": " << received_topic);
        if (!use_separate_ipc_) {
          subscriber.recv(msg, zmq::recv_flags::none);
          if (received_topic == ("topic" + std::to_string(topic_index))) {
            message_count_[topic_index]++;
            DBG_PRINT("Subscriber " << topic_index << " received message for topic.");
          } else {
            DBG_PRINT("Unexpected topic: " << received_topic);
          }
        } else {
          message_count_[topic_index]++;
          DBG_PRINT("Subscriber " << topic_index << " received message.");
        }
      } else {
        // DBG_PRINT("No message received by subscriber " << topic_index);
      }
    }

    DBG_PRINT("Subscriber " << topic_index << " exiting...");
  }
};

int main(int argc, char* argv[]) {
  size_t num_topics = 5;
  bool use_separate_ipc = true;

  if (argc >= 2) {
    num_topics = std::stoi(argv[1]);
  }
  if (argc >= 3) {
    std::string ipc_mode = argv[2];
    if (ipc_mode == "separate") {
      use_separate_ipc = true;
    } else if (ipc_mode == "shared") {
      use_separate_ipc = false;
    }
  }

  std::cout << "Starting benchmark with " << num_topics << " topics using "
            << (use_separate_ipc? "separate IPC" : "shared IPC") << "." << std::endl;

  PubSubBenchmark benchmark(num_topics, use_separate_ipc);
  benchmark.Run();

  return 0;
}


  • 编译指令:
    g++ -std=c++14 -o zmq_ipc_benchmark zmq_ipc_benchmark.cpp -lzmq -pthread -O2
  • 执行结果
$ ./zmq_ipc_benchmark 10 separate                                                                                                                                                                                                                                
Starting benchmark with 10 topics using separate IPC.
Benchmark completed in 10.0023 seconds.
Topic 0 received 178 messages.
Topic 1 received 177 messages.
Topic 2 received 177 messages.
Topic 3 received 177 messages.
Topic 4 received 177 messages.
Topic 5 received 177 messages.
Topic 6 received 177 messages.
Topic 7 received 177 messages.
Topic 8 received 177 messages.
Topic 9 received 177 messages.

请添加图片描述··

$ ./zmq_ipc_benchmark 10 shared                                                                                                                                                                                                                                  
Starting benchmark with 10 topics using shared IPC.
Benchmark completed in 10.0223 seconds.
Topic 0 received 178 messages.
Topic 1 received 178 messages.
Topic 2 received 178 messages.
Topic 3 received 178 messages.
Topic 4 received 178 messages.
Topic 5 received 178 messages.
Topic 6 received 177 messages.
Topic 7 received 177 messages.
Topic 8 received 177 messages.
Topic 9 received 177 messages.

在这里插入图片描述
可以看到cpu消耗没有想象的那么大。

4.4 测试要点:

  1. 吞吐量测试:通过统计 SUB 端接收的每个 topic 的消息总数,计算单位时间内的消息吞吐量。可根据 message_count_ 进行统计。
  2. 延迟测试:使用 std::chrono 记录消息的发送和接收时间,并计算每条消息的往返延迟。可以在 PUB 端发送时间戳,SUB 端接收到后计算差值。
  3. 资源消耗观察:使用外部工具(如 htopperf)观察 CPU 和内存的消耗情况,比较共用 IPC 和独立 IPC 的资源占用差异。

5. 建议:

  • 如果系统中的 topic 数量较多、消息量不大,建议采用多个 topic 共用一个 IPC,这样可以减少资源消耗,管理上也更加简单。
  • 如果每个 topic 的消息量较大,并且不同 topic 之间有明显的隔离需求,建议采用每个 topic 使用独立 IPC,这样可以获得更好的性能和隔离性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值