2. EMQX节点与集群基本概念

在动手部署 EMQX 集群之前,必须先厘清一组基础术语:节点、集群、Erlang 分布式、Mnesia、集群路由、会话与连接。它们不是彼此孤立的定义,而是一条从“单机进程”到“多机协同”的依赖链——节点是集群的最小单元,Erlang 分布式是节点互相发现与通信的底座,Mnesia 负责保存集群自身的元数据,集群路由决定消息如何在节点间找到目标,会话与连接则描述客户端与集群之间的实际状态。理解这条链,才能看懂后续的集群组建、脑裂处理与高可用设计。

节点:EMQX 的运行实例

一个 EMQX 节点,就是一台(或一个容器中)运行起来的 EMQX 服务进程。它由 Erlang/OTP 虚拟机(BEAM)承载,拥有独立的监听端口、独立的进程树和独立的内存空间。节点在启动时会被赋予一个节点名(Node Name),格式为 name@host,例如 emqx@192.168.1.11 或 emqx@node1.example.com。这个名称是节点在整个 Erlang 分布式系统中的唯一身份标识,也是其他节点找到它的依据。

节点名中的 host 部分必须能被其他节点解析到,通常使用 IP 地址或可解析的主机名。如果使用短名称(-sname),则要求所有节点处于同一 DNS 域内;生产环境更推荐使用长名称(-name)配合完整域名或 IP。节点启动后,可以通过 emqx ctl status 查看自身状态,通过 emqx ctl cluster status 查看它当前所属的集群视图。

集群:多个节点的逻辑整体

集群是由两个或更多 EMQX 节点组成的逻辑整体。对客户端而言,集群应当表现为一个统一的 MQTT 接入点:无论连到哪个节点,都能订阅到其他节点上的主题,也能把消息发布给连接在其他节点上的订阅者。这种“多节点、单视图”的能力,正是集群存在的意义。

EMQX 集群有两种典型组网方式:手动加入与自动发现。手动方式通过 emqx ctl cluster join emqx@192.168.1.12 显式指定目标节点;自动发现则借助静态节点列表、DNS、etcd、Kubernetes 等机制,让节点在启动时自动找到彼此。无论哪种方式,最终都要形成一张全互联的 Erlang 分布式网络。

Erlang 分布式:节点互联的底座

EMQX 的集群能力并非自己从零实现,而是直接构建在 Erlang/OTP 的分布式机制之上。当两个节点使用相同的 Cookie(一个共享密钥)并成功建立 TCP 连接后,它们就进入了同一个 Erlang 分布式系统。此后,一个节点可以像调用本地函数一样调用另一个节点上的函数,这种能力称为远程过程调用(RPC)。

Erlang 分布式还提供了节点监控机制:节点之间会互相监视,一旦某个节点宕机或网络断开,其他节点会收到 nodedown 通知,从而触发集群视图更新。EMQX 的集群管理、路由同步、会话迁移都建立在这套机制之上。因此,排查集群问题时,第一步往往是确认节点之间能否互相 ping 通、Cookie 是否一致、节点名解析是否正确。

Mnesia:集群元数据的存储

Mnesia 是 Erlang 自带的分布式数据库,EMQX 用它保存集群级别的元数据,例如集群节点列表、路由表、部分配置与 ACL 规则等。Mnesia 表可以设置为在多个节点上复制,从而在某个节点故障时仍能从其他节点读取数据。

需要特别区分的是:Mnesia 保存的是集群自身的状态,而不是 MQTT 消息本身。普通 MQTT 消息默认不落盘,会话状态则根据会话类型(是否持久会话)决定是否保存。理解这一点,有助于避免把“集群元数据高可用”与“消息不丢失”混为一谈。

集群路由:消息如何找到订阅者

当客户端 A 连接到节点 N1,订阅了主题 sensor/room1/temp;客户端 B 连接到节点 N2,向同一主题发布消息。N2 必须知道“这个主题的订阅者在 N1 上”,才能把消息转发过去。这张“主题 → 节点”的映射关系,就是集群路由表。

EMQX 会在集群内同步路由信息:每个节点把自己承载的订阅关系广播给其他节点,各节点据此维护一份全局路由视图。消息到达任一节点后,该节点查询本地路由表,若发现目标订阅者在其他节点,就通过 Erlang 分布式通道转发。路由表是最终一致的,节点加入或退出时会触发增量同步。因此,集群规模越大、订阅越频繁,路由同步的开销也越值得关注。

