## 模块 1：讨论与环境准备

+ 本模块将首先讨论在 SQL 层缩容时应用开发的最佳实践，请认真阅读相关内容。
+ 我们还会为实验做一些准备工作，包括扩容一台 TiDB Server（以便后续可以缩容），以及导入一些数据。

### 步骤 1. 讨论：SQL 层缩容时的应用开发最佳实践

> [!TIP]
> 本模块讨论的内容仅适用于 SQL 层的缩容，不适用于 TiKV 或 PD 的缩容。TiKV 或 PD 的缩容对应用是透明的，原因有两点：（1）应用不会直接与 TiKV 或 PD 通信，网络负载均衡器和 TiProxy 也不会与 TiKV 和 PD 通信；（2）TiDB 与 PD/TiKV 之间的连接是 gRPC 协议。

+ 通常，应用与 TiDB 通信有三种方式：
  + 通过 TiProxy
  + 通过网络负载均衡器
  + 直接连接到某一台 TiDB 实例（生产环境中不推荐如此操作）

+ TiProxy 是 TiDB 集群的可选组件。TiProxy 提供连接迁移、故障转移、服务发现和快速部署等功能。如果您使用 TiProxy，SQL 层的缩容对应用也是透明的，可以避免下面讨论的所有复杂性。

+ 如果应用通过网络负载均衡器或直接连接 TiDB，TiDB 的缩容过程可能对应用不是完全透明的。此时需要遵循本模块讨论的应用开发最佳实践。原因在于应用（或负载均衡器）与 TiDB 之间使用的是 MySQL 协议，而 MySQL 协议的客户端假设只有一台服务器，而不是分布式集群。此外，连接依赖于 TCP 状态。

+ 在生产环境中，极少有人让应用直接连接某台 TiDB 实例。本模块主要讨论和对比两种情况：（a）应用通过网络负载均衡器连接 TiDB；（b）应用通过 TiProxy 连接 TiDB。

