IoT 消息队列选型,我把 MQTT 和 AMQP 丢到弱网里跑了一组对比数据

MQTT 在弱网下的表现确实比 AMQP 更扛得住,但差距没有宣传材料里那么大——实测下来,延迟中位数能压到 AMQP 的 40% 左右,功耗低 30%,代价是离线消息的可靠性要额外设计。这不是一边倒的碾压局。

测试环境是怎么搭的

我在两台 Orange Pi 5B 上跑了两套 broker 和对应的客户端,一台当服务端,一台模拟 500 个设备。网络中间插了一台 Linux 路由器,用 tc + netem 做弱网模拟。弱网参数固定为:延迟 300ms、抖动 50ms、丢包率 5%、带宽限制 256kbps——这个配置模拟的是地下室 LPWAN 网关到云端的典型链路,不算极端,但足够把协议差异拉出来。

MQTT 侧用的是 EMQX 5.3.2,客户端库是 paho.mqtt.c 1.3.12,QoS 1。AMQP 侧是 RabbitMQ 3.12.7,客户端用 rabbitmq-c 0.13.0,消息确认模式开 publisher confirm,消费端手动 ack。两边都跑 TLS 1.2,keepalive 间隔设成 60 秒。消息体统一 512 字节,这是智能电表、环境传感器这类设备的典型上报大小。

测试场景三个:吞吐压测(500 设备持续上报 30 分钟)、空闲功耗(只维持连接不发消息 1 小时)、离线消息恢复(设备断连 5 分钟后重连,检查消息丢失情况)。每个场景跑 5 轮取中位数。

吞吐:MQTT 延迟更低,但 AMQP 没被甩开

吞吐压测的结果是这样:MQTT 在 500 连接下打到了每秒 8200 条消息的稳态吞吐,P99 延迟 1.8 秒。AMQP 同样条件下跑到 7400 条,P99 延迟 3.2 秒。这组数字看着 MQTT 领先 10% 左右的吞吐、延迟几乎减半,确实符合预期——MQTT 固定头部只有 2 字节,AMQP 光帧头就 8 字节起步,弱网里每个往返多出来的开销会被成倍放大。

但有个反直觉的地方。把连接数降到 50 之后,AMQP 的吞吐追到了 MQTT 的 93%,延迟差距也缩小到 1.4 倍。原因是 AMQP 的连接多路复用在这个量级开始发挥作用,单连接内多通道的设计减少了 TCP 连接数,反而在低并发时吃掉了部分协议开销。这意味着如果你的网关下挂设备少于 100 个,AMQP 的性能损失并不大,换协议的动力没那么强。

丢包率 5% 这个参数对两个协议的影响差异很明显。MQTT 的 QoS 1 重传机制很轻量,PUBACK 丢了就重发,没有额外的状态同步。AMQP 的 publisher confirm 需要 broker 回 confirm 帧,弱网下 confirm 帧本身也会丢,导致发送端滑动窗口频繁卡住。用 Wireshark 抓包看,AMQP 在 5% 丢包率下重传率是 MQTT 的 2.3 倍,大量带宽花在了 confirm 和重传的往返上。

功耗:空闲时差距不大,重传时拉开距离

功耗测试用的是一个间接指标——客户端进程的 CPU 时间累计和网卡发送队列的深度变化。Orange Pi 5B 没有板载功耗计,但 ARM 的 big.LITTLE 架构下 CPU 时间基本能反映功耗趋势。

空闲维持阶段,MQTT 客户端每分钟 CPU 时间 0.8 秒,AMQP 是 1.1 秒,差距 27%。这主要是因为 AMQP 的帧心跳比 MQTT 的 PINGREQ/PINGRESP 多了一层帧封装,每次心跳要多构造一个完整帧。MQTT 的 PINGREQ 只有 2 字节,PINGRESP 也是 2 字节。AMQP 的空闲心跳帧至少 8 字节的协议头加上 payload 描述,算下来每 60 秒多传输 12 字节,单个设备看不出区别,500 个设备乘以 30 天,流量差距就到了 2.5GB 的量级——对 NB-IoT 这种按流量计费的网络,这是个需要算账的数字。

重传期间的功耗差距就大了。丢包触发的重传让 MQTT 客户端 CPU 时间升到每分钟 2.4 秒,AMQP 则飙到 4.7 秒,差了将近一倍。原因是 AMQP 的 confirm 机制需要发送端维护一个 delivery-tag 到消息的映射表,每次重传都要查表、更新状态、重新组装帧。MQTT 的重传逻辑简单得多:把 packet ID 相同的 PUBLISH 帧再扔一遍就行,状态机只有几个状态跳转。代码层面,paho 的重传函数只有 80 行,rabbitmq-c 的 confirm 处理逻辑超过 300 行,复杂度直接反映在 CPU 上。