连接与会话:客户端状态的两个层次

连接(Connection)指客户端与某个 EMQX 节点之间的一条 MQTT 网络链路,它绑定在具体节点上,具有 TCP 连接、Keep Alive 心跳、客户端 ID 等属性。连接是“物理”的,一旦该节点故障,这条连接就会断开。

会话(Session)则是 MQTT 协议层面的状态集合,包括订阅列表、未确认的 QoS 1/2 消息、离线消息队列等。会话可以独立于连接存在:当 Clean Session = false(MQTT 3.1.1)或会话过期时间大于 0(MQTT 5.0)时,客户端断开后会话仍被保留,待其重新连接后恢复。EMQX 支持会话在集群内迁移,使得客户端重连到另一节点时仍能恢复原有订阅与未收消息。

记忆要点:连接是“连在哪个节点上”,会话是“订阅和消息状态是什么”。连接随节点故障而断,会话则可通过持久化与迁移在集群中延续。
EMQX 节点与集群核心概念关系图 客户端 A 订阅 sensor/room1/temp 客户端 B 发布 sensor/room1/temp 节点 N1 emqx@192.168.1.11 连接 Connection TCP / KeepAlive / ClientID 会话 Session 订阅列表 / 离线消息 / QoS 状态 Mnesia 元数据 节点列表 / 路由表 / ACL 节点 N2 emqx@192.168.1.12 连接 Connection TCP / KeepAlive / ClientID 会话 Session 订阅列表 / 离线消息 / QoS 状态 Mnesia 元数据 节点列表 / 路由表 / ACL MQTT 连接 MQTT 连接 Erlang 分布式 RPC / nodedown 监控 集群路由转发:N2 查路由表 → 消息发往 N1 连接绑定在具体节点,节点故障即断开; 会话可持久化并在集群内迁移,重连后可恢复; Erlang 分布式提供节点互联与故障通知; Mnesia保存集群元数据,不保存普通消息。

概念之间的依赖关系

把上述概念串起来看:节点是运行实例,多个节点通过 Erlang 分布式互联形成集群;集群用 Mnesia 保存节点列表与路由表等元数据;集群路由依据路由表把消息从发布节点转发到订阅节点;连接把客户端锚定到某个节点,会话则记录订阅与消息状态,并可在节点间迁移。任何一环出问题,都会表现为“客户端连得上但收不到消息”“集群状态不一致”或“重连后订阅丢失”等具体故障。

一个可观察的验证示例

假设在两台主机上分别启动 EMQX,节点名分别为 emqx@192.168.1.11 与 emqx@192.168.1.12,Cookie 均设置为 emqxsecret。在 N2 上执行加入命令:

# 在 N2 上执行,加入 N1
emqx ctl cluster join emqx@192.168.1.11

# 查看集群状态
emqx ctl cluster status
# 预期输出类似:
# Cluster status: #{running_nodes =>
#   ['emqx@192.168.1.11','emqx@192.168.1.12'],
#   stopped_nodes => []}

随后用两个 MQTT 客户端验证路由:客户端 A 连到 N1 并订阅 sensor/room1/temp,客户端 B 连到 N2 并向该主题发布一条消息。若 A 能收到消息,说明 N2 的路由表已包含 N1 上的订阅关系,集群路由工作正常。此时若在 N1 上执行 emqx ctl routes list,可以看到该主题对应的节点条目;若把 N1 停止,再执行 emqx ctl cluster status,会看到 N1 进入 stopped_nodes,这正是 Erlang 分布式 nodedown 通知在集群视图上的体现。

常见误区:把 Mnesia 当成消息存储。Mnesia 只保存集群元数据;普通 MQTT 消息默认不持久化,持久会话的离线消息才与存储相关。排查“消息丢失”时,应先确认会话类型与 QoS,而不是只盯着 Mnesia。

掌握这些术语及其关系后,后续的集群组建、节点发现、脑裂自动恢复、会话迁移与高可用部署,都将围绕“节点—分布式—Mnesia—路由—连接/会话”这条主线展开。理解每个概念在链路中的位置,才能在出现异常时快速定位是互联层、元数据层还是路由/会话层的问题。