一、项目背景
本项目是一个基于 C++ 实现的轻量级消息队列(Message Queue)系统,仿照 RabbitMQ 核心架构设计。项目实现三大核心能力:消息发布与路由 (DIRECT / FANOUT / TOPIC 三种交换机模式)、消息持久化与消费 (支持自动确认 / 手动确认)、多信道连接复用(单 TCP 连接承载多信道)。底层基于 Muduo 高性能网络库,使用 Protobuf 作为序列化协议,元数据存储使用 SQLite,消息体以自定义二进制文件格式持久化。项目为学习型框架,核心价值在于从零理解消息队列的交换机路由、绑定机制、持久化策略和消费者组调度。
二、项目简介
本项目包含以下核心功能模块:
-
消息发布模块:客户端通过 TCP 长连接与服务端通信,通过信道(Channel)声明交换机、队列和绑定关系,发布消息时由 Router 根据交换机类型(DIRECT/FANOUT/TOPIC)和 routing_key 路由到目标队列。
-
消息消费模块:支持消费者订阅队列,服务端通过 ConsumerManager 管理消费者组,采用轮询(Round-Robin)策略向消费者推送消息。支持自动确认(auto_ack)和手动确认(basicAck)两种模式。
-
虚拟主机模块:VirtualHost 作为资源隔离容器,管理所属的交换机、队列、绑定关系和消息数据,一个服务端可承载多个虚拟主机。
-
持久化层 :元数据(交换机、队列、绑定)存储在 SQLite 数据库中;消息体存储在自定义二进制文件中(格式:
| 消息长度(8B) | 消息体 |),支持有效消息标记和垃圾回收。 -
协议层:基于 Protobuf 定义请求/响应协议(mq_proto.proto、mq_msg.proto),使用 Muduo 的 ProtobufCodec 进行编解码和消息分发。
三、测试计划
3.1 测试策略
采用白盒集成测试 + 黑盒性能压测 双轨策略。白盒部分基于项目已有单元测试(gtest)验证各模块逻辑正确性;黑盒部分通过自研压测客户端 benchmark_client 对整体链路进行性能评估,覆盖不同并发连接数下的吞吐量、延迟和稳定性。
3.2 测试环境
| 组件 | 说明 |
|---|---|
| 服务端 | Ubuntu 24.04 云服务器,C++ (g++ 13.3, C++17) |
| 压测客户端 | 同机部署,基于项目客户端 SDK 自研 |
| 通信方式 | localhost TCP,Protobuf 协议 |
| 服务端口 | 8085 |
| 测试模式 | FANOUT 交换机,非持久化(纯内存路径) |
| 编译模式 | Debug (-g -o0) |
3.3 提测方式
分两层递进测试:单元测试 → 性能压测。先执行已有 gtest 单元测试确认各模块逻辑正确,再通过自研压测工具进行递增并发压测,最后结合代码审查识别架构层面问题。
四、测试工具
| 工具 | 用途 |
|---|---|
| Google Test (gtest) | 服务端模块单元测试(10 个测试用例) |
| benchmark_client (自研 C++) | 多连接并发 QPS 压测 |
| Makefile + g++ | 编译构建与依赖管理 |
| std::chrono + std::atomic | 压测计时与计数 |
| 代码审查 (Code Review) | 架构合理性分析、Bug 发现、瓶颈识别 |
五、测试类型与覆盖情况
| 测试类型 | 覆盖范围 | 发现问题数量 | 备注 |
|---|---|---|---|
| 单元测试 | Exchange / Queue / Binding / Message / Host / Route / Consumer / Channel / Connection / File 十个模块 | 2(编译级) | protobuf 版本不兼容导致 2 个用例编译失败 |
| 功能测试 | 编译构建、服务启动、TCP 建连、信道创建、消息发布与响应 | 0 | 核心链路功能正常 |
| 并发测试 | 1/2/5/10 连接并发压测 | 1 | 10 连接触发客户端 segfault |
| 性能测试 | 4 档递增连接数(1/2/5/10) | 0 | 10 连接档位崩溃未完成 |
| 代码审查 | 全量源码(mqserver / mqclient / mqcommon) | 3 | GC 逻辑错误、线程池默认值、返回值遗漏 |
六、单元测试
6.1 测试编译情况
| 用例 ID | 用例名称 | 测试模块 | 编译结果 | 说明 |
|---|---|---|---|---|
| TC-U-001 | mq_filetest | 文件操作 | PASS | 编译通过 |
| TC-U-002 | mq_exchangetest | 交换机管理 | FAIL | std::unordered_map 无法隐式转为 protobuf::Map |
| TC-U-003 | mq_queuetest | 队列管理 | PASS | 编译通过 |
| TC-U-004 | mq_bindingtest | 绑定管理 | PASS | 编译通过(有 warning) |
| TC-U-005 | mq_msgtest | 消息管理 | PASS | 编译通过 |
| TC-U-006 | mq_hosttest | 虚拟主机 | FAIL | 同 TC-U-002,protobuf API 不兼容 |
| TC-U-007 | mq_routetest | 路由算法 | PASS | 编译通过(有 warning) |
| TC-U-008 | mq_consumertest | 消费者管理 | PASS | 编译通过(有 warning) |
| TC-U-009 | mq_channeltest | 信道管理 | PASS | 编译通过 |
| TC-U-010 | mq_connectiontest | 连接管理 | PASS | 编译通过 |
测试结果:8/10 编译通过,2 例因系统 protobuf 版本(3.21)与项目使用版本(3.20)API 差异导致编译失败,非代码逻辑错误。
七、性能测试
7.1 测试方案
采用递增连接数压力模型,从 1 连接逐步提升至 10 连接,固定每连接发送 10,000 条消息。所有连接在同一时刻并发启动,每个连接独立完成 TCP 建连 → 打开信道 → 声明 FANOUT 交换机 → 声明队列 → 绑定 → 循环发布消息 → 关闭信道流程。通过 std::atomic<uint64_t> 全局计数器汇总成功消息数,std::chrono::steady_clock 计算端到端耗时。
压测客户端启动方式:
./benchmark_client [ip] [port] [连接数] [总消息数]
每个连接内部流程:
benchmark_client
└─ worker (per thread, per connection)
├─ AsyncWorker 创建(含 EventLoopThread + threadpool)
├─ Connection 创建(TcpClient 连接服务端)
├─ Channel::openChannel() ← 打开信道
├─ declareExchange(FANOUT) ← 声明交换机
├─ declareQueue() ← 声明队列
└─ for i in 0..N:
└─ basicPublish(exchange, body) ← 同步发布(发送 + waitResponse)
7.2 性能测试结果汇总
递增连接数压力测试:
| 档位 | 连接数 | 总消息数 | 耗时 (s) | 吞吐量 (msg/s) | 平均延迟 (ms) | 错误数 | 结果 |
|---|---|---|---|---|---|---|---|
| 基线 | 1 | 1,000 | 0.199 | 5,025 | 0.20 | 0 | PASS |
| A档 | 2 | 10,000 | 2.049 | 4,880 | 0.20 | 0 | PASS |
| B档 | 5 | 50,000 | 3.779 | 13,231 | 0.08 | 0 | PASS |
| C档 | 10 | 100,000 | --- | --- | --- | --- | CRASH |
7.3 关键发现
-
吞吐量不随连接数线性增长:1 连接 5,025 QPS,2 连接仅 4,880 QPS(甚至微降),证明瓶颈在服务端而非客户端。5 连接时达 13,231 QPS(2.6x),说明多连接能部分并行,但远非线性扩展。
-
单 EventLoop 瓶颈明显 :服务端
mq_broker.hpp中使用单一_baseloop.loop()处理所有连接的 I/O 和业务逻辑,每条约 100μs 的处理时间决定了吞吐量上限 ≈ 10,000 msg/s。 -
Debug 编译的影响 :当前为
-g -O0Debug 编译,Release 编译(-O2)预计 QPS 可达 2~3 倍提升。 -
C 档崩溃定位:客户端在创建第 8 个 TcpClient 时触发 segfault,堆栈指向 Muduo Connector 初始化阶段,疑为 Muduo 静态库在多线程并发创建 TcpClient 时的内部竞争条件。
八、项目测试 Bug 简述
8.1 Bug 优先级统计
本次测试共发现 3 个 Bug,其中 P0 级(逻辑错误)1 个,P1 级(性能隐患)1 个,P3 级(代码规范)1 个。
| 优先级 | 数量 | 说明 |
|---|---|---|
| P0(逻辑错误) | 1 | GC 条件判断反向 |
| P1(性能隐患) | 1 | 线程池默认 1 线程 |
| P3(代码规范) | 1 | declareExchange 忽略返回值 |
8.2 Bug 详情列表
BUG-001 (P0):消息文件 GC 触发条件逻辑反转
| 字段 | 内容 |
|---|---|
| 位置 | mqserver/mq_message.hpp:324 QueueMessage::GCCheck() |
| 复现条件 | 持久化消息数量 > 2000 时触发 GC 判断 |
| 现象 | 有效消息 > 50% 时触发 GC(不必要的文件重写);有效消息 < 50% 时反而不触发 GC(文件空洞永不清理) |
| 根因 | 条件 _valid_count * 10 / _total_count > 5 应改为 < 5。_valid_count 为有效消息数,_total_count 为总消息数(含已删除空洞),GC 应在空洞较多时触发 |
| 修复方案 | 将判断条件改为 _valid_count * 10 / _total_count < 5 |
BUG-002 (P1):线程池默认仅为 1 线程
| 字段 | 内容 |
|---|---|
| 位置 | mqcommon/mq_threadpool.hpp:17 + mqserver/mq_broker.hpp:36 |
| 现象 | threadpool 构造函数默认参数 threadnum = 1,Server 实例化时未传参,导致消息消费推送完全串行化,多核 CPU 无法利用 |
| 修复方案 | Server 构造时传入 std::thread::hardware_concurrency() 或合理默认值(如 4) |
BUG-003 (P3):declareExchange 返回值被忽略
| 字段 | 内容 |
|---|---|
| 位置 | mqserver/mq_channel.hpp:62-66 |
| 现象 | declareExchange() 调用 _host->declareExchange() 获取返回值 ret,但始终 basicResponse(true, ...),忽略了实际返回值 |
| 修复方案 | 改为 basicResponse(ret, req->rid(), req->cid()) |
九、遗留问题
以下为代码审查中识别的架构层面待改进项,非本次测试发现的阻塞性缺陷:
| 编号 | 问题 | 优先级 | 说明 |
|---|---|---|---|
| 1 | 单 EventLoop 架构 | P1 | 所有连接的 I/O 和业务逻辑在同一线程串行处理,无法利用多核。Muduo 支持 setThreadNum(N) 开启多 Reactor,本项目未使用 |
| 2 | 同步文件 I/O 阻塞 IO 线程 | P1 | 持久化消息的 FileHelper::write() 在 EventLoop 线程中同步执行,阻塞所有其他连接。应改为异步刷盘或独立 I/O 线程 |
| 3 | 无消息流控 / 背压机制 | P1 | _msgs 为无界 std::list,生产者速度超过消费者时无限增长至 OOM |
| 4 | 客户端同步等待响应 | P2 | Channel::basicPublish() 使用条件变量同步等待 basicCommonResponse,限制单连接吞吐量。可改为异步回调或批量 ACK |
| 5 | Protobuf 多次序列化 | P2 | 单条消息链路经历客户端序列化 → 服务端反序列化 → 封装序列化 → 客户端反序列化,共 4 次操作 |
| 6 | 无连接池 / 连接复用 | P2 | 压测客户端每次新建连接和信道,未实现连接复用 |
| 7 | 文件无分片机制 | P3 | 持久化文件持续追加写入,无大小限制和分片策略 |
| 8 | write 无 fsync | P2 | 持久化仅 write() 到 page cache,OS 崩溃可能丢数据 |
| 9 | 无 RPC 超时机制 | P2 | waitResponse() 无超时保护,服务端无响应时客户端永久阻塞 |
十、测试结论
整体测试结果 :本次项目测试条件通过。核心消息发布链路(编译→启动→建连→发布→响应)功能正常,纯内存非持久化场景下 QPS 达 5,000 ~ 13,000。单元测试 8/10 通过(2 例因 protobuf 版本差异编译失败,非逻辑错误)。
性能瓶颈:吞吐量瓶颈明确为服务端单 EventLoop 架构,单核处理上限约 10,000 msg/s(Debug 编译)。持久化模式下受同步文件 I/O 影响,QPS 预计降至 200 ~ 5,000(视磁盘类型)。
Bug 情况:共发现 P0 级 Bug 1 个(GC 逻辑反转)、P1 级 Bug 1 个(线程池默认 1 线程)、P3 级 Bug 1 个(返回值忽略)。另识别架构改进项 9 个,均不影响当前基本功能。
耗时统计:项目测试耗时约 1.5 小时(含编译 5min + 环境准备 10min + 单元测试 20min + 性能压测 30min + 代码审查 25min)。
项目补充信息:
| 项目 | 信息 |
|---|---|
| 项目框架 | C++ (g++ 13.3, C++17),Muduo 网络库,Protobuf 3.20,SQLite3 |
| 代码仓库 | mq/ 目录,服务端/客户端/公共库分离结构 |
| 压测工具 | mqclient/benchmark_client.cc,自研 C++ 多连接并发工具 |
| 单元测试 | mqtest/ 目录,GTest 框架,共计 10 个模块测试 |
测试总结与优化建议:
-
优先修复 P0 Bug(GC 条件反转),避免持久化场景下文件空洞无法回收导致的磁盘空间浪费
-
调整线程池大小至
hardware_concurrency(),可立即提升消费推送并发度 -
后续应在 Release 编译下重新压测,获取更接近生产环境的 QPS 数据
-
建议补充持久化模式(DURABLE)下的性能测试,评估同步文件 I/O 对吞吐量的实际影响
-
可引入 JMeter 或 wrk 进行更规范的压力模型测试(需先实现 HTTP 协议适配层)
-
建议对有消费者在线场景进行端到端压测,覆盖完整的 publish → route → push → consume 链路
本报告测试数据均来源于实际执行结果。压测工具代码位于 mqclient/benchmark_client.cc,可重复执行验证。