## モジュール2: ネットワークロードバランサー利用時のTiDBスケールインにおけるアプリケーションのベストプラクティス

このモジュールでは、ネットワークロードバランサーを利用してTiDBサーバーをスケールインする際のアプリケーションのベストプラクティスについて説明します。

下図は現時点でのTiDBクラスタのトポロジーを示しています。赤い点線で囲まれたTiDBサーバーは後ほどスケールインされます。

![demo016-m7-tapo-nlb_aa_002_20250821.png 85%](https://lab-static.pingcap.com/images/2025/9/5/3fac225ddd21a2a170c5a725da8590ffb3a465d3.png)

### 手順 1. TiDBサーバーをネットワークロードバランサーに登録する

  ```bash
  source ./hosts-env.sh
  source ./cloud-env.sh

  LB_ARN=`aws elbv2 describe-load-balancers --names ${LAB:USER_UNIQUE_TAG} --query "LoadBalancers[0].LoadBalancerArn" --output text --region ${LAB:REGION_CODE}`

  DEMO_TG_ARN=`aws elbv2 describe-target-groups --load-balancer-arn ${LB_ARN} --query "TargetGroups[0].TargetGroupArn" --output text --region ${LAB:REGION_CODE}`

  aws elbv2 register-targets --target-group-arn ${DEMO_TG_ARN} --targets "Id=${HOST_DB1_PRIVATE_IP},Port=4000" "Id=${HOST_DB2_PRIVATE_IP},Port=4000" --region ${LAB:REGION_CODE}

  sleep 60;
  echo TiDB instances registered to NLB target group ${DEMO_TG_ARN}
  ```

  ```output
  TiDB instances registered to NLB target group arn:aws:elasticloadbalancing:us-west-2:xxxxxxxxxxxx:targetgroup/xxxxxx-TG-Z2TREFSV0YSJ/8efa9967b824f524
  ```

### 手順 2. ターゲットグループ内のTiDBサーバーのヘルスステータスを確認する

  ```bash
  aws elbv2 describe-target-health --region ${LAB:REGION_CODE} --target-group-arn ${DEMO_TG_ARN} --output table
  ```

  ```output
  -------------------------------------------------
  |             DescribeTargetHealth              |
  +-----------------------------------------------+
  ||          TargetHealthDescriptions           ||
  |+---------------------------------------------+|
  ||               HealthCheckPort               ||
  |+---------------------------------------------+|
  ||  10080                                      ||
  |+---------------------------------------------+|
  |||                  Target                   |||
  ||+-------------------+---------------+-------+||
  ||| AvailabilityZone  |      Id       | Port  |||
  ||+-------------------+---------------+-------+||
  |||  ap-northeast-1a  |  10.90.2.178  |  4000 |||
  ||+-------------------+---------------+-------+||
  |||               TargetHealth                |||
  ||+------------------+------------------------+||
  |||  State           |  healthy               |||
  ||+------------------+------------------------+||
  ||          TargetHealthDescriptions           ||
  |+---------------------------------------------+|
  ||               HealthCheckPort               ||
  |+---------------------------------------------+|
  ||  10080                                      ||
  |+---------------------------------------------+|
  |||                  Target                   |||
  ||+-------------------+--------------+--------+||
  ||| AvailabilityZone  |     Id       | Port   |||
  ||+-------------------+--------------+--------+||
  |||  ap-northeast-1a  |  10.90.2.70  |  4000  |||
  ||+-------------------+--------------+--------+||
  |||               TargetHealth                |||
  ||+------------------+------------------------+||
  |||  State           |  healthy               |||
  ||+------------------+------------------------+|
  ```

### 手順 3. ロードバランサーのDNS名を取得する

  ```bash
  LB_DNS=`aws elbv2 describe-load-balancers --region ${LAB:REGION_CODE} --names ${LAB:USER_UNIQUE_TAG} --query 'LoadBalancers[0].DNSName' --output text`
  echo ${LB_DNS}
  ```

  ```output
  g-xxxx-xxxx-xx-xx-xxxxx-xxxx.elb.ap-northeast-1.amazonaws.com
  ```

### 手順 4. 再接続ロジックなしでTiDBサーバーに対して以下のコードを実行する

+ [ターミナル1](ssh://monitor1-ssh#1)で、以下のコードを確認し、実行してください。
+ 下記のコードでは4つのスレッドが起動します。各スレッドは無限ループでネットワークロードバランサーに接続し、レコードを更新します。`sleep(4)`コマンドで長時間クエリを模倣しています。
+ スレッドが後でスケールインされるTiDBサーバーにルーティングされた場合、手順1で説明したAまたはBの状況に遭遇する可能性があります。

  ```bash
  cat demo_no_retry.py
  ```

  ```output
  while True:
      try:
          conn = connect(
              database="test",
              host=lb_dns,
              port=4000,
              user="root",
              password="",
          )
          cursor = conn.cursor(prepared=True)

          random_integer = random.randint(0, 10)
          random_id=random.randint(1, 10)
          query = "UPDATE test.users SET balance = " + str(random_integer) + " WHERE id = " + str(random_id) + ";"

          cursor.execute("BEGIN")
          cursor.execute(query)
          time.sleep(4)

          try:
              cursor.execute("COMMIT")
              print(f"Thread {thread_id}: Committed. user {random_id} balance updated to: {random_integer}")
          except Error as e:
              print(f"Thread {thread_id}: Error during query execution: {e}")
          try:
              cursor.close()
              conn.close()
          except Error as e:
              pass

      except (Error, MySQLInterfaceError) as connect_err:
          print(f"Thread {thread_id}: Connection or execution error: {connect_err}")
  ```

  ```bash
  python3 demo_no_retry.py ${LB_DNS}
  ```

  ```output
  ...
  Thread 3: Committed. user 3 balance updated to: 3
  Thread 4: Committed. user 9 balance updated to: 4
  ...
  ```

### 手順 5. 再試行ロジック付きでTiDBサーバーに対してコードを実行する

+ [ターミナル2](ssh://monitor1-ssh#2)を開き、以下のコードを確認し、実行してください。
+ 今回は、TiDBスケールイン中に接続が切れた場合の再試行ロジックが追加されています。
+ 再試行間隔は「5秒」に設定されています。

  ```bash
  cat demo_retry.py
  ```

  ```output
  ...
  def retry(max_attempts=5, delay=5):
      def decorator(func):
          @wraps(func)
          def wrapper(*args, **kwargs):
              for attempt in range(max_attempts):
                  try:
                      return func(*args, **kwargs)
                  except (Error, MySQLInterfaceError) as e:
                      if attempt == max_attempts - 1:  # Last attempt
                          print(f"All {max_attempts} attempts failed. Last error: {e}")
                          raise
                      print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
                      time.sleep(delay)
              return None
          return wrapper
      return decorator

  @retry(max_attempts=5, delay=5)
  def execute_database_operation(thread_id):
  ...
  ```

  ```bash
  LB_DNS=`aws elbv2 describe-load-balancers --region ${LAB:REGION_CODE} --names ${LAB:USER_UNIQUE_TAG} --query 'LoadBalancers[0].DNSName' --output text`
  echo ${LB_DNS}
  ```

  ```output
  g-xxxx-xxxx-xx-xx-xxxxx-xxxx.elb.ap-northeast-1.amazonaws.com
  ```

  ```bash
  python3 demo_retry.py ${LB_DNS}
  ```

  ```output
  ...
  Thread 2: Committed. user 4 balance updated to: 2
  Thread 1: Committed. user 1 balance updated to: 7
  ...
  ```

### 手順 6. TiDBサーバーをスケールインする

+ [ターミナル3](ssh://monitor1-ssh#3)を開き、以下のコマンドを実行してください。

  ```bash
  ~/.tiup/bin/tiup cluster scale-in tidb-demo --node ${HOST_DB2_PRIVATE_IP}:4000 -y
  ```

  ```output
  ...
  クラスタ `tidb-demo` のスケールインに成功しました
  ```

### 手順 7. [ターミナル1](ssh://monitor1-ssh#1)の様子を観察する

+ [ターミナル1](ssh://monitor1-ssh#1)では、以下のようなエラーメッセージが表示される場合があります。

  ```output
  Thread X: Connection or execution error: Lost connection to MySQL server during query
  ```

  ```output
  Thread X: Connection or execution error: 2003 (HY000): Can't connect to MySQL server on 'g-xxx-xxx-xxx-xxx-77fd842711201a37.elb.ap-northeast-1.amazonaws.com:4000' (111)
  ```

### 手順 8. [ターミナル2](ssh://monitor1-ssh#2)の様子を観察する

+ [ターミナル2](ssh://monitor1-ssh#2)では、以下のメッセージが表示される場合があります。本アプリケーションは最大5回まで再試行し、各再試行の間に5秒待機します。モジュール1でも説明した通り、最悪の場合ネットワークロードバランサー（NLB）がTiDBサーバーの異常を検知してクエリを正常なTiDBサーバーにルーティングするまでに15秒かかります。本アプリケーションの再試行ウィンドウはNLBがスケールインされたTiDBサーバーの異常を検知するのに十分な時間を確保しているため、アプリケーションのゼロダウンタイムを実現できます。

  ```output
  Attempt 1 failed: Lost connection to MySQL server during query. Retrying...
  ```

  ```output
  Attempt 1 failed: 2003 (HY000): Can't connect to MySQL server on 'g-pingcap-com-ch-migrate-6540307-d8eaff67a1fb8f9e.elb.ap-northeast-1.amazonaws.com:4000' (111). Retrying...
  ```

+ [ターミナル2](ssh://monitor1-ssh#2)の結果から、`demo_connection_reconnect.py`はシンプルな再試行ロジックでTiDBサーバーのスケールインに対応できていることが分かります。したがって、このアプリケーションはTiDBのゼロダウンタイム機能を活用できます。

### 手順 9. ワークロードを停止する

+ [ターミナル1](ssh://monitor1-ssh#1)および[ターミナル2](ssh://monitor1-ssh#2)で、`ctrl` + `c`を使ってワークロードを停止してください。

### 手順 10. ターゲットグループからTiDBノードを登録解除する

+ [ターミナル1](ssh://monitor1-ssh#1)で、以下のコマンドを実行してください。**必ずターミナル1で実行してください。**

  ```bash
  TIDB_TARGETS=$(aws elbv2 describe-target-health --region ${LAB:REGION_CODE} --target-group-arn ${DEMO_TG_ARN} --query 'TargetHealthDescriptions[?Target.Port==`4000`].Target' --output json)
  ```

  ```bash
  if [ "$TIDB_TARGETS" != "[]" ]; then
    aws elbv2 deregister-targets --region ${LAB:REGION_CODE} --target-group-arn ${DEMO_TG_ARN} --targets "$TIDB_TARGETS"
    echo "Deregistered TiDB servers"
  else
    echo "No TiDB servers found to deregister"
  fi
  ```

  ```output
  Deregistered TiDB servers
  ```

### 手順 11. 登録解除の結果を確認する

+ 以下のコマンドで登録解除の結果を確認できます。次のいずれかの出力が表示されます。

  ```bash
  aws elbv2 describe-target-health --region ${LAB:REGION_CODE} --target-group-arn ${DEMO_TG_ARN} --output table
  ```

  ```output
  ----------------------
  |DescribeTargetHealth|
  +--------------------+
  ```

  ```output
  -------------------------------------------------------------
  |                   DescribeTargetHealth                    |
  +-----------------------------------------------------------+
  ||                TargetHealthDescriptions                 ||
  |+--------------------------------------+------------------+|
  ||  HealthCheckPort                     |  10080           ||
  |+--------------------------------------+------------------+|
  |||                        Target                         |||
  ||+---------------------------+---------------------------+||
  |||  AvailabilityZone         |  ap-northeast-1a          |||
  |||  Id                       |  10.90.2.226              |||
  |||  Port                     |  4000                     |||
  ||+---------------------------+---------------------------+||
  |||                     TargetHealth                      |||
  ||+--------------+----------------------------------------+||
  |||  Description |  Target deregistration is in progress  |||
  |||  Reason      |  Target.DeregistrationInProgress       |||
  |||  State       |  draining                              |||
  ||+--------------+----------------------------------------+||
  ||                TargetHealthDescriptions                 ||
  |+--------------------------------------+------------------+|
  ||  HealthCheckPort                     |  10080           ||
  |+--------------------------------------+------------------+|
  |||                        Target                         |||
  ||+---------------------------+---------------------------+||
  |||  AvailabilityZone         |  ap-northeast-1a          |||
  |||  Id                       |  10.90.2.172              |||
  |||  Port                     |  4000                     |||
  ||+---------------------------+---------------------------+||
  |||                     TargetHealth                      |||
  ||+--------------+----------------------------------------+||
  |||  Description |  Target deregistration is in progress  |||
  |||  Reason      |  Target.DeregistrationInProgress       |||
  |||  State       |  draining                              |||
  ||+--------------+----------------------------------------+||
  ```

### まとめ

+ このモジュールでは、ネットワークロードバランサーを利用してTiDBサーバーをスケールインする際のアプリケーションのベストプラクティスについて説明しました。
+ 十分な再試行時間ウィンドウを設けることで、TiDBサーバーのスケールイン時にもネットワークロードバランサーを利用してアプリケーションのゼロダウンタイムを実現できることを示しました。
