1.1 没有背压,为什么发送完消息后业务还是慢了好几倍?
队列还有空位,发送也没有阻塞,看起来一切正常。但是,连续发送一批消息后,再执行同样一段业务代码,耗时却可能增加好几倍。既然没有等待,多出来的时间究竟花在哪里?
为追求生产者热路径性能,采用双层缓存设计,通过批量发布和传输,让生产者使用小缓存,让消费者使用大缓存处理消息,尽可能降低消息传输对业务执行的影响。支持单个或多个生产者、消费者;当前要求 C++20。
队列还有空位,发送也没有阻塞,看起来一切正常。但是,连续发送一批消息后,再执行同样一段业务代码,耗时却可能增加好几倍。既然没有等待,多出来的时间究竟花在哪里?
一条消息只有 8 或 16 字节,复制这些数据应该很快。但消息越小,发送过程中其他工作的开销就越值得关注。同样的数据量和发送次数,逐条提交与批量提交,性能究竟能相差多少?
消费者处理速度出现波动,消息不断积压,最终用满了缓存。等待会打断业务线程的节奏,丢弃又可能损失重要消息。除了等待和丢弃,能否给消息安排另一条去路,同时让内存占用保持可控?
平时发送的可能只是几十字节的记录,偶尔却要传输几 MiB 的快照、采样结果或数据块。一次发送就要写入大片内存,怎样才能既快速传输,又尽量减少对随后业务执行的影响?
缓存设小了,担心突发消息装不下,让生产者等待;设大了,又可能为偶尔出现的峰值长期保留大量内存。日常消息量与峰值相差很大时,怎样才能兼顾突发容量和内存占用?
TwoBufQueue 正是围绕这些问题设计和持续优化的。具体解决思路与后续计划,请参考问题与解决。想了解具体性能,请继续往下看。
下图比较生产者提交消息的吞吐,单位 M条/s,越高越好。TwoBufQueue 使用 WriteSession,消费者同时运行。
所有队列执行同一流程:正常执行业务 → 连续发送一批消息 → 立刻执行同样的业务。图中为发送后耗时 / 正常耗时,越低越好。
本机正常业务约 13 ns/次。发送 16 MiB 后,TwoBufQueue 为 18.2 ns/次,ReaderWriterQueue 为 58.2,rigtorp 为 42.5,atomic_queue 为 99.8。完整实验与参数
两组实验在 Apple M3 上分别测量,不能混为同一个配置:发送吞吐实验中,两层池各为 256 MiB,回调循环为 0;业务实验中,生产者池为 16 KiB、消费者池为 256 MiB,回调循环为 64。开源队列均使用 256 MiB 消息存储。三轮平均,高 QoS,未硬绑核;测量范围与限制见完整报告。
当你对第一章的这些问题感兴趣或者也遇到相关问题时,就可以考虑使用 TwoBufQueue。
简单地说:生产者线程是热路径线程,消费者线程是辅助线程;希望热路径线程尽量保持稳定高效,同时希望生产者线程能够高速发送消息。例如机器人、传感器、量化交易等低延时应用。
同时,我会持续完善这个组件。如果您有相关的需求或想法,欢迎随时提出;只要是公共、通用的需求,我都会尽力去实现好。
两层传输增加复制和后台工作。若最重要的是每条消息从发送到接收的时间,或消息很少、处理很轻,前面提到的几个单层队列可能更合适。另外,生产线程和消费线程不能绑定同一个 CPU 核,否则两层缓存就相当于单层缓存。
通过 Writer 发送数据,在 on_record 回调中接收消息。
#include <TwoBufQueue.hpp>
#include <cstdio>
#include <string_view>
int main() {
//---------------1.初始化-----------------------------------------------
auto writer = TwoBufQueue::make_writer<TwoBufQueue::Writer<>>();
// 接收消息的回调函数。
auto on_record = [](std::int32_t, const std::byte* data,
std::size_t size) noexcept {
std::printf("%.*s\n", static_cast<int>(size),
reinterpret_cast<const char*>(data));
};
TwoBufQueue::TwoBufQueue queue(on_record, writer);
//---------------2.生产者写数据------------------------------------------
bool ok = true;
for (std::string_view text : {"hello", "telemetry", "goodbye"})
ok &= writer.write_mem(text.data(), static_cast<std::uint16_t>(text.size()));
writer.flush(); // 发布并通知;不等待回调完成。
//---------------3.结束-------------------------------------------------
writer.finish(); // 停止生产,交接尾块。
queue.stop(); // 等待复制和回调排空,再结束作用域。
//---------------4.校验测试结果------------------------------------------
return ok && queue.error_code() == TwoBufQueue::ReadError::none ? 0 : 1;
}