# 对 EMQX 集群执行滚动更新

使用 EMQX Operator 3.0 对 EMQX 集群执行平滑滚动更新。

## 前提条件

- [安装 EMQX Operator 3.0](../getting-started.md)，并配置 `kubectl` 以访问 Kubernetes 集群。
- 确保 Kubernetes 环境可以为 `LoadBalancer` Service 分配外部地址。
- 准备可支持至少 3,000 个并发连接的有效 EMQX Enterprise 许可证，并将本示例中的许可证占位符替换为许可证密钥。详情请参见[管理许可证](./configure-emqx-license.md)。
- 安装 `jq`，用于查看节点疏散状态。
- 安装 [MQTTX CLI](https://mqttx.app/cli)，用于建立测试连接。
- 对于 Core-Replicant 集群，请至少配置两个 Core 副本。EMQX Operator 不接受 Core 副本数少于两个的 Core-Replicant 配置。

## 滚动更新的工作原理

如果对 EMQX 自定义资源的修改会改变 Pod 模板，例如修改镜像、镜像拉取策略、资源请求、Core 模板或 Replicant 模板，EMQX Operator 会按照以下阶段执行滚动更新：

1. **更新 Core 节点：** Operator 会在同一个 StatefulSet 中逐个更新 Core Pod。Operator 使用目标模板重新创建每个 Pod，并等待该 Pod 就绪后再继续更新。至少有一个更新后的 Core 节点就绪后，才能开始更新 Replicant 节点。
2. **更新 Replicant 节点：** Operator 采用类似 Deployment 的滚动更新方式，最多按照 `maxSurge` 指定的数量创建更新后的 Replicant Pod，同时最多按照 `maxUnavailable` 指定的数量疏散上一修订版本的 Replicant Pod。这两个参数用于控制滚动更新速度和可用性。在该修订版本的 Replicant Pod 迁移完成之前，Operator 会保留上一修订版本的至少一个 Core 节点，确保迁移期间每个修订版本都有可用的 Core 节点。
3. **完成滚动更新：** Replicant Pod 迁移完成后，Operator 会继续更新其余 Core 节点。当所有目标 Pod 均已就绪，且不再存在上一修订版本的 Replicant Pod 时，滚动更新完成。

默认情况下，Operator 会在移除 Pod 前按照配置的速率疏散 MQTT 连接和会话。如需禁用节点疏散，请将 `.spec.updateStrategy.evacuationStrategy.type` 设置为 `Disabled`。

以下步骤通过修改 Pod 模板中的注解来演示此机制，不会升级 EMQX。如需升级 EMQX，请更新 `.spec.image`；Operator 会使用相同的滚动更新机制。

## 配置更新策略

1. 创建一个 `apps.emqx.io/v3beta1` EMQX CR，并配置更新策略。

  ```yaml
  apiVersion: apps.emqx.io/v3beta1
  kind: EMQX
  metadata:
    name: emqx
  spec:
    image: emqx/emqx:6.3.2
    config:
      roots:
        license:
          key: "..."
    updateStrategy:
      type: RollingUpdate
      evacuationStrategy:
        # MQTT 客户端疏散速率，单位为连接数/秒：
        connectionEvictionRate: 1000
        # MQTT 会话疏散速率，单位为会话数/秒：
        sessionEvictionRate: 1000
        # 删除 Pod 前的等待时间：
        waitTakeover: 10
      replicants:
        maxUnavailable: 1
        maxSurge: 1
    coreTemplate:
      spec:
        replicas: 2
    replicantTemplate:
      spec:
        replicas: 3
    listenersServiceTemplate:
      spec:
        type: LoadBalancer
  ```

  ::: warning

  `maxUnavailable` 和 `maxSurge` 不能同时为 `0`。如果 `maxUnavailable` 为 `100%`，则 `maxSurge` 必须大于 `0`。有关字段定义、默认值和校验规则，参见 [ReplicantsUpdateStrategy](../reference/v3beta1-reference.md#replicantsupdatestrategy)。

  :::

2. 将以上内容保存为 `emqx-update.yaml`，并使用 `kubectl apply` 部署：

  ```bash
  $ kubectl apply -f emqx-update.yaml
  emqx.apps.emqx.io/emqx created
  ```

3. 检查 EMQX 集群状态。

  确保 `STATUS` 为 `Ready`。此过程可能需要一些时间。

  ```bash
  $ kubectl get emqx
  NAME      STATUS   AGE
  emqx      Ready    8m33s
  ```

## 建立测试连接

使用 MQTTX CLI 建立 MQTT 连接，以便在滚动更新期间观察连接疏散过程。MQTTX CLI 支持自动重连。

获取 `emqx-listeners` Service 的外部地址。以下命令同时适用于发布 IP 地址或主机名的负载均衡器。

```bash
export EMQX_HOST="$(kubectl get service emqx-listeners -o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}')"
```

确认 `EMQX_HOST` 中已包含地址后，使用 MQTTX 向端口 `1883` 上的默认 TCP 监听器建立 3,000 个连接：

```bash
mqttx bench conn -h "${EMQX_HOST}" -p 1883 -c 3000
[10:05:21 AM] › ℹ  Start the connect benchmarking, connections: 3000, req interval: 10ms
✔  success   [3000/3000] - Connected
[10:06:13 AM] › ℹ  Done, total time: 31.113s
```

## 触发更新

1. 更新 Core 和 Replicant Pod 模板中的注解，以触发滚动更新。命令将时间戳与随机后缀组合，以降低重复使用相同注解值的可能性。

  ```bash
  ROLLOUT_ID="$(date +%s)-${RANDOM}"

  kubectl patch emqx emqx --type=merge -p \
    "{\"spec\":{\"coreTemplate\":{\"metadata\":{\"annotations\":{\"docs.emqx.com/rollout-id\":\"${ROLLOUT_ID}\"}}},\"replicantTemplate\":{\"metadata\":{\"annotations\":{\"docs.emqx.com/rollout-id\":\"${ROLLOUT_ID}\"}}}}}"
  ```

  预期输出：

  ```text
  emqx.apps.emqx.io/emqx patched
  ```

2. 检查更新进度。

  ```bash
  $ kubectl get emqx emqx -o json | jq ".status.nodeEvacuations"
  [
    {
      "nodeName": "emqx@10.244.4.56",
      "initialConnections": 33,
      "initialSessions": 0,
      "connectionEvictionRate": 200,
      "sessionEvictionRate": 200,
      "state": "waiting_takeover",
      "sessionRecipients": [
        "emqx@10.244.4.57",
        "emqx@10.244.4.58"
      ]
    }
  ]
  ```

  | 字段                     | 说明                                             |
  |--------------------------|--------------------------------------------------|
  | `nodeName`               | 当前正在疏散的节点。                             |
  | `state`                  | 节点疏散阶段。                                   |
  | `sessionRecipients`      | MQTT 会话的接收节点。                            |
  | `sessionEvictionRate`    | 此节点的 MQTT 会话疏散速率（会话数/秒）。        |
  | `connectionEvictionRate` | 此节点的 MQTT 连接疏散速率（连接数/秒）。        |
  | `initialSessions`        | 此节点的初始会话数。                             |
  | `initialConnections`     | 此节点的初始连接数。                             |

  可查看对应的 [EMQX 节点状态](../reference/v3beta1-reference.md#emqxnode)中的 `connections` 和 `sessions` 计数器，估算节点疏散进度。

3. 等待更新完成。

  ```bash
  $ kubectl get emqx
  NAME      STATUS   AGE
  emqx      Ready    8m33s
  ```

  确保 `STATUS` 为 `Ready`。更新所需时间取决于 MQTT 客户端和会话的数量。

  更新完成后，可使用 `kubectl get pods` 验证所有 Pod 是否都在运行预期模板。

## 滚动更新示意图

下图以 3,000 个连接为例，展示 Replicant 滚动更新期间一种可能的连接分布。该图并非基于 Grafana 实测数据。

<svg viewBox="0 0 920 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Replicant 滚动更新期间的连接分布示意图">
  <rect width="920" height="360" fill="#111827"/>
  <rect x="58" y="34" width="680" height="266" fill="#121a24" stroke="#263241"/>
  <g stroke="#263241" stroke-width="1">
    <path d="M58 60H738M58 108H738M58 156H738M58 204H738M58 252H738M58 300H738"/>
    <path d="M80 34V300M160 34V300M260 34V300M360 34V300M460 34V300M560 34V300M660 34V300M738 34V300"/>
  </g>
  <g fill="#9ca3af" font-family="sans-serif" font-size="12">
    <text x="25" y="304">0</text>
    <text x="16" y="256">600</text>
    <text x="10" y="208">1,200</text>
    <text x="10" y="160">1,800</text>
    <text x="10" y="112">2,400</text>
    <text x="10" y="64">3,000</text>
    <text x="350" y="322">更新进度</text>
  </g>
  <g fill="none" stroke-linecap="round" stroke-linejoin="round">
    <path d="M80 300 C105 300 135 180 160 62 C210 58 270 61 330 60 C390 61 445 59 505 60 C565 62 630 59 700 60" stroke="#73bf69" stroke-width="2"/>
    <path d="M160 217 L300 216 C330 218 355 254 382 300" stroke="#e24d42" stroke-width="1.2"/>
    <path d="M160 221 L400 222 C430 222 455 257 482 300" stroke="#8f7ee7" stroke-width="1.2"/>
    <path d="M160 225 L500 224 C530 225 565 259 600 300" stroke="#5794f2" stroke-width="1.2"/>
    <path d="M280 300 C315 300 350 246 382 218 L400 218 C430 218 455 205 482 200 L500 200 C535 200 570 190 600 180 L700 180" stroke="#f2cc0c" stroke-width="1.2"/>
    <path d="M380 300 C415 300 450 260 482 248 L500 248 C535 248 570 225 600 221 L700 221" stroke="#ff9830" stroke-width="1.2"/>
    <path d="M500 300 C535 300 570 283 600 264 L700 264" stroke="#56a64b" stroke-width="1.2"/>
  </g>
  <g font-family="sans-serif" font-size="9">
    <rect x="758" y="46" width="144" height="166" rx="4" fill="#101923" stroke="#263241"/>
    <circle cx="768" cy="66" r="3" fill="#73bf69"/><text x="776" y="69" fill="#d1d5db">总计 3,000</text>
    <circle cx="768" cy="88" r="3" fill="#e24d42"/><text x="776" y="91" fill="#d1d5db">emqx-replicant-86f864f9-0</text>
    <circle cx="768" cy="110" r="3" fill="#8f7ee7"/><text x="776" y="113" fill="#d1d5db">emqx-replicant-86f864f9-1</text>
    <circle cx="768" cy="132" r="3" fill="#5794f2"/><text x="776" y="135" fill="#d1d5db">emqx-replicant-86f864f9-2</text>
    <circle cx="768" cy="154" r="3" fill="#f2cc0c"/><text x="776" y="157" fill="#d1d5db">emqx-replicant-648c45c7-0</text>
    <circle cx="768" cy="176" r="3" fill="#ff9830"/><text x="776" y="179" fill="#d1d5db">emqx-replicant-648c45c7-1</text>
    <circle cx="768" cy="198" r="3" fill="#56a64b"/><text x="776" y="201" fill="#d1d5db">emqx-replicant-648c45c7-2</text>
  </g>
  <g fill="#d1d5db" font-family="sans-serif">
    <text x="58" y="24" font-size="14">Replicant 滚动更新示意图，maxSurge = 1，maxUnavailable = 1</text>
  </g>
</svg>

| 标签/前缀                   | 说明                                           |
|-----------------------------|------------------------------------------------|
| 总计                        | 示例连接总数，在图中显示为最上方的曲线。       |
| `emqx-replicant-86f864f9`   | 原修订版本 Replicant Pod 集合的示例名称前缀。  |
| `emqx-replicant-648c45c7`   | 更新后 Replicant Pod 集合的示例名称前缀。      |

更新 Replicant 节点时，从正在疏散的 Pod 断开连接的客户端可能会重新连接到其他仍在提供服务的 Pod，包括更新后的 Pod。图中的曲线仅用于说明这种连接重新分布，并非实测数据。实际连接数可能发生波动，具体取决于节点疏散速率、集群可用容量和客户端重连行为。
