1. MQTT broker 选型困局:Mosquitto 和 EMQX 各自的边界
1.1 Mosquitto:轻量可靠,但天生是单机角色
很多做物联网的团队,第一个接触的 MQTT broker 就是 Mosquitto。它属于 Eclipse 基金会,C 语言写的,安装包几兆字节,一条命令就能跑起来,配置文件也就几十行。正是这种“小快灵”的特质,让它成了边缘网关、开发环境、中小规模设备接入的首选,我自己早期做车联网试验台架时也是从它起步的。
但用久了就会发现,Mosquitto 的能力边界非常清晰。单实例连接数做到十万级是有可能的,前提是文件描述符、内存、CPU 都调到位;消息吞吐在 128 到 256 字节的小报文场景下,每秒处理几万条也不算夸张。真正麻烦的是它没有一个像样的集群方案。官方推荐的扩展方式还是桥接,就是把多个实例用 bridge 配置串起来,消息按规则在节点之间转发。这个桥接机制做简单级联可以,想靠它实现高可用和水平扩展,很快就得面对会话状态同步、消息路由漂移、故障切换不一致这些头疼的问题。
所以规模一上来,Mosquitto 往往会从“首选”变成“瓶颈”。连接数从几千涨到几万,消息量从每秒几百涨到几千,延迟开始抖动,运维同事开始半夜被报警电话叫醒,这时候大家就会开始认真研究要不要换方案。
1.2 EMQX:性能怪物,但别被“开源”两个字带偏
EMQX 是另一个绕不开的名字。它用 Erlang/OTP 编写,这个技术栈天生就是为了高并发、低延迟、长连接的分布式系统准备的。官方宣传单集群能支撑千万级甚至亿级连接,虽然实际生产环境里能跑到百万连接已经算很不错的规模,但相比 Mosquitto 确实是另一个量级。
更吸引人的是,EMQX 开源版采用 Apache License 2.0,规则引擎、数据桥接、Dashboard、REST API、集群能力这些都包含在开源版里,可以免费商用。很多团队会以为“开源版等于全功能版”,等做到生产环境才发现,安全认证的高级插件、审计日志、多活同步、更完善的数据集成、企业级运维工具,这些能力都在商业版里,需要单独采购授权。
这里有个背景可能很多人没注意到:EMQX 本身就是国内团队发起并主导的国际知名开源项目。所以聊“国产替代 EMQX”,本质上不是在处理“换掉国外产品”的问题,而是在“开源版、商业版、自研方案”之间选择一个适合自己团队的信任模型。这个认知非常重要,后面所有选型判断都建立在这上面。
1.3 为什么技术负责人开始认真思考“替代”
抛开“为了国产而国产”的跟风心理,企业真正产生替换冲动,通常源于三类实际痛点。
第一种是许可证合规压力。一些大型企业或高合规要求行业的项目,在供应商准入和项目验收阶段会要求提交第三方依赖清单、许可证扫描报告,甚至会专门审查每一个开源组件的许可证类型。Mosquitto 的 EPL 2.0、某些嵌入式方案里可能牵扯到的 GPL 系组件,在这种审查下都会变成风险点,需要逐项评估和解释。
第二种是供应链稳定性的担忧。开源项目的维护节奏、社区活跃度、安全漏洞响应速度,都不受单个企业控制。一旦线上环境暴露了高危 CVE,而上游迟迟不发布修复版本,技术负责人就得自己做补丁维护,这种“被绑架”的感觉时间长了很难受。
第三种是对“可控性”的追求。团队希望核心链路的核心代码掌握在自己手里,遇到问题可以随时定位、修改、发布,而不是在论坛里等回复。这个诉求很合理,但代价也很大,因为自研一个可用的 MQTT broker 远不止实现协议报文那么简单,后面我会详细拆。
2. 国产 MQTT 协议栈的技术路线与自研要点
2.1 三条自研路线:嵌入式、网关型、全场景
所谓“本土化或自研 MQTT 协议栈”,落在工程上其实是三条差异很大的路线,投入和产出完全不同。
第一条是嵌入式设备端协议栈。STM32、ESP32、RT-Thread、FreeRTOS 加 lwIP,这是最常见的一套组合。目的是在资源受限的 MCU 上实现 MQTT 客户端能力,替代对 Eclipse Paho 等外部客户端的依赖。MQTT 报文结构本身不复杂,一个精简客户端核心代码可能只有几百行,裁掉 QoS2、裁掉 TLS、只保留 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ 几个报文,很多团队几周就能跑通。
第二条是网关型或边缘型 broker。设备接入侧用一个进程接收 MQTT 连接,做协议解析和消息路由,再把数据转发到后端的 Kafka、RocketMQ、Pulsar 或内部业务服务。这种场景下,Java 系用 Netty,Go 系用原生 net 包或 gnet,C++ 系用 muduo,常见做法是“只做接入网关,不做完整 broker”。因为不需要实现全局主题树、不需要持久会话、不需要集群协调,工程复杂度可控。
第三条是完整意义上的全场景 broker,要支持多协议接入、规则引擎、数据桥接、集群节点、动态扩缩容、高可用故障转移。这个工程量不是一个团队几个月能消化的,市场上成熟产品已经积累了很深的代码和运维经验,除非极特殊需求,否则我不建议任何企业从零做这种级别的系统。
2.2 自研协议栈的核心模块到底有哪些
很多人以为自研 MQTT 协议栈就是“解析报文、回包”,这是最大的误解。一个能稳定扛住生产流量的 broker,至少需要完整处理以下模块。
报文编解码层是基础。MQTT 报文由固定头、可变头、负载三部分组成。固定头第一个字节的高四位表示报文类型,从 CONNECT(1) 到 AUTH(15) 一共 14 类;剩余长度字段采用变长编码,每个字节的低七位表示数据,最高位是连续标志,最大能表示到 256MB。只要这个解析层写错了,后面全是空中楼阁。
会话状态管理层是最容易出问题的部分。连接建立时根据 Clean Session 标志决定是否复用旧会话;QoS1 需要保存待确认的 PUBLISH;QoS2 要维护 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步握手的中间状态;客户端断线重连后要根据 session 状态恢复未完成的流程。这里涉及大量状态转移,任何一步考虑不周,轻则消息重复,重则消息丢失。
主题树和通配符匹配是实现消息路由的核心。把以/分割的主题构建成字典树,处理+单层通配符和#多层通配符。主题匹配的性能直接决定 broker 的转发能力,尤其在一对多订阅场景下,一个主题下的订阅者数量大时,匹配算法的效率差距会被放大很多倍。
遗嘱消息和保留消息是 MQTT 区别于普通消息队列的重要特性。遗嘱消息在客户端异常断开时由 broker 代发,保留消息让新订阅者能立即拿到主题的最新状态。这两个能力在设备上下线监控、状态同步场景里是刚需,也是自研时容易漏掉的功能。
心跳和保活机制是连接可靠性的底座。客户端需要在 Keep Alive 时间内发送 PINGREQ,broker 需要在 1.5 倍时间内没收到任何报文就判定连接失效,关闭连接并触发遗嘱消息。弱网环境下这个参数不调好,会出现大量误判断连和连接堆积。
安全层方面,TLS 双向认证、用户名密码认证、Token 校验、证书吊销检查,这些在工业场景里往往是硬性要求。Mini 设备上做 TLS 要选好加密套件,平衡握手性能和硬件算力。
2.3 协议是开放标准,“看懂协议”比“拿到代码”更重要
聊到这里,有一个关键点必须强调:MQTT 协议是 OASIS 发布的开放标准,实现一个兼容 MQTT 3.1.1 或 5.0 的协议栈,本身不存在版权障碍,也没有高频的专利风险。这意味着“自己实现一个协议栈”这件事,在合规上看是干净的路子。
但要注意区分“自己实现”和“抄代码”。从某个开源项目里复制一段解析逻辑然后改了变量名,这在法律上依然可能被视为衍生作品,许可证义务并不会因为你改了名就消失。反过来,参考协议文档从零写实现、设计自己的状态机,这才是真正意义上的自研。
嵌入式场景里经常被问到的“STM32 移植 MQTT”,本质上是同一个逻辑。lwIP 负责 TCP/IP,mbedTLS 负责加密,MCU 上跑一个极简 MQTT 客户端处理报文,这也属于自研协议栈的范畴。再往上走,工业现场用 node-red 或 KepServer 做 OPC UA 转 MQTT,那已经属于协议网关层,和自研 broker 完全是两个话题,但都是协议栈生态里的重要环节。
3. 开源版权与商用风险:EPL、Apache 2.0 红线到底在哪
3.1 EPL 2.0 的传染边界,别踩了才恍然大悟
Mosquitto 使用的是 Eclipse Public License 2.0,这是一种“弱 copyleft”许可证。它和 GPL 有本质区别,但也不是完全不传染。EPL 的要求可以概括为:如果修改了 EPL 覆盖的源文件,并且把修改后的程序以源码或可执行形式分发给第三方,那么必须把涉事文件的修改版源码提供给接收者。
关键在于“涉事文件”的界定。EPL 的传染性是基于“文件级别”和“衍生作品”判断的,不是整条编译链一锅端。如果你在 Mosquitto 源码里改了一个.c文件然后编译,之后把二进制发给客户,那就必须公开这个改过的.c文件源码;如果你把 Mosquitto 作为一个独立进程跑在 Linux 容器里,你的业务代码通过标准 MQTT 协议和它通信,二者是独立进程、独立维护,那么业务代码通常不被视为 EPL 的衍生作品,可以保持闭源。
所以实际工程里,用 Mosquitto 最安全的姿势是“不改源码、独立部署、外部联动”。通过标准 MQTT 接口做鉴权、业务逻辑、消息转发,把项目自己的代码和 EPL 代码隔绝在不同进程里。凡是需要改源码才能实现的定制功能,都要重新评估是否值得承担分发后的开源义务。
3.2 Apache 2.0 的宽松,不等于没有暗坑
EMQX 开源版的 Apache License 2.0 是出了名的宽松,允许修改、允许闭源、允许商用,只要保留版权声明和 NOTICE 文件即可。这也是很多企业愿意在 EMQX 基础上做二次开发的原因。
但 Apache 2.0 里有一条容易被忽略的专利条款。贡献者在提交代码时,自动授予使用者一份专利许可;反过来,如果使用者对贡献者发起专利诉讼,那么这份专利许可会自动终止。通俗地说,你不能一边用着人家的专利授权,一边告人家侵权。对于绝大多数企业来说这个条款不构成风险,但合规评审时还是应该写进评估报告里。
EMQX 真正要警惕的不是许可证本身,而是“开源版和商业版的边界”。开源版功能看起来极其完整,但企业级的认证鉴权插件、审计日志、多活同步、某些数据桥接能力、商业支持 SLA 都在付费版本里。很多团队开发阶段用开源版,到生产环境发现缺功能,再做数据迁移和版本切换,成本远高于一开始就谈好商业授权。
3.3 商用风险全维度对比,用一张表说清楚
| 维度 | Mosquitto | EMQX 开源版 | EMQX 企业版 | 自研/国产协议栈 |
|---|---|---|---|---|
| 许可证 | EPL 2.0 | Apache 2.0 | 商业授权 | 自有版权 |
| 传染性风险 | 修改文件需开源 | 风险低,保留声明即可 | 不适用 | 无 |
| 单机连接能力 | 十万级 | 百万级 | 百万级以上 | 取决于实现质量 |
| 集群能力 | 弱,桥接为主 | 较强,开源版可用 | 强,多活高可用 | 需自研 |
| 规则引擎/数据桥接 | 弱 | 基础版有 | 完整版 | 需自研 |
| 合规交付成本 | 中,需说明 EPL | 低 | 低,有商业合同 | 低 |
| 运维生态 | 命令行+少量插件 | Dashboard+API | Dashboard+API+支持 | 自建工具链 |
表格里最直观的结论是:风险不在“用没用开源软件”,而在“改了哪些代码、发了哪些交付物、用了哪些条款不熟悉的能力”。这些问题在项目启动前想清楚,比等项目黄了再去补窟窿省事得多。
4. 替代落地实操:五种合规方案与选型决策表
4.1 方案A:继续用 Mosquitto,但把进程隔离做到位
如果 Mosquitto 的功能暂时够用,更换成本又太高,最务实的做法是把它“隔离”成一个黑盒组件。不改任何源码,用 Docker 容器或 systemd 独立进程部署;自定义的逻辑全部放在外部模块里:独立鉴权服务、独立的 WebSocket 网关、独立的消费程序。通信一律走标准 MQTT 协议或 Mosquitto 的 HTTP 接口,不要和它的内部数据结构发生任何耦合。
隔离到位之后,许可证风险基本可以被锁在容器之内。但前提是团队里每个人都严格遵守这个约定。我见过项目后期为了一个“小功能”直接进容器里改配置、打补丁,甚至把补丁连同镜像一起交付出去,合规风险在当时是看不见的,项目验收扫许可证时才集体傻眼。
4.2 方案B:EMQX 开源版做主,商业授权做兜底
连接规模大、需要集群、需要规则引擎的团队,EMQX 开源版依然是最快落地的路线。部署建议直接用 Docker Compose 或 Kubernetes 方式铺集群,配置好 Mnesia 集群、Dashboard 账户、TLS 证书。同时建议和厂商聊一轮商业授权,不一定要当年就买,但要让销售把“开源版、企业版功能对照表”和报价发给你,这样才知道未来两年如果业务翻倍,升级路径是什么。
有一个执行细节值得注意:EMQX 的版本升级会改变集群内部数据格式,从 4.x 升到 5.x 有专门的迁移工具,但数据迁移前一定要在测试环境完整演练,尤其是会话数据、规则配置、认证用户的迁移。实战中直接在生产环境升主版本,导致规则引擎全部失效的案例并不少见。
4.3 方案C:基于宽松许可证的协议库做二次封装
如果不想用完整的 broker 产品,但也不想完全从零写,可以在许可证宽容的开源库基础上做封装。Java 生态里 Moquette 使用 Apache 2.0,Go 生态里有 gmqtt(MIT/Apache 类许可),Python 生态里有 hbmqtt,这些都可以作为协议接入层的底座。
二次封装要守住的底线是:核心协议库保持原样或仅做升级,不修改源码;外层写自己的鉴权、存储、管理接口、监控上报;业务层完全用自己的代码实现。这样做的好处是开发速度快、合规边界清晰,坏处是这些小型协议库的集群能力、生产稳定性、社区维护力度远不如 Mosquitto 和 EMQX,上线前需要自己补很多工程化能力。
4.4 方案D:全自研轻量 broker,什么时候值得做
如果你的团队有扎实的 C/Go/Java 网络编程能力,连接规模属于可控范围,且有强烈的“代码自有”诉求,那么自研一个轻量 broker 是可行的。这里说的轻量,指的是面向有限设备接入规模、单机或双机热备、不必做大规模集群,而不是再造一个 EMQX。
自研路线的实施节奏可以拆成五步走。第一步搭建 TCP 监听服务,处理连接建立和断开;第二步实现 MQTT CONNECT 报文解析,返回 CONNACK,不通过就拒绝连接;第三步实现 SUBSCRIBE 和 PUBLISH 基本转发,这里先不追求高吞吐;第四步把 QoS1、遗嘱、保留消息做进来,同时加上主题过滤和鉴权;第五步再考虑持久会话、监控指标、TLS 支持。
用 Go 写一个处理 CONNECT 报文的骨架,核心思路是这样的:
func handleConnect(conn net.Conn) error { header := make([]byte, 1) if _, err := io.ReadFull(conn, header); err != nil { return err } // 固定头首字节高四位: 报文类型 // CONNECT = 1 if header[0]>>4 != 1 { conn.Close() return errors.New("first packet is not CONNECT") } // 读取剩余长度字段,变长编码最多4字节 var remainingLength int multiplier := 1 for i := 0; i < 4; i++ { b := make([]byte, 1) if _, err := io.ReadFull(conn, b); err != nil { return err } remainingLength += int(b[0]&127) * multiplier if b[0]&128 == 0 { break } multiplier *= 128 } // 读取载荷并解析协议名、协议级别、连接标志、keepalive、clientID等 payload := make([]byte, remainingLength) if _, err := io.ReadFull(conn, payload); err != nil { return err } // 确定协议名, MQTT 3.1.1 固定是 "MQTT" if string(payload[0:4]) != "MQTT" { conn.Close() return errors.New("unsupported protocol name") } connack := []byte{0x20, 0x02, 0x00, 0x00} _, err := conn.Write(connack) return err }这个骨架只是入口,真正投产还要处理并发连接、读写缓冲区、半包粘包、心跳超时、状态存储。但好处是每行代码都是自己团队的,出了任何问题都能快速定位。
4.5 方案E:全托管服务,把运维和合规打包出去
还有一条被低估的路是全托管 MQTT 服务。设备端直接接入云厂商或专业物联网厂商提供的 MQTT 服务,不用自己维护 broker,连接管理、证书管理、消息存储、监控告警都由服务方负责。对企业用户来说,这是合规负担最小、上线速度最快的方案。
痛点在于数据归属和厂商锁定。设备产生的业务数据都流过服务方的网关,很多企业在这方面有顾虑。用之前要把数据流向、接口协议、导出能力、服务等级协议逐一确认清楚,避免业务跑大了之后发现拔不出来。
5. 常见问题与避坑实录
5.1 问题速查表,先对号入座
| 常见问题 | 判断逻辑 | 处理方向 |
|---|---|---|
| 改了 Mosquitto 源码,必须开源吗 | 是否分发了修改后的二进制/源码 | 分发则需开源修改文件;仅自用不分发,义务较轻 |
| EMQX 开源版集群免费,能直接商用吗 | Apache 2.0 允许商用 | 可以,但注意哪些功能在企业版里 |
| 基于 Apache 2.0 库改的 broker 能否闭源 | Apache 2.0 允许闭源衍生 | 可以,保留版权声明即可 |
| 自研 broker 和标准客户端互通失败 | 大概率是报文解析或会话状态问题 | 用 Wireshark 抓包抓 MQTT 报文逐字节对比 |
| MCU 上用 Paho 客户端,固件会被“传染”吗 | Paho 部分组件是 EPL/EDL | 独立模块编译静态库时做隔离,审计时说明 |
| 设备频繁掉线重连,连接堆积 | 检查心跳参数和线程模型 | 调整 Keep Alive,排查半开连接清理逻辑 |
| 消息偶发重复或丢失 | 大概率是 QoS2 状态机或会话保存逻辑缺陷 | 重点检查 PUBREC/PUBREL/PUBCOMP 状态恢复 |
5.2 我在客户现场踩过的三个坑
第一个坑是交付镜像里躺着一个 GPL 库。项目开发时引入了一个做 JSON 解析的第三方库,功能好用,人人都在用,但没人注意它的许可证。到客户验收阶段,对方要求提交依赖清单和许可证扫描报告,扫描工具直接把 GPL 相关条目标红。最后团队成员花了一周时间替换掉所有相关调用,加班加点重新出包。后来我养成了习惯,任何依赖引入前先跑一次许可证检查,这件事应该和写单元测试一样常态化。
第二个坑是拿 Mosquitto 桥接做“伪集群”。当时客户要求高可用,我们图省事把两台 Mosquitto 用桥接模式串起来,测试环境一切正常。生产环境流量上来之后,消息开始乱漂移,一个设备上报的数据在两个节点间反复转发,下游消费端出现大量重复。后来才意识到,桥接模式需要非常精细地设计路由规则和消息流向,不能指望它像集群一样自动分片。换到 EMQX 或自研方案后,这类问题才彻底解决。
第三个坑是自研 broker 的 QoS2 处理。我们早期版本为了省事,把 QoS2 降级成 QoS1 处理,结果在弱网条件下消息重复率高到无法接受。后来老老实实按协议实现四步握手,把 packet ID 和发送中的消息状态做进会话存储,设备重连后先恢复未完成流程,再恢复数据收发,重复率才降下来。协议标准写得很清楚,但实现层面很多细节只有跑过真实设备才会暴露。
5.3 避坑工具箱,这些工具能救你命
做 MQTT 协议栈开发或 broker 选型,我会建议准备几类工具。MQTTX 和 mosquitto_pub/sub 是日常功能联调必备的客户端工具,能快速验证 broker 的基础行为。Wireshark 抓包配合 mqtt 过滤表达式,可以逐包看协议细节,定位互通问题几乎离不开它。压测工具有必要准备一套,常用方式是用脚本并发地建立连接、收发消息,观察 broker 的 CPU、内存、文件描述符变化。批量创建设备连接来模拟生产规模的方案我试过,能发现很多只有高并发下才会暴露的资源泄漏问题。
许可证合规最好也工具化,FOSSA、Black Duck、Snyk 这些扫描工具都可以接入 CI 流程,每次构建自动扫描第三方依赖。企业环境里如果部署这些工具有难度,至少维护一份手动更新的依赖清单和许可证台账,做到任何依赖可追溯、可解释。
最后再分享一个经验。我自己评估 MQTT 方案时,不会先看技术指标,而会先回答三个问题:出故障时谁能最快定位并修复?许可证扫描能不能一次通过?业务从 A 方案切到 B 方案的数据迁移链路是否清晰?这三个问题想清楚了,选型方向基本就明确了。国产自研的价值是可控性和安全感,但这份安全感是用代码质量、测试覆盖和运维能力换来的,而不是靠“自己人写的”这个标签决定的。如果现在让我重新做一次选型,我会先花一天时间把上面的对比表和风险清单列完整,再决定是继续用 Mosquitto、上 EMQX 商业版,还是开始写自己的第一行协议解析代码。