简介:NanoMQ 是一款面向物联网边缘平台的轻量级、高速 MQTT 代理,基于 NNG 异步 I/O 线程模型,在协议层与重新设计的传输层中扩展了 MQTT 支持,并增强异步 IO 机制以最大化整体容量,完整兼容 MQTT V3.1.1 与 V5.0。本资源适合从事物联网网关、边缘计算与消息中间件开发的工程师及学习者,用于搭建本地 MQTT 服务、研究 Broker 内部实现或进行二次开发。压缩包共 238 个文件,约 9.47MB,以 40 个 C 源文件与 39 个头文件为核心实现,辅以 12 个 Python 脚本、7 个 conf 配置、6 个 yml 与 3 个 yaml 部署文件、多个 Dockerfile 变体及 pem、key 证书文件,另有 41 个 md 文档与 49 张 png 图示帮助理解架构。内容覆盖 broker、client、rest_api、bridge、aws_bridge、nng_proxy 等模块,可据此梳理连接管理、发布订阅、桥接与 REST 接口的代码脉络。目前已有 380 人学习下载,适合希望深入 MQTT 代理实现与边缘消息通信的读者参考。
1. 边缘网关上的 MQTT 代理,为什么“轻”比“快”更难做到
很多做物联网边缘接入的同行,第一次听到 NanoMQ MQTT Broker 这个名字,第一反应是“又一个 MQTT 代理”。但真正在 ARM 网关、工控盒子、车载终端这类资源受限设备上部署过的人会明白,边缘场景里最稀缺的从来不是吞吐量,而是内存和启动时间。一台 256MB 内存的边缘网关,跑一个标准 MQTT 代理,光运行时就要吃掉几十兆,再叠加容器和业务进程,系统就开始频繁 OOM。NanoMQ 的定位正是冲着这个痛点来的:它是一款用于物联网边缘平台的轻量级且速度极快的 MQTT 代理,核心用 C 语言实现,常驻内存可以压到几兆级别,启动时间在毫秒级。这篇文章面向的是需要在边缘设备上落地 MQTT 接入的工程师,从编译、配置、桥接到压测和排错,把我在实际项目里踩过的路讲清楚,让你能照着在自己的设备上跑通一套可用的边缘消息接入层。
2. 把 NanoMQ 跑起来:从源码编译到最小可用配置
2.1 为什么边缘场景优先选 NanoMQ 而不是通用代理
选型这件事,我一般会先看三个硬指标:常驻内存、二进制体积、依赖复杂度。通用 MQTT 代理功能全,但为了支持集群、规则引擎、大量插件,代码量和运行时开销都上去了。在边缘设备上,这些能力大部分用不上,反而成了负担。NanoMQ 的设计取舍很明确:单进程、事件驱动、无重型依赖,编译出来的二进制通常在几百 KB 到一两 MB 之间,静态链接后可以直接丢进精简的根文件系统。
另一个容易被忽略的点是启动速度。边缘设备经常面临断电重启,如果代理启动要好几秒,上层业务的重连逻辑就会乱。NanoMQ 的启动路径很短,没有复杂的初始化流程,实测在 ARM Cortex-A7 级别的芯片上,从进程拉起监听到端口可用,通常在几十毫秒内完成。这个特性在需要快速恢复的工业现场特别有用。
还有一点是协议兼容性。NanoMQ 支持 MQTT 3.1.1 和 MQTT 5.0,也支持桥接到其他代理。这意味着你可以在边缘侧用它做本地接入,再通过桥接把数据汇聚到中心节点,不需要在边缘设备上跑一个功能臃肿的完整代理。
2.2 源码编译:依赖、CMake 参数与交叉编译要点
在 x86 开发机上先跑通,再交叉编译到目标板,这是我习惯的顺序。NanoMQ 用 CMake 构建,依赖很少,核心只需要一个 C 编译器和 CMake。如果要启用 TLS 和 WebSocket,才需要额外引入对应的库。
先拉源码并编译一个最小版本:
# 克隆源码(这里用占位路径,实际替换为你自己的仓库地址) git clone <nanomq-repo-url> nanomq cd nanomq # 创建构建目录,保持源码树干净 mkdir build && cd build # 最小配置:关闭 TLS、WebSocket 等可选特性,只保留核心 MQTT cmake .. \ -DNANOMQ_TLS=OFF \ -DNANOMQ_WEBSOCKET=OFF \ -DNANOMQ_BUILD_BENCH=ON \ -DCMAKE_BUILD_TYPE=Release # 编译,-j 后面跟 CPU 核心数 make -j4编译完成后,build目录下会生成nanomq可执行文件。这里有几个参数值得说明:NANOMQ_TLS=OFF在边缘内网场景下可以关掉,省掉 OpenSSL 依赖和几 MB 的库体积;NANOMQ_BUILD_BENCH=ON会额外编译压测工具,方便后面做性能验证;CMAKE_BUILD_TYPE=Release一定要设,Debug 版本在边缘设备上跑会明显更吃内存。
交叉编译时,关键是提供一个正确的 toolchain 文件。我一般会写一个arm-toolchain.cmake:
# arm-toolchain.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) # 替换成你实际的交叉编译器前缀 set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) # 目标板根文件系统路径,用于查找依赖库 set(CMAKE_FIND_ROOT_PATH /opt/arm-rootfs) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)然后cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-toolchain.cmake重新配置即可。交叉编译最容易翻车的地方是依赖库的架构不匹配,如果报链接错误,先用file命令确认目标库的架构,再检查CMAKE_FIND_ROOT_PATH是否指向了正确的根文件系统。
2.3 最小可用配置:监听端口、认证与日志
NanoMQ 的配置文件是 HOCON 格式,结构清晰。一个最小可用的配置大概长这样:
# nanomq.conf mqtt { property { # 最大报文大小,边缘场景不用设太大 max_packet_size = 1024 # 最大并发连接数,根据设备内存调整 max_inflight_window = 32 } listeners.tcp { bind = "0.0.0.0:1883" } } # 日志配置,边缘设备存储有限,建议只记 warn 以上 log { to_console = true level = warn }启动命令很简单:
./nanomq start --conf nanomq.conf这里max_packet_size设成 1024 字节,是因为边缘传感器上报的数据包通常很小,限制报文大小可以防止异常客户端占用过多内存。max_inflight_window控制未确认消息的窗口,设成 32 是在内存和吞吐之间取的折中值,如果你的设备内存特别紧张,可以降到 16。日志级别设成 warn,避免高频 info 日志写满 flash。
启动后,用mosquitto_sub或任意 MQTT 客户端连上去测一下:
# 订阅测试主题 mosquitto_sub -h 127.0.0.1 -p 1883 -t "edge/test" -v # 另一个终端发布消息 mosquitto_pub -h 127.0.0.1 -p 1883 -t "edge/test" -m "hello nanomq"如果订阅端能收到消息,说明最小链路已经通了。这一步看起来简单,但很多问题其实出在配置文件的缩进和括号上,HOCON 对格式敏感,改完配置先用nanomq start --conf nanomq.conf --dry-run之类的校验方式确认语法,能省不少排查时间。
3. 桥接与规则:让边缘数据真正流到中心
3.1 MQTT 桥接的配置结构与主题映射
边缘代理单独跑起来只是第一步,真正有价值的是把边缘数据可靠地送到中心节点。NanoMQ 支持 MQTT 桥接,配置里用bridges.mqtt段来定义。一个典型的桥接配置:
bridges.mqtt.emqx { server = "tcp://center-broker.example.com:1883" proto_ver = 4 clientid = "edge-gw-01" keepalive = 60 clean_start = true forwards = [ { remote_topic = "edge/gw01/data" local_topic = "sensor/#" qos = 1 } ] subscription = [ { remote_topic = "cmd/gw01/#" local_topic = "cmd/#" qos = 1 } ] }forwards定义的是从边缘往中心转发的规则,subscription是从中心订阅指令下发到边缘。proto_ver = 4对应 MQTT 3.1.1,如果中心支持 MQTT 5.0 可以改成 5。qos = 1保证至少一次送达,边缘场景下数据丢失的代价通常比重复高,所以 QoS 1 是常用选择。
主题映射这里有个细节:local_topic支持通配符,remote_topic是实际转发到中心时用的主题。如果你有多个网关,建议在remote_topic里带上网关 ID,方便中心侧区分数据来源。
3.2 用规则引擎做本地过滤与格式转换
边缘设备产生的数据往往需要先做一轮过滤或格式转换,再往中心送。NanoMQ 内置了轻量的规则引擎,可以在本地对消息做处理。比如只转发温度超过阈值的告警:
rules.alert { sql = "SELECT payload.temp AS temp, clientid FROM \"sensor/+/data\" WHERE payload.temp > 80" action = { mqtt { topic = "edge/alert" qos = 1 } } }这条规则从sensor/+/data主题订阅消息,筛选出temp字段大于 80 的记录,转发到edge/alert主题。SQL 里的payload.temp要求上报的数据是 JSON 格式,比如{"temp": 85, "humidity": 40}。如果上报的是二进制或自定义格式,需要先用其他方式解析。
规则引擎的价值在于减少上行流量。边缘侧可能有几百个传感器每秒上报,但真正需要传到中心的可能只有异常数据。在本地做过滤,能显著降低中心侧的接入压力和网络带宽消耗。不过要注意,规则引擎本身也会消耗 CPU,规则条数不宜过多,我一般控制在 10 条以内,复杂逻辑还是放到中心或专门的流处理组件里做。
3.3 桥接断线重连与消息缓存策略
边缘到中心的网络不会永远稳定,桥接断线是常态。NanoMQ 在桥接层面有重连机制,但消息缓存策略需要根据业务配置。关键参数是clean_start和会话保持。
如果clean_start = true,每次重连都会清掉之前的会话状态,未确认的消息会丢失。对于允许丢数据的场景,这样最省资源。如果数据不能丢,需要设clean_start = false,并且中心代理要支持会话保持。但会话保持会占用中心侧内存,网关数量多的时候要权衡。
另一个做法是在边缘侧做本地缓存。NanoMQ 支持将消息暂存到本地,等桥接恢复后再补发。这个能力在配置里通过max_inflight_window和队列相关参数控制。我通常会把max_inflight_window设得比纯本地接入时大一些,比如 64 或 128,给重连后的补发留出缓冲。但也不能太大,否则内存占用会上去,边缘设备扛不住。
实际项目里,我见过因为clean_start设错导致重连后数据大面积丢失的情况。排查时先看桥接日志里有没有session present相关的记录,再确认中心侧对会话过期的配置。这个坑不复杂,但容易被忽略。
4. 性能验证:压测、内存观测与参数调优
4.1 用自带压测工具跑出基线数据
NanoMQ 编译时如果开了NANOMQ_BUILD_BENCH=ON,会生成压测工具。我一般先用它跑一个基线,了解目标设备的能力上限。
# 启动 100 个连接,每个连接每秒发 10 条消息,持续 30 秒 ./nanomq_bench \ -h 127.0.0.1 \ -p 1883 \ -c 100 \ -t "bench/test" \ -m "payload_data" \ -n 10 \ -d 30参数含义:-c是并发连接数,-n是每秒消息数,-d是持续时间。跑完后会输出吞吐量、延迟分布等数据。在 ARM 网关上的典型结果是:100 连接、每秒 1000 条 QoS 0 消息,CPU 占用在 20% 到 40% 之间,内存增长平稳。如果 QoS 1,吞吐会下降,因为要等确认。
压测时要注意,客户端本身也会消耗资源。如果压测机和被测代理跑在同一台设备上,数据会失真。最好用另一台机器做压测客户端,或者至少把压测进程的优先级调低。
4.2 内存与 CPU 的观测方法
边缘设备上观测资源占用,top和free是最直接的。但 MQTT 代理的内存增长往往是渐进的,需要持续观察。我习惯用一段脚本定时采样:
#!/bin/bash # 每 5 秒记录一次 nanomq 的内存和 CPU while true; do ps -o pid,rss,pcpu,comm -C nanomq >> /tmp/nanomq_monitor.log sleep 5 donerss是常驻内存,pcpu是 CPU 占用。跑一段时间后看rss是否稳定。如果持续增长,可能是消息队列积压或者连接泄漏。NanoMQ 本身有连接数上限,但客户端异常断开时如果清理不及时,也会导致内存缓慢上涨。
CPU 方面,边缘设备通常没有性能计数器,只能看整体占用。如果 CPU 长期高于 70%,要考虑是不是规则引擎太复杂,或者 QoS 1 的消息太多导致确认开销大。降低max_inflight_window可以减少并发确认的压力,但会牺牲吞吐。
4.3 关键参数调优:窗口、超时与队列长度
调优的核心是找到内存和吞吐的平衡点。下面这张表是我在多个边缘项目里总结的常用参数范围:
| 参数 | 作用 | 保守值 | 激进值 | 适用场景 |
|---|---|---|---|---|
| max_inflight_window | 未确认消息窗口 | 16 | 128 | 内存紧张取小,吞吐优先取大 |
| max_packet_size | 单报文最大字节 | 512 | 4096 | 传感器小包取小,带文件传输取大 |
| keepalive | 心跳间隔秒 | 30 | 120 | 网络差取小,省电场景取大 |
| max_connections | 最大连接数 | 200 | 2000 | 按设备内存和客户端数量定 |
调参时一次只改一个,改完跑一轮压测,记录数据。同时改多个参数,出了问题很难定位是哪个引起的。另外,边缘设备的温度也会影响性能,高温降频时吞吐会下降,这是硬件限制,调参解决不了。
5. 避坑与排查:边缘部署中最容易翻车的几个点
5.1 连接数上不去,先看文件描述符限制
现象:压测时连接数到几百就上不去了,客户端报连接超时。
原因:Linux 默认的单进程文件描述符限制通常是 1024,每个 MQTT 连接占一个 fd,加上其他开销,实际能支撑的连接数远低于这个值。
解决:在启动脚本里用ulimit -n 65535提高限制,或者修改/etc/security/limits.conf。如果是 systemd 管理,在 service 文件里加LimitNOFILE=65535。改完用cat /proc/<pid>/limits确认生效。
5.2 消息延迟忽高忽低,检查 QoS 和网络抖动
现象:QoS 1 的消息偶尔延迟好几秒才到,QoS 0 正常。
原因:QoS 1 需要等 PUBACK,如果网络抖动导致确认包丢失,代理会重传,重传间隔通常是指数退避的,所以延迟会拉长。另外,max_inflight_window满了之后,新消息会排队,也会增加延迟。
解决:先确认网络质量,用ping和tcpdump看有没有丢包。如果网络本身没问题,检查max_inflight_window是不是太小,适当调大。如果业务允许,对延迟敏感的数据改用 QoS 0,牺牲可靠性换延迟。
5.3 桥接反复断开,排查 keepalive 和中心侧限制
现象:桥接连接每隔几分钟就断一次,日志里显示 keepalive timeout。
原因:边缘到中心的网络延迟高,或者中心侧对连接数、心跳频率有限制。如果keepalive设得太小,网络稍微抖动就会超时。
解决:把keepalive从 60 调到 120 或更大,给网络留出余量。同时确认中心侧没有对单个 clientid 的连接频率做限制。如果中心侧有多个节点,检查桥接配置里的 server 地址是不是指向了负载均衡,有些负载均衡对长连接的空闲超时设得很短。
5.4 规则引擎不生效,检查 SQL 语法和 payload 格式
现象:配置了规则,但消息没有被过滤或转发。
原因:最常见的是 SQL 里的字段路径和实际 payload 不匹配。比如 payload 是{"data":{"temp":85}},SQL 里写payload.temp就取不到,应该写payload.data.temp。另外,如果 payload 不是合法 JSON,规则引擎解析会失败,但日志级别不够时看不到报错。
解决:先把日志级别临时调到 debug,看规则引擎有没有解析报错。然后用mosquitto_pub发一条符合格式的消息,确认规则能命中。SQL 里的主题通配符也要注意,sensor/+/data和sensor/#的匹配范围不同,写错了会漏掉消息。
5.5 内存缓慢增长,重点查会话保持和离线消息
现象:代理跑几天后内存从几兆涨到几十兆,重启后恢复。
原因:如果开启了会话保持,离线客户端的消息会缓存在内存里,客户端一直不上线,缓存就一直不释放。另外,如果桥接的clean_start = false,未确认的消息也会积压。
解决:给会话设置过期时间,NanoMQ 支持配置会话的最大空闲时间,超时后清理。对于桥接,如果业务允许丢数据,把clean_start改成 true。定期用nanomq的管理接口或日志查看当前连接数和队列长度,发现异常增长及时处理。
6. 进阶技巧:用共享订阅和主题别名压榨边缘性能
共享订阅是我在边缘项目里用得最多的进阶特性之一。当多个业务进程需要消费同一批传感器数据时,如果每个进程都单独订阅,代理要复制多份消息,内存和 CPU 都会上去。共享订阅让多个订阅者共享一个订阅组,消息只发给组内一个成员,天然适合做负载均衡。
在 NanoMQ 里配置共享订阅,主题格式是$share/<group>/<topic>。比如:
# 两个订阅者加入同一个组 mosquitto_sub -h 127.0.0.1 -p 1883 -t '$share/processors/sensor/+/data' -v mosquitto_sub -h 127.0.0.1 -p 1883 -t '$share/processors/sensor/+/data' -v发布者正常往sensor/1/data发消息,两条订阅会轮流收到,不会重复。这个特性在边缘侧做多进程数据处理时特别省资源,因为代理只需要维护一份消息队列。
另一个技巧是 MQTT 5.0 的主题别名。边缘设备的上行带宽通常有限,主题字符串如果很长,每次发布都带完整主题会浪费流量。主题别名允许客户端和代理协商一个短整数代替长主题。第一次发布时带完整主题和别名,后续只发别名。在 NanoMQ 的配置里,MQTT 5.0 监听器默认支持主题别名,客户端侧需要自己实现别名映射。对于每秒上报几千条的场景,这个优化能省下可观的带宽。
验证共享订阅是否生效,可以看代理的订阅列表,或者用压测工具发消息,观察两个订阅者的接收计数是否大致均衡。如果发现某个订阅者收不到,检查组名是否一致,以及主题通配符是否匹配。
最后说一个我自己的习惯:每次在边缘设备上部署新版本前,先用旧版本跑一轮相同的压测,把数据存下来做对比。边缘设备的性能波动大,没有基线数据,很难判断是新版本变差了还是环境变了。这个习惯帮我避免了好几次误判。希望帮到你。
本文还有配套的精品资源,点击获取