# EMQX 集群设计

MQTT 是一种有状态协议，这要求消息服务器必须维护每个 MQTT 会话的状态信息，包括已订阅的主题和未完成的消息传输。在集群化 MQTT 消息服务器时，一个主要的挑战是确保这些状态在所有集群节点之间能够高效且可靠地同步和复制。

EMQX 是一个高度可扩展且具备容错能力的 MQTT 消息服务器，能够在多节点集群模式下运行。集群化的 EMQX 提高了物联网消息系统的可扩展性、可用性、可靠性和管理性，因此对于大型或关键业务应用来说，集群化是推荐的方式。本页面将探讨为何需要集群化 MQTT 消息服务器以及 EMQX 如何实现这一点，从而支持一个集群内数百万个不同的通配符订阅者。

有关创建和运行 EMQX 集群的详细说明，请参阅[分布式集群介绍](../cluster/introduction.md)。

## 集群设计的关键方面

在设计集群时，需要考虑多个关键因素，这些因素通常决定了集群的成功与否。简要概述如下：

* **集中管理**：集群应具备集中管理的能力，即可以通过单一的管理控制台监控和控制集群中的所有节点。

* **数据一致性**：集群确保所有节点对路由信息具有一致的视图，这是通过在集群中的所有节点之间复制数据来实现的。

* **易于扩展**：为减少集群管理的复杂性，添加新节点到集群应该是一个简单的过程。集群应能够自动检测新节点并将其加入集群。

* **集群再平衡**：集群应能以最小的操作开销检测和处理节点负载不平衡问题，并将工作负载重新分配给负载较轻的节点。这确保了即使一个或多个节点发生故障，集群也能继续运行。

* **大规模集群**：为了满足系统日益增长的需求，集群可以通过增加节点来进行扩展，从而实现横向扩展。

* **自动故障切换**：如果某个节点发生故障，集群将自动检测故障并将工作负载重新分配给剩余的节点，确保集群持续运行。

* **网络分区容忍**：集群应具备容忍网络分区的能力，即使发生网络分区，集群仍能继续运行。

EMQX 通过多种方式确保以最高效的方法实现上述目标。以下章节将对集群的各个关键方面进行详细介绍。

## 数据复制通道

为便于元数据和消息的复制，EMQX 使用 Erlang 分布协议和自定义分布协议进行节点间的远程过程调用。在 EMQX 集群中，有两个数据复制通道：

- 元数据复制，例如各节点订阅的（通配符）主题的路由信息。该通道由"Erlang 分布"协议驱动，使每个节点既可以作为客户端，也可以作为服务器。该协议的默认监听端口号为 4370。

- 消息传递，例如将消息从一个节点转发到另一个节点时。消息传递通道使用连接池，每个节点默认监听端口号为 5370（在 Docker 容器中运行时为 5369）。这与使用单一连接的 Erlang 分布协议不同。

下图展示了包含发布-订阅流程的两个数据复制通道。虚线连接节点表示元数据复制，实线箭头表示消息传递通道。

<img src="./assets/clustering.png" alt="Data replication channels" style="zoom: 50%;" />

## 内置数据库

EMQX 将不同类型的内部数据存储在以下两个内置数据库管理系统之一中：

- *Mria*：一个轻量级内存数据库，用于读密集型工作负载，例如路由表和运行时配置。在 CAP 定理的框架下，该数据库以*可用性*为设计目标。

- *持久化存储（DS）*：一个基于磁盘的流式数据库，用于写密集型工作负载和大量数据，例如发送到[持久化会话](../../guides/durability/durability_introduction.md)的消息和[消息队列](../message-queue/message-queue-concept.md)。该数据库以*一致性*为设计目标。

这两个数据库管理系统的特性差异显著，在网络分区时提供不同的保障。本文重点介绍 *Mria* 及 EMQX 在不涉及持久化特性（该特性为可选功能）时的行为。有关持久化存储的详细信息，请参阅[持久化存储](./durable-storage.md)。

