1. 为什么要在容器里搭 ActiveMQ 的 MQTT 集群
做物联网或者消息中间件这块的朋友,大概率都绕不开一个组合:ActiveMQ 做消息代理,MQTT 做设备侧协议。原因不复杂,MQTT 天生轻量、长连接友好、报文头极小,特别适合带宽紧张、设备算力有限的场景;而 ActiveMQ 作为老牌消息中间件,支持多协议接入、持久化、事务、集群,把 MQTT 挂到它上面,等于让设备侧用最省事的协议说话,后端用成熟的消息体系兜底。
但真到落地的时候,单机版 ActiveMQ 是撑不住的。设备一多,连接数上去,单节点既扛不住吞吐,也扛不住故障——节点一挂,所有设备掉线重连,消息堆积、重复消费、会话丢失全来了。所以集群是刚需,而Docker 化部署则是让这套集群能快速复现、快速迁移、快速扩缩容的关键手段。
这篇内容就是围绕“ActiveMQ + MQTT 集群搭建(docker 版本)”这个标题展开的。我会把整个方案的设计思路、镜像选型、网络规划、配置细节、启动顺序、验证方法、踩坑记录全部摊开讲。适合两类人看:一类是刚接触 ActiveMQ 和 MQTT、想快速跑通一套可用集群的开发者;另一类是已经在用单机版、正打算往集群迁移、需要一份可抄作业的落地方案的运维或后端同学。哪怕你之前没碰过 Docker 编排,跟着步骤走也能搭起来。
需要先说明一点:ActiveMQ 有两个大版本分支,经典版(5.x)和下一代 Artemis。两者对 MQTT 的支持方式、集群模型差别很大。本文聚焦经典版 ActiveMQ 5.x 的 MQTT 集群,因为它在存量项目里出现频率最高,网上资料也最杂,坑最多,最值得讲清楚。Artemis 的方案我会在选型章节里点一下差异,方便你做判断。
2. 方案整体设计与选型拆解
2.1 集群模型怎么选:静态网络 vs 动态发现
ActiveMQ 经典版做集群,核心是Network of Brokers(Broker 网络),也就是多个 Broker 通过内部连接互相转发消息,形成一个逻辑上的大 Broker。这里面又分两种拓扑:
- 静态网络(Static Network):每个 Broker 在配置文件里写死其他 Broker 的地址,互相连。优点是可控、可预测,缺点是扩缩容要改配置重启。
- 动态发现(Dynamic Discovery):用组播或者共享存储来发现彼此。优点是弹性好,缺点是组播在很多云环境和容器网络里根本走不通,排查起来极其痛苦。
在 Docker 环境里,我强烈建议用静态网络。原因很实在:容器网络的组播支持参差不齐,跨主机更是基本别想;而静态网络配合 Docker 的自定义 bridge 网络,容器名可以直接当主机名解析,配置写起来反而干净。扩缩容虽然要改配置,但集群规模在个位数到十几个 Broker 的场景下,这点维护成本完全可以接受。
所以整体架构定下来:三个 ActiveMQ Broker 容器,跑在同一个自定义 Docker 网络上,两两之间建立静态网络连接,每个 Broker 都开启 MQTT 传输监听,对外暴露各自的 MQTT 端口。
2.2 为什么是三个节点而不是两个
两个节点也能组网,但三个节点是有讲究的。ActiveMQ 的静态网络里,消息转发是“有连接就转发”,两个节点时,任何一个节点故障,整个集群就退化成单点,没有冗余意义。三个节点时,挂掉一个还剩两个,网络拓扑仍然成立,客户端重连到存活节点后依然能收到全量消息。这是最小的高可用单元。
再往上加节点,收益递减但复杂度线性上升——每个新节点要和已有所有节点建连,连接数是 n(n-1) 的量级。所以三节点是性价比最高的起点,本文就按三节点来搭,你理解了之后扩到五个、七个是同样的套路。
2.3 镜像选型:官方镜像够不够用
ActiveMQ 5.x 的官方镜像在 Docker Hub 上是有的,但直接用会遇到两个问题:一是默认配置里 MQTT 传输没开,二是集群配置需要挂载自定义的activemq.xml。所以正确姿势是基于官方镜像 + 挂载配置文件 + 环境变量调参,而不是自己从零写 Dockerfile。
这里有个细节值得说:官方镜像的启动脚本会读取ACTIVEMQ_CONFIG_*系列环境变量来覆盖部分配置,但集群相关的<networkConnector>配置它管不到,必须走挂载。所以我的做法是准备一份完整的activemq.xml模板,用环境变量在启动时做变量替换,或者干脆为每个节点准备一份独立配置。三节点规模下,后者更省心,前者更适合后续做自动化。
提示:不要用
latest标签。ActiveMQ 5.x 不同小版本之间,MQTT 的行为和网络连接器的默认参数有差异,锁定一个具体版本(比如 5.16.x 或 5.17.x 的某个确定版本)能避免“昨天还好好的今天起不来”这种玄学问题。
2.4 端口规划与网络暴露策略
每个 Broker 需要暴露的端口不止一个,理清楚很关键:
| 端口 | 用途 | 是否需要对外 |
|---|---|---|
| 61616 | OpenWire(Broker 间网络连接、JMS 客户端) | 集群内部必须通,对外可选 |
| 1883 | MQTT | 必须对外,设备侧连接 |
| 8161 | Web 控制台 | 建议对外,方便运维查看 |
| 61613 | STOMP(可选) | 按需 |
关键点在于:Broker 之间的网络连接走的是 61616(OpenWire),不是 MQTT 端口。很多人第一次配集群时会误以为 Broker 之间也用 MQTT 互连,结果配了半天连不上。记住,MQTT 是给终端设备用的接入协议,Broker 内部通信走的是 OpenWire 的tcp://连接器。
在 Docker 里,集群内部通信走自定义网络的容器名解析,不需要映射到宿主机;只有 MQTT 和 Web 控制台需要-p映射出来给外部访问。这个区分能帮你少暴露一堆端口,安全性也更好。
3. 核心配置细节与实操要点
3.1 自定义 Docker 网络的搭建
第一步永远是网络。不要用默认 bridge,默认 bridge 不支持容器名 DNS 解析,集群配置里写容器名会直接解析失败。建一个自定义网络:
docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ activemq-cluster-net指定 subnet 是个好习惯,避免和宿主机上其他 Docker 网络网段冲突。建好之后,后续所有容器都--network activemq-cluster-net接入,容器之间用容器名互访。
3.2 activemq.xml 的关键改动点
这是整个搭建的核心。官方默认的activemq.xml需要动三处:传输连接器、网络连接器、持久化配置。我逐个拆。
传输连接器部分,默认只有 OpenWire 的tcp://0.0.0.0:61616。要加 MQTT,需要补一个mqtt://0.0.0.0:1883:
<transportConnectors> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> </transportConnectors>这里的maximumConnections和maxFrameSize不是随便写的。设备侧连接数往往很大,默认值偏保守;maxFrameSize设到 100MB 是为了防止大报文被截断,如果你的设备只发小报文,可以调小到 10MB 以内,减少内存压力。
网络连接器部分,这是集群的灵魂。以节点 broker1 为例,它要连 broker2 和 broker3:
<networkConnectors> <networkConnector name="to-broker2" uri="static:(tcp://broker2:61616)" duplex="true" networkTTL="2" conduitSubscriptions="false" dynamicallyIncludedDestinations=">"/> <networkConnector name="to-broker3" uri="static:(tcp://broker3:61616)" duplex="true" networkTTL="2" conduitSubscriptions="false" dynamicallyIncludedDestinations=">"/> </networkConnectors>几个参数必须解释清楚,否则你抄了也不知道为什么:
duplex="true":双向连接。设成 true 后,broker1 连 broker2 的同时,broker2 也能通过这条连接往 broker1 发消息,不需要 broker2 再单独配一条反向连接。三节点两两互连时,这个参数能省掉一半配置。networkTTL="2":消息在网络中的最大跳数。三节点全互联的情况下,任意两点直连,TTL 设 2 足够;设太大容易造成消息在网络里绕圈,设太小又可能导致多跳转发失败。conduitSubscriptions="false":这个参数是踩坑重灾区。默认 true 时,多个消费者订阅同一队列,网络连接会把它们合并成一条“管道”订阅,导致消息在 Broker 间分配不均。设成 false 后,每个消费者的订阅独立转发,负载更均衡。代价是网络连接上的订阅数变多,但在三节点规模下完全可接受。dynamicallyIncludedDestinations=">":表示所有动态创建的 destination 都参与网络转发。>是通配符,代表全部。如果你只想转发特定队列,可以写成queue://your.queue.name。
持久化部分,默认是 KahaDB,集群场景下建议确认journal相关参数。三节点各自独立持久化,不做共享存储(共享存储是另一套 HA 方案,和网络集群是两条路,别混用)。KahaDB 的默认配置在中等负载下够用,如果消息量大,可以调journalMaxFileLength,但这是后话,先跑通再说。
3.3 每个节点的差异化配置
三个节点的activemq.xml结构完全一样,只有两处不同:brokerName和网络连接器里指向的地址。brokerName必须全局唯一,这是集群识别节点身份的依据,重名会导致连接混乱。
我的做法是准备一个模板文件,用brokerName和连接地址做占位符,启动前用脚本替换生成三份。三节点手工改也不累,但如果你打算扩到更多节点,模板化是必须的。
注意:
brokerName改了之后,KahaDB 的数据目录最好也按节点区分开,否则多个容器挂载同一个数据卷会直接损坏数据。每个节点用独立的宿主机目录挂载,比如/data/amq/broker1、/data/amq/broker2。
3.4 容器启动参数的门道
启动容器时,除了挂载配置和数据目录,还有几个参数值得关注:
docker run -d \ --name broker1 \ --network activemq-cluster-net \ --hostname broker1 \ -p 1883:1883 \ -p 8161:8161 \ -v /data/amq/broker1/activemq.xml:/opt/apache-activemq/conf/activemq.xml \ -v /data/amq/broker1/data:/opt/apache-activemq/data \ -e ACTIVEMQ_OPTS="-Xms512m -Xmx1024m" \ apache/activemq-classic:5.17.6--hostname显式指定容器主机名,和容器名保持一致,避免某些环境下主机名解析出问题。ACTIVEMQ_OPTS控制 JVM 堆大小,默认值偏小,集群场景下每个节点给到 1G 上限比较稳妥,具体看你的消息吞吐和堆积量。
启动顺序上,先起 broker1,等它完全起来(日志出现ActiveMQ Broker started)再起 broker2 和 broker3。原因是网络连接器在启动时会尝试连接对端,如果对端还没起来,会不断重试并刷大量警告日志。虽然最终能连上,但日志会很脏,排查问题时干扰很大。等第一个节点稳定后再起其余节点,日志干净,问题一目了然。
4. 完整实操流程与验证方法
4.1 从零到集群跑通的完整步骤
把前面的碎片串成一条完整流水线,按顺序执行:
- 建网络:
docker network create那条命令,确认docker network ls能看到。 - 准备目录:为三个节点各建配置目录和数据目录,权限给足(ActiveMQ 容器内以非 root 用户运行,挂载目录权限不对会启动失败)。
- 生成三份配置:基于模板替换
brokerName和网络连接地址,分别放到三个配置目录。 - 启动 broker1:用上面的
docker run命令,docker logs -f broker1观察启动日志。 - 启动 broker2、broker3:broker1 就绪后依次启动,同样观察日志。
- 验证集群连接:进任意节点的 Web 控制台,看 Connections 页面,应该能看到来自其他节点的网络连接。
- 验证 MQTT 收发:用 MQTT 客户端连 broker1 发消息,连 broker2 订阅,看能否收到。
第 6 步的验证特别重要。Web 控制台默认账号密码是 admin/admin,登录后进Connections,如果集群配对了,你会看到类型为Network的连接,对端地址是其他节点的容器 IP。看不到就说明网络连接器没生效,回去查配置和网络连通性。
4.2 MQTT 跨节点收发的验证细节
验证集群是否真正工作,光看连接不够,要实际发消息。用mosquitto_pub和mosquitto_sub最方便:
# 在 broker1 上发布 mosquitto_pub -h 127.0.0.1 -p 1883 -t "test/cluster" -m "hello from broker1" # 在 broker2 上订阅(另开终端) mosquitto_sub -h 127.0.0.1 -p 1883 -t "test/cluster" -v注意这里的端口映射:如果你把三个节点的 1883 都映射到了宿主机,那宿主机上只能有一个 1883 可用,另外两个要映射到不同端口(比如 1884、1885)。验证时分别连不同端口即可。这一步能跑通,说明消息确实从 broker1 转发到了 broker2,集群生效。
有个容易忽略的点:MQTT 的 topic 和 ActiveMQ 的 destination 映射关系。ActiveMQ 把 MQTT 的 topic 映射成内部的 Topic destination,默认是mqtt://前缀或者直接同名,具体取决于版本和配置。如果你在 Web 控制台看不到预期的 destination,别慌,先确认映射规则,再判断是不是集群问题。
4.3 持久化订阅在集群下的行为
MQTT 有个重要特性叫持久化会话(clean session = false),设备断线重连后要能收到断线期间的消息。在集群环境下,这个行为依赖 Broker 间的订阅转发。前面conduitSubscriptions="false"和dynamicallyIncludedDestinations=">"的配置,正是为了保证持久化订阅能跨节点生效。
实测下来,设备连 broker1 建立持久化订阅后断线,消息从 broker2 进来,只要网络连接正常,消息会被转发到 broker1 暂存,设备重连 broker1 后能收到。但如果设备重连到了 broker3,行为就取决于订阅信息是否同步到了 broker3——这就是集群 MQTT 最微妙的地方。经典版 ActiveMQ 的持久化订阅跨节点同步并不完美,这是它的已知局限。如果你的业务对“断线消息绝对不丢”要求极高,要么让设备固定连同一节点(用负载均衡做会话保持),要么考虑 Artemis。
5. 常见问题与排查技巧实录
5.1 集群起不来的高频原因速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 容器启动即退出 | 配置文件 XML 语法错误 | docker logs看解析异常行号 |
| 启动成功但无网络连接 | 容器名解析失败 | 进容器ping broker2测试 |
| 网络连接反复断开重连 | brokerName 重复 | 检查三份配置的 brokerName |
| MQTT 端口连不上 | 传输连接器未配置或端口未映射 | 查 activemq.xml 和-p参数 |
| 数据目录报权限错误 | 挂载目录属主不对 | chown到容器内运行用户 |
| 消息不跨节点转发 | dynamicallyIncludedDestinations 配置错误 | 确认通配符写法和 destination 类型 |
这张表是我实际排查时总结的,覆盖了八成以上的启动期问题。重点说两个最容易翻车的。
容器名解析失败:这是自定义网络没生效或者容器没接入同一网络的典型症状。进容器执行ping broker2,如果提示bad address,说明 DNS 解析不通。检查docker network inspect看三个容器是否都在同一网络下。还有一种隐蔽情况:容器启动时网络还没完全就绪,导致首次解析失败,但 ActiveMQ 的重试机制通常能自愈,如果日志里只有早期几条解析失败、后面恢复正常,可以忽略。
brokerName 重复:这个坑很阴。三个节点如果brokerName都叫localhost(默认值没改),集群表面上能连上,但消息路由会乱套,表现为消息时有时无、消费者收不到本该收到的消息。排查时进 Web 控制台看每个节点的 Broker 名称,必须三个不同。
5.2 消息重复与顺序问题
集群环境下,消息重复是常见现象,根源在于网络连接器的转发机制和消费者的重连。ActiveMQ 本身不保证跨节点的严格顺序,也不保证 exactly-once。如果你的业务不能容忍重复,消费端必须做幂等——用消息 ID 去重是最简单的做法。
顺序问题更微妙。同一个队列的消息,如果生产者在不同节点发布,消费者又在不同节点订阅,消息到达顺序无法保证。要保顺序,就得让同一业务键的消息固定走同一节点,这需要在生产端做路由,属于业务层设计,不是集群配置能解决的。这一点在方案设计阶段就要想清楚,别等上线了才发现顺序乱了。
5.3 资源与性能的实操心得
跑三节点集群,宿主机资源要给够。每个 ActiveMQ 容器 JVM 堆 1G,加上堆外内存和系统开销,单容器实际占用 1.5G 左右,三节点就是 4.5G,宿主机至少留 8G 内存才从容。磁盘方面,KahaDB 的 journal 文件会预分配,每个节点预留几个 G 的空间,别让磁盘满了导致 Broker 挂起。
还有一个实测经验:网络连接器的数量会影响启动速度。三节点全互联,每个节点两条网络连接,启动时这些连接要逐一建立,节点越多启动越慢。如果你扩到五节点以上,启动时耐心等,别以为卡死了就重启,反复重启反而会让连接状态更乱。
提示:生产环境务必把 Web 控制台的默认密码改掉,并且不要把 8161 端口直接暴露到公网。集群内部通信的 61616 端口也不要对外映射,只在 Docker 网络内可达即可。
5.4 日志排查的正确姿势
ActiveMQ 的日志分几类:启动日志、连接日志、消息转发日志。排查集群问题时,重点看INFO级别的网络连接相关日志,搜索关键词NetworkConnector、bridge、connection。如果看到Network connection between ... failed,基本就是网络或配置问题;如果看到bridge to ... established,说明连接成功。
日志级别可以在log4j2.properties里调,排查阶段把网络相关的 logger 调到DEBUG,能看到每条消息的转发细节,非常有用。但记得排查完调回去,DEBUG 级别日志量巨大,长期开着会拖慢性能、占满磁盘。
6. 这套方案能走多远,以及什么时候该换路
三节点静态网络集群,能覆盖中小规模的物联网接入场景——几千到几万设备连接、每秒几百到几千条消息,这套方案是稳的。它的优势在于部署简单、配置透明、排查路径清晰,Docker 化之后迁移和复现成本极低。
但它的天花板也明确:静态配置意味着扩缩容要改配置重启;经典版的持久化订阅跨节点同步不完美;网络连接器的消息转发在节点数增多后开销上升。当你的设备规模上到十万级、或者对消息可靠性要求到了“断线消息一条不能丢”的程度,就该认真评估 Artemis 了。Artemis 的集群模型更现代,支持更灵活的地址路由和更可靠的订阅同步,代价是配置复杂度和学习曲线都更高。
我的建议是:先用这套 Docker 版经典集群把业务跑起来,把 MQTT 的 topic 设计、QoS 等级、持久化策略这些业务层的东西打磨清楚。等业务量真的顶到这套架构的边界了,再迁移到 Artemis,那时候你对 MQTT 和消息中间件的理解已经足够支撑你做正确的选型判断。架构演进是跟着业务走的,不是一步到位堆最复杂的方案。
最后分享一个我在实际搭建中养成的小习惯:每次改完配置重启集群前,先用docker exec进容器手动跑一遍配置校验,确认 XML 能正常解析再重启。这个动作花不了半分钟,但能避免“改错一个标签导致整个集群起不来、然后花半小时排查”的窘境。集群这东西,稳比快重要。