文章目录
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
地址:PUB
和SUB
端的 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 设计思路:
- 使用多个
topic
,两种情况下压测:多个 topic 共用一个 IPC
:所有topic
的消息通过同一个IPC
通道传输。每个 topic 使用独立 IPC
:每个topic
都使用独立的IPC
地址传输。
- 消息生成:
PUB
端按固定频率发送不同topic
的消息(可以用线程或异步方式)。
- 消息接收:
SUB
端订阅特定topic
,并统计接收到的消息数以及接收的延迟。
- 性能测量:
- 测量总消息吞吐量(单位时间内接收的消息数)。
- 测量消息的延迟(从发送到接收到的时间差)。
- 使用 CPU、内存监控工具(如
htop
)手动观察系统的资源消耗。
4.2 代码结构:
PubSubBenchmark
类
- 负责管理
PUB
和SUB
的 socket 连接。 - 提供不同的测试模式(共用一个
IPC
和使用独立IPC
)。
ZmqPublisher
和 ZmqSubscriber
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 测试要点:
- 吞吐量测试:通过统计
SUB
端接收的每个topic
的消息总数,计算单位时间内的消息吞吐量。可根据message_count_
进行统计。 - 延迟测试:使用
std::chrono
记录消息的发送和接收时间,并计算每条消息的往返延迟。可以在PUB
端发送时间戳,SUB
端接收到后计算差值。 - 资源消耗观察:使用外部工具(如
htop
或perf
)观察 CPU 和内存的消耗情况,比较共用IPC
和独立IPC
的资源占用差异。
5. 建议:
- 如果系统中的
topic
数量较多、消息量不大,建议采用多个topic
共用一个IPC
,这样可以减少资源消耗,管理上也更加简单。 - 如果每个
topic
的消息量较大,并且不同topic
之间有明显的隔离需求,建议采用每个topic
使用独立IPC
,这样可以获得更好的性能和隔离性。