*Mria* 表可进一步分为两类：

- *Regular 表*：表内容在全局范围内保持一致。大多数 Mria 表属于此类。

- *Merge 表*：一种特殊类型的表，其中每条记录归属于特定的 EMQX 节点。只有拥有该记录的节点可以修改它，对其他节点来说该记录是只读的。

### 节点角色：Core 与 Replicant

Mria 采用混合网络拓扑，包含两种节点角色：`core`（核心节点）和 `replicant`（副本节点）。

<img src="./assets/mria-cluster.png" alt="Mria Cluster" style="zoom: 40%;" />

每个 EMQX 集群至少需要一个 Core 节点，Replicant 节点数量不限。

Core 节点是 Mria 的骨干，负责协调对 *Regular 表*的更新。此类表的更新会在所有 Core 节点之间同步复制。由于协调操作存在一定开销，建议将 Core 节点数量控制在满足数据冗余要求的最小范围内，其余节点分配 Replicant 角色。集群中 Core 节点的典型数量为 3 个。

Replicant 节点不直接参与事务处理。它们连接到 Core 节点，被动地复制来自 Core 节点的数据更新。Replicant 节点不允许执行任何写操作，写请求会被转发至 Core 节点执行。此外，由于 Replicant 节点会从 Core 节点复制数据，它们拥有完整的本地数据副本，可实现最高效的读操作，从而降低 EMQX 中各类数据库查询的延迟。

由于 Replicant 节点不参与写操作，随着更多 Replicant 节点加入集群，写操作的延迟不会受到影响。这使得构建拥有数十个 Replicant 节点的大型 EMQX 集群成为可能。

出于性能考虑，数据复制被划分为独立的数据流：多个相关的数据表可以分配到同一个 RLOG Shard（复制日志分片），事务从 Core 节点顺序复制到 Replicant 节点。不同的 RLOG Shard 之间相互独立。

### Merge 表

Merge 表是一种特殊类型的 Mria 表，其中每条记录可以明确地与特定的 EMQX 节点关联。EMQX 路由表就是一个典型示例。路由表是 MQTT 消息服务器中最重要的分布式数据结构，用于存储所有主题的路由信息，以确定哪些节点应接收发布到特定主题的消息。

与 Regular 表的行为不同，每个 EMQX 节点（无论是 Core 还是 Replicant）都可以直接更新自己的记录，无需与其他节点协调。更新随后异步复制到集群中的其余节点。从某种意义上说，每个节点对 Merge 表既充当 Core 又充当 Replicant。

这种设计具有以下优势：

- 降低写延迟
- 减轻 Core 节点的负载
- 提升分区容忍性：即使在完全分区的网络中，也能保证每个节点至少保留自己的路由信息。

当网络分区愈合时，各节点会合并其路由表内容，这也是该表类型名称的由来。

## 集中管理

EMQX 支持集中管理，集群中的所有节点均可通过单一管理控制台进行监控和控制，便于管理大量设备和消息。该控制台通过网页浏览器访问，提供用户友好的集群管理界面。任何 `core` 类型的节点均可作为管理 HTTP API 的接入点。

在线配置管理功能支持在不重启节点的情况下对集群中的所有节点进行配置变更，在需要添加或删除节点等集群配置更新场景中尤为实用。

## 易于扩展

EMQX 设计上易于横向扩展。您可以随时通过 CLI、API 或 Dashboard 向集群添加或删除节点。

例如，向集群添加新节点只需执行如下命令：

```bash
emqx ctl cluster join emqx@node1.my.net
```

其中 `emqx@node1.my.net` 是集群中的某个节点。

您也可以在 Dashboard 上点击按钮邀请新节点加入集群。

借助丰富的管理接口，您可以轻松编写脚本进行集群管理，并将其纳入 DevOps 流程。

