# EMQX 集群设计

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

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

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

## 集群设计的关键方面

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

- **集中管理**：集群应具备集中管理的能力，这意味着可以通过单一的管理控制台监控和控制集群中的所有节点。
- **数据一致性**：集群确保所有节点对路由信息具有一致的视图。这是通过在集群中的所有节点之间复制数据来实现的。
- **易于扩展**：为减少集群管理的复杂性，添加新节点到集群应该是一个简单的过程。集群应能够自动检测新节点并将其添加到集群中。
- **集群再平衡**：集群应能以最小的操作开销检测和处理节点负载不平衡问题，并将工作负载重新分配给负载较轻的节点。这确保了即使一个或多个节点发生故障，集群也能继续运行。
- **大规模集群**：为了满足系统日益增长的需求，集群可以通过增加节点来进行扩展。这使得集群能够横向扩展，以满足系统的不断增长需求。
- **自动故障切换**：如果某个节点发生故障，集群将自动检测故障并将工作负载重新分配给剩余的节点。这确保了即使一个或多个节点发生故障，集群也能继续运行。
- **网络分区容忍度**：集群应具备容忍网络分区的能力，这意味着即使发生网络分区，集群仍能继续运行。

在接下来的章节中，我们将详细讨论集群的这些关键方面。

## 集中管理

EMQX 可以集中管理，因为集群中的所有节点都可以通过一个管理控制台进行监控和控制。这使得管理大量设备和消息变得简单。该控制台通过网页浏览器访问，并提供了一个用户友好的界面来管理集群。任何 `core` 类型的节点都可以作为管理 HTTP API 的终端（我们将在下一节讨论不同的节点类型）。

在线配置管理功能允许您在不重启节点的情况下对集群中的所有节点进行配置更改。这在需要更改集群配置（如添加或删除节点）时特别有用。

## 数据一致性

在 MQTT 消息服务器集群中，最重要的分布式数据结构是路由表，它用于存储所有主题的路由信息。路由表用于确定哪些节点应接收发布到特定主题的消息。在本节中，我们将讨论 EMQX 如何确保集群中所有节点的路由表一致性。

EMQX 集群使用完整的 ACID（原子性、一致性、隔离性、持久性）事务，确保集群中所有 `core` 节点的路由表一致性，并通过 `core` 节点到 `replica` 节点的异步复制，确保集群中所有节点的路由表最终一致性。

接下来，我们将深入探讨 EMQX 数据一致性是如何实现的。

### 数据复制通道

在 EMQX 集群中，有两个数据复制通道。

- 元数据复制，例如各个节点订阅的（通配符）主题的路由信息。
- 消息传递，例如将消息从一个节点转发到另一个节点时。

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

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

### EMQX 节点如何通信

EMQX 使用 Erlang/OTP 内置数据库 Mnesia 来存储 MQTT 会话状态。为了便于数据库和消息复制，使用了 Erlang 分布协议和自定义分布协议进行消息服务器间的远程过程调用。

数据库复制通道由“Erlang 分布”协议驱动，使每个节点既可以作为客户端，也可以作为服务器。该协议的默认监听端口号是 4370。

相反，消息传递通道使用连接池，并且每个节点默认配置监听端口号为 5370（在 Docker 容器中运行时为 5369）。这种方法不同于使用单一连接的 Erlang 分布协议。

### 路由表复制

Mnesia 集群采用全网状拓扑设计，其中集群中的每个节点相互连接并持续检查其活跃状态。

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

然而，全网状拓扑对集群规模有实际限制。对于 EMQX 5.0 之前的版本，建议将集群规模控制在 5 个节点以内。超过此数量时，垂直扩展（使用更强大的机器）是保持集群性能和稳定性的更佳选择。

在我们的基准测试环境中，我们成功实现了 [EMQX Enterprise 4.3 的 1 千万并发连接](https://www.emqx.com/en/resources/emqx-v-4-3-0-ten-million-connections-performance-test-report)。

虽然我们不要求客户报告其生产部署的详细信息，但根据与我们分享的信息，已知最大的生产集群由 7 个节点组成。

管理大型 Mnesia 集群的主要挑战之一是脑裂风险。当网络分区将节点隔离成多个子集群时，每个子集群都认为自己是唯一的活动集群。这种风险在大型集群中特别明显，因为网络开销的 N^2 复杂性会导致节点在高负载下响应变慢。此外，Erlang 分布通道中的队头阻塞可能会延迟心跳消息的发送，进一步增加脑裂风险。

在 EMQX v5 中，我们通过引入 [Mria](https://github.com/emqx/mria)（一种带有异步事务日志复制的增强版 Mnesia），大大提高了集群的可扩展性。Mria 使用了一种新的网络拓扑，包括两种类型的节点角色：`core` 和 `replicant`（有时简称为 `replica`）。

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

在 EMQX v5 集群中，`core` 节点仍然形成与旧版本相同的全网状网络。然而，`replicant` 节点仅连接到一个或多个 `core` 节点，而不相互连接。

### 核心节点和副本节点

核心节点的行为与 4.x 版本中的 Mnesia 节点相同：核心节点以全连接的方式形成集群，每个节点都可以发起事务、持有锁等。因此，EMQX v5 仍然要求核心节点在部署时尽可能可靠。

副本节点不再直接参与事务处理。它们连接到核心节点并被动地复制来自核心节点的数据更新。副本节点不允许执行任何写操作，而是将写操作交由核心节点执行。此外，由于副本节点会从核心节点复制数据，它们拥有完整的本地数据副本，以实现最高效的读操作，从而有助于减少 EMQX 路由的延迟。

由于副本节点不参与写操作，当更多副本节点加入集群时，写操作的延迟不会受到影响。这允许创建更大的 EMQX 集群。

出于性能考虑，不相关数据的复制可以分为独立的数据流，即多个相关的数据表可以分配到同一个 RLOG 分片（复制日志分片），事务从核心节点顺序复制到副本节点。但是不同的 RLOG 分片是异步的。

## 易于扩展

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

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

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

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

或者，您可以在 Dashboard 上点击一个按钮来邀请新节点加入集群。

借助丰富的管理接口，您可以轻松编写脚本进行集群管理，并将其作为 DevOps 管道的一部分。

在 EMQX v5 中，`replica` 节点被设计为无状态，因此它们可以放置在自动扩展组中，以实现更好的 DevOps 实践。

## 集群重平衡

当一个新节点加入集群时，它将从空状态开始。通过一个良好的负载均衡器，新连接的客户端可能更有机会连接到新节点。然而，现有的客户端仍然会连接到旧节点。

如果客户端在相对较短的时间内重新连接，集群可以快速达到平衡。如果客户端没有重新连接，集群可能会长时间保持不平衡状态。

为了解决这个问题，EMQX Enterprise（从版本 4.4 开始）引入了一个新功能，称为“[集群负载重平衡](../deploy/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 的“自动修复”功能可以在网络分区后自动恢复集群。当启用此功能时，在网络分区发生并恢复后，集群中的节点将按照以下步骤进行修复：

1. 节点向具有最长正常运行时间的领导节点报告分区情况。
2. 领导节点创建一个全局的网络分裂视图，并选择多数派中的一个节点作为协调员。
3. 领导节点请求协调员指挥少数派一侧的节点重新启动。
4. 请求少数派一侧的所有节点执行重启操作。

## 总结

在本文中，我们介绍了 EMQX v5 的新集群架构。我们还讨论了适用于生产环境的 MQTT 消息服务器集群的关键方面，包括可扩展性、自动故障切换、网络分区容忍等，以及 EMQX 如何帮助您实现这些目标。