+ 下面我们来讨论 TiDB 节点在缩容时的行为：
  + 步骤 1：关闭 TCP 端口（默认 4000），拒绝新的连接
  + 步骤 2：对于已有连接，检查是否有事务正在执行
    + 情况 2.1：如果没有事务在运行，直接关闭连接
    + 情况 2.2：如果有事务在运行，则会等待一段时间让事务完成，超时或事务完成后（以先到者为准）关闭连接。（该等待时间在源码中称为 [gracefulCloseConnectionsTimeout](https://github.com/pingcap/tidb/blob/bed8f2573a7d6189f1611d913130cfb3150c9100/cmd/tidb-server/main.go#L924-L934C19)，为 15 秒，且不可配置。）

+ 接下来讨论网络负载均衡器（NLB）的健康检查流程。以 AWS 的 NLB 为例，相关参数如下（详细说明见 [网络负载均衡器目标组的运行状况检查](https://docs.aws.amazon.com/zh_cn/elasticloadbalancing/latest/network/target-group-health-checks.html)）：
  + `HealthCheckTimeoutSeconds`：健康检查超时时间（秒），即目标在该时间内无响应则判定为失败。范围 2–120 秒，本实验设置为 5 秒。
  + `HealthCheckIntervalSeconds`：每次健康检查的间隔时间（秒），范围 5–300 秒，本实验设置为 5 秒。
  + `UnhealthyThresholdCount`：连续失败多少次健康检查后，判定为不健康，范围 2–10 秒，本实验设置为 2 秒。
  + `Unhealthy draining interval`：目标被标记为不健康后，现有连接的排空时间（秒）。排空结束后，现有连接会被断开。本实验设置为 0。

+ TiDB 节点在最佳情况下（即缩容操作恰好在健康检查前完成）被标记为不健康的等待时间为 `(UnhealthyThresholdCount - 1) * HealthCheckIntervalSeconds + HealthCheckTimeoutSeconds`。本实验为 `(2 - 1) * 5 + 5 = 10` 秒。

+ 下面通过几个例子帮助理解上述计算。为简化，下图中将 `HealthCheckIntervalSeconds` 简称为 `interval`，`UnhealthyThresholdCount` 简称为 `count`，`HealthCheckTimeoutSeconds` 简称为 `timeout`。

  + 情况 1：`interval = 5`，`count = 2`，`timeout = 2`。TiDB 节点在最佳情况下 7 秒后被标记为不健康。
  ![demo009-m7-interval-case1_aa_001_20250808.png 85%](https://lab-static.pingcap.com/images/2025/8/8/14ae237101b018cd0389e4808d8e33ebbc499cc3.png)

  + 情况 2：`interval = 5`，`count = 2`，`timeout = 6`。TiDB 节点在最佳情况下 11 秒后被标记为不健康。
  ![demo009-m7-interval-case2_aa_001_20250808.png 85%](https://lab-static.pingcap.com/images/2025/8/8/6ac54809141c7d7c25a5ba2180490e96da018790.png)

  + 情况 3：`interval = 5`，`count = 3`，`timeout = 2`。TiDB 节点在最佳情况下 12 秒后被标记为不健康。
  ![demo009-m7-interval-case3_aa_001_20250808.png 85%](https://lab-static.pingcap.com/images/2025/8/8/96e212e18bcebfdea83aefa0b817aeaf227ecab2.png)

+ 在最不幸运的情况下，如果缩容操作恰好在健康检查刚发出后完成，并且健康检查立即返回成功，则需要等待 `UnhealthyThresholdCount * HealthCheckIntervalSeconds + HealthCheckTimeoutSeconds`。以情况 1 为例：

  + 情况 1：`interval = 5`，`count = 2`，`timeout = 2`。TiDB 节点在最坏情况下 12 秒后被标记为不健康。
  ![demo009-m7-interval-case1_worse_aa_001_20250808.png 85%](https://lab-static.pingcap.com/images/2025/8/8/9c3c48f009559e5b8c149d5716561ca3992acc00.png)

+ 综上，在本实验设置下（`interval = 5`，`count = 2`，`timeout = 5`），TiDB 节点被目标组标记为不健康的时间范围为 10 秒到 15 秒之间。

+ 根据 TiDB Server 缩容流程，应用可能需要额外处理以下几种情况：
  + （情况 A）在步骤 1，负载均衡器还未将待缩容的 TiDB 标记为不健康，仍可能分配新连接到该节点，可能报错 `dial tcp {host}:{port} connect: connection refused`。
  + （情况 B）在步骤 2，情况 2.1，如果关闭的连接被归还到连接池并再次使用，可能报错 `begin transaction failed`。
  + （情况 C）在步骤 2，情况 2.1，对于有多个事务的连接（如 autocommit 开启），前面的事务可能成功，后续事务可能报错 `invalid connection`。
  + （情况 D）在步骤 2，情况 2.2，对于运行超过 15 秒的事务，可能报错 `Lost connection to MySQL server`。

> [!TIP]
> 建议合理使用连接校验（connection validation），定期检查连接的健康状态。

+ 针对上述情况，简单的重试逻辑可以解决部分问题。
+ 接下来我们将演示 TiDB Server 被缩容时应用的表现，分为三种场景：
  + 应用直接连接 TiDB，无重试
  + 应用直接连接 TiDB，有重试
  + 应用通过 TiProxy 连接，无重试

### 步骤 2. 连接 TiUP 控制机

+ 点击这里 - [终端 1](ssh://monitor1-ssh#1) 打开分配给本实验的虚拟机终端。

### 步骤 3. 查看集群拓扑

+ 当前您应该有 1 台 TiDB Server 和 3 台 TiKV Server。

   ```bash
   ~/.tiup/bin/tiup cluster display tidb-demo
   ```

   ```output
   Cluster type:       tidb
   Cluster name:       tidb-demo
   Cluster version:    v${LAB:TIDB_VERSION}
   Deploy user:        ec2-user
   SSH type:           builtin
   Dashboard URL:      http://35.91.126.63:2379/dashboard
   Grafana URL:        http://10.90.4.204:3000
   ID                 Role          Host         Ports        OS/Arch       Status  Data Dir                      Deploy Dir
   --                 ----          ----         -----        -------       ------  --------                      ----------
   10.90.4.204:9093   alertmanager  10.90.4.204  9093/9094    linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
   10.90.4.204:3000   grafana       10.90.4.204  3000         linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
   10.90.3.89:2379    pd            10.90.3.89   2379/2380    linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
   10.90.4.204:9090   prometheus    10.90.4.204  9090/12020   linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
   10.90.1.74:4000    tidb          10.90.1.74   4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
   10.90.1.103:20160  tikv          10.90.1.103  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
   10.90.1.30:20160   tikv          10.90.1.30   20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
   10.90.1.34:20160   tikv          10.90.1.34   20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
   10.90.4.204:6000   tiproxy       10.90.4.204  6000/3080    linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
   Total nodes: 9
   ```

### 步骤 4. 扩容一台 TiDB Server

+ 现在我们扩容一台 TiDB Server，以便后续可以进行缩容操作。

  ```bash
  ~/.tiup/bin/tiup cluster scale-out tidb-demo scale-out-tidb-2.yaml \
  --user ec2-user \
  -i ~/.ssh/pe-class-key-${LAB:REGION_NAME}.pem \
  -y
  ```

  ```output
  ...
  Scaled cluster `tidb-demo` in successfully
  ```

### 步骤 5. 查看集群拓扑

+ 确认现在有两台 `TiDB` 和一台 `TiProxy`。

  ```bash
  ~/.tiup/bin/tiup cluster display tidb-demo
  ```

  ```output
  Cluster type:       tidb
  Cluster name:       tidb-demo
  Cluster version:    v8.5.1
  Deploy user:        ec2-user
  SSH type:           builtin
  Dashboard URL:      http://3.112.81.1:2379/dashboard
  Grafana URL:        http://10.90.4.40:3000
  ID                 Role          Host         Ports        OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----        -------       ------  --------                      ----------
  10.90.4.40:9093    alertmanager  10.90.4.40   9093/9094    linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  10.90.4.40:3000    grafana       10.90.4.40   3000         linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
  10.90.2.141:2379   pd            10.90.2.141  2379/2380    linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
  10.90.4.40:9090    prometheus    10.90.4.40   9090/12020   linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  10.90.1.165:4000   tidb          10.90.1.165  4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.1.66:4000    tidb          10.90.1.66   4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  10.90.3.160:20160  tikv          10.90.3.160  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.193:20160  tikv          10.90.3.193  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.3.243:20160  tikv          10.90.3.243  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  10.90.4.40:6000    tiproxy       10.90.4.40   6000/3080    linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  ```

### 步骤 6. 导入数据

  ```bash
  mysql -h ${HOST_DB1_PRIVATE_IP} -P 4000 -u root
  ```

  ```sql
  USE test
  CREATE TABLE `users` (
    `id` int,
    `name` varchar(100),
    `balance` int
  );
  INSERT INTO users VALUES (1, 'Alice', 10);
  INSERT INTO users VALUES (2, 'Bob', 10);
  INSERT INTO users VALUES (3, 'Charlie', 10);
  INSERT INTO users VALUES (4, 'Doug', 10);
  INSERT INTO users VALUES (5, 'Edison', 10);
  INSERT INTO users VALUES (6, 'Frank', 10);
  INSERT INTO users VALUES (7, 'Grace', 10);
  INSERT INTO users VALUES (8, 'Hannah', 10);
  INSERT INTO users VALUES (9, 'Iris', 10);
  INSERT INTO users VALUES (10, 'Jack', 10);
  ```

  ```sql
  SET GLOBAL autocommit = 0;
  exit
  ```