在 EMQX v5 中，`replica` 节点被设计为无状态，因此可以将其部署在自动扩缩容组中，以更好地支持 DevOps 实践。

## 集群再平衡

当新节点加入集群时，它将从空状态开始运行。借助合适的负载均衡器，新连接的客户端有更大概率连接到新节点，但现有客户端仍会保持连接到原有节点。

如果客户端在较短时间内重新连接，集群可以快速恢复平衡。如果客户端长期不重连，集群可能长时间处于不平衡状态。

为解决这一问题，EMQX（自 4.4 版本起）引入了[集群负载再平衡](../../guides/cluster/rebalancing.md)功能。该功能通过将会话从过载节点迁移到负载较轻的节点，实现集群负载的自动再平衡。

"再平衡"的一种极端形式是"疏散"，即将指定节点上的所有会话全部迁移出去，适用于需要从集群中移除某个节点的场景。

## 集群规模

在数百万并发连接的规模下，横向扩展是唯一可行的方案，因为没有单台机器能够承载如此数量的连接。

在 EMQX v5 中，`core`-`replica` 集群架构使集群能够扩展到更大的规模。

在我们的基准测试中，我们在一个由 23 个节点组成的集群中测试了 5000 万发布者加 5000 万通配符订阅者的场景。详情请参阅我们的[博客文章](https://www.emqx.com/zh/blog/reaching-100m-mqtt-connections-with-emqx-5-0)。

为什么选择通配符？因为通配符订阅是衡量 MQTT 消息服务器集群可扩展性的黄金标准，它将底层数据结构和算法的能力挑战到极限。

## 自动故障切换

MQTT 协议规范中没有会话亲和性的概念，这意味着客户端可以连接到集群中的任意节点，并仍然能够接收其订阅主题的消息。MQTT 也没有服务发现机制，因此客户端需要提前知道集群节点的地址。这通常要求客户端配置包含所有集群节点地址的列表，或者更好的方式是使用负载均衡器将客户端路由到合适的节点。

EMQX 设计上支持与集群前端的负载均衡器配合使用。通过健康检查接入点，负载均衡器可以检测集群中各节点的健康状态，并将客户端路由到合适的节点。

借助 Erlang 的节点监控机制，EMQX 节点能够相互监控健康状态，并自动将不健康的节点从集群中移除。

## 网络分区容错

当网络分区发生时，集群可能分裂为多个孤立的子集群，每个子集群都认为自己是唯一的活动集群，这被称为"脑裂"问题。生产环境中的集群应能够自动从网络分区中恢复。

EMQX 的 autoheal 功能可以在网络分区后自动恢复集群。启用该功能后，在网络分区发生并恢复时，集群中的节点将按以下步骤进行修复。

Core 节点和 Replicant 节点的恢复流程有所不同。

### Core 节点恢复

1. 节点向正常运行时间最长的领导节点报告分区情况。
1. 领导节点创建全局网络分裂视图，并从多数派中选择一个 Core 节点作为协调者。
1. 领导节点请求协调者指令少数派一侧的 Core 节点重启。
1. 少数派 Core 节点以多数派分区的 Mria 表内容替换自身的表内容。

### Replicant 节点恢复

Replicant 节点在网络分区修复过程中不会重启，从而保留客户端连接。

它们将执行以下步骤：

1. 少数派分区中的 Replicant 节点重新初始化其 *Regular* Mria 表的副本。
1. 路由表内容进行合并：多数派分区中的节点与少数派分区重新建立路由，反之亦然。
1. 客户端在全局会话注册表中重新建立存在记录。

有关分区恢复过程中产生的日志消息与告警，请参阅 [Mria 日志与告警](../../guides/observability/mria-alarms.md)。

## 总结

本文介绍了 EMQX 的集群架构，以及生产级 MQTT 消息服务器集群的关键方面，包括可扩展性、自动故障切换、网络分区容忍等，以及 EMQX 如何帮助您实现这些目标。