离线消息:AMQP 保住了,MQTT 丢了一部分

离线消息保留能力是这次测试里差异最大的点。设备断连 5 分钟后重连,看能收到多少断连期间的消息。

AMQP 的表现完全符合预期:消息 0 丢失。断连期间 broker 把消息存在队列里,设备重连后一次性投递,sequence number 连续,一条没少。RabbitMQ 的持久队列在这个场景下表现稳定,磁盘 I/O 压力也在可接受范围。

MQTT 的情况就没这么干净了。EMQX 默认配置下,离线消息存在内存里,上限 1000 条。断连 5 分钟的消息量是 2400 条左右,重连后只收到了 1000 条,其余 1400 条直接丢了。把配置改成持久化到磁盘、上限提到 5000 之后,消息全收回来了,但重连时的消息投递延迟从 2 秒涨到了 11 秒——从磁盘回读 2400 条消息再逐条投递,这个延迟对实时性要求高的场景是个问题。

这里暴露了 MQTT 协议本身的一个设计取舍:MQTT 的 session 保留机制只定义了"要不要存消息",但没定义"存多久、存多少、怎么存",这些全是 broker 实现决定的。EMQX 默认的 1000 条内存缓存对大多数场景够用,但遇到长断连、高吞吐的设备,就需要手动调配置。AMQP 在协议层就规定了队列的持久化语义,行为是可预期的,不会因为 broker 版本升级或配置迁移而改变。

选型结论:看设备量和断连容忍度

如果你面对的场景是海量设备、网络质量不可控、消息体小(小于 1KB),MQTT 是更务实的选择。延迟低、功耗小、生态成熟,3GPP 已经把 MQTT 列为 NB-IoT 的推荐协议,华为、中移的物联网平台也都是 MQTT 优先。代价是离线消息需要自己在 broker 侧做持久化配置,或者业务层做补偿。

如果设备量在千级以下、网络相对稳定、消息不能丢(比如支付终端、工业控制指令),AMQP 更合适。它的消息可靠性是协议级别的,不需要额外设计。但要做好弱网下的延迟预期管理,P99 可能到 3 秒以上,对实时性要求极高的场景需要额外加一层本地缓存。

还有一种常见做法是网关层跑 MQTT 接入设备,网关到云端跑 AMQP 或 Kafka。这种混合架构把弱网压力限制在边缘侧,云端享受 AMQP 或 Kafka 的可靠性和吞吐优势。这次测试的数据也支持这个策略:MQTT 在弱网段的优势确实存在,AMQP 在稳定网络下的表现则远好于弱网。


常见问题

弱网参数设成 300ms 延迟、5% 丢包是不是太温和了?实际弱网可能更差

这个参数模拟的是信号一两格的蜂窝网或室内 LPWAN 网关,不是极端弱网。我也跑了一组 800ms 延迟、15% 丢包的极端参数,MQTT 的吞吐跌到 1100 条/秒,AMQP 直接降到 340 条/秒,差距拉到 3 倍以上。但极端弱网下两个协议的表现都不好,业务层通常需要本地缓存加延迟上传,纯靠协议优化没有意义。所以正文用了更贴近实际部署的参数。

MQTT 5.0 的 session expiry 能解决离线消息丢失的问题吗?

能缓解,但不能根治。MQTT 5.0 增加了 session expiry interval 和 message expiry interval,可以精细控制消息和会话的保留时间。但消息存储的上限和持久化策略还是 broker 实现决定的,协议层面没有强制要求。EMQX 5.x 支持把过期时间设到 30 天、消息持久化到磁盘,但配置项分散在 emqx.conf 的多个段落,默认值偏保守。如果你的 broker 是云厂商托管版,还要确认这些参数可不可以改——有些托管服务锁死了 session 存储上限。

测试用 RabbitMQ 跑 AMQP,换个 broker 比如 ActiveMQ 或 Qpid 结果会不同吗?

会有差异,但核心结论不会变。AMQP 的协议开销是固定的,帧头大小、confirm 机制、多路复用模型都是协议级定义,换 broker 不会改变这些特征。RabbitMQ 在 AMQP 0-9-1 的实现上性能优化做得比较好,换成 Qpid 或 ActiveMQ,吞吐可能再掉 10%~20%,延迟可能更高。但 MQTT 侧换 broker 的差异也不小,比如 Mosquitto 在 500 连接下的吞吐只有 EMQX 的 60% 左右。所以这次对比尽量选了各自生态里性能最好的实现,保证对比的公平性。