## モジュール 3: 予期しないダウンタイム時に TiProxy を活用するアプリのベストプラクティス

+ 想定外の事象は起こり得ます。TiDB サーバーを動かすハードウェアが突然故障したり、クラウドのアベイラビリティゾーン障害に直面することもあります。こうした場合、接続は途切れる可能性があります。
+ TiDB は高可用性（HA）を維持するよう設計されています。本モジュールのベストプラクティスに従えば、この種の事象にも容易に対処できます。
+ ここでは、TiDB サーバーの予期しない障害を例に説明します。

### 手順 1. TiDB サーバーを 1 台スケールアウト

+ [ターミナル 2](ssh://monitor1-ssh#2) で TiDB サーバーを 1 台増設します。

  ```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
  ```

+ クラスタトポロジを確認します。TiDB サーバー 2 台、TiKV 3 台、TiProxy 1 台になっているはずです。

  ```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://${LAB:HOST_PD1_PUBLIC_IP}:2379/dashboard
  Grafana URL:        http://${LAB:HOST_MONITOR1_PRIVATE_IP}:3000
  ID                 Role          Host         Ports        OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----        -------       ------  --------                      ----------
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:9093     alertmanager  ${LAB:HOST_MONITOR1_PRIVATE_IP}    9093/9094    linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:3000     grafana       ${LAB:HOST_MONITOR1_PRIVATE_IP}    3000         linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
  ${LAB:HOST_PD1_PRIVATE_IP}:2379   pd            ${LAB:HOST_PD1_PRIVATE_IP}  2379/2380    linux/x86_64  Up|L    /tidb-data/pd-2379            /tidb-deploy/pd-2379
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:9090     prometheus    ${LAB:HOST_MONITOR1_PRIVATE_IP}    9090/12020   linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  ${LAB:HOST_DB1_PRIVATE_IP}:4000   tidb          ${LAB:HOST_DB1_PRIVATE_IP}  4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  ${LAB:HOST_DB2_PRIVATE_IP}:4000   tidb          ${LAB:HOST_DB2_PRIVATE_IP}  4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  ${LAB:HOST_KV1_PRIVATE_IP}:20160   tikv          ${LAB:HOST_KV1_PRIVATE_IP}   20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_KV2_PRIVATE_IP}:20160  tikv          ${LAB:HOST_KV2_PRIVATE_IP}  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_KV3_PRIVATE_IP}:20160  tikv          ${LAB:HOST_KV3_PRIVATE_IP}  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:6000   tiproxy       ${LAB:HOST_MONITOR1_PRIVATE_IP}  6000/3080    linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  Total nodes: 10
  ```

### 手順 2. ワークロードを実行

+ [ターミナル 1](ssh://monitor1-ssh#1) でワークロードを実行します。
+ 各スレッドが別々の TiDB サーバー上で動作していることが確認できます（IP は `${LAB:HOST_DB1_PRIVATE_IP}` と `${LAB:HOST_DB2_PRIVATE_IP}`）。
+ コードはモジュール 1 の `demo_tiproxy_no_retry.py` と同じです。詳細はモジュール 1 を参照してください。

  ```bash
  python3 demo_tiproxy_no_retry.py
  ```

  ```output
  ...
  Thread 1: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 3 balance updated to: 2
  Thread 4: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 1 balance updated to: 6
  Thread 3: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 9 balance updated to: 8
  Thread 1: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 10 balance updated to: 5
  Thread 2: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 1 balance updated to: 3
  ...
  ```

### 手順 3. [ターミナル 2](ssh://monitor1-ssh#2) で TiDB サーバーを強制終了

+ この手順では `kill -9 <pid>` を使用して、TiDB サーバーの突発的な障害を模擬します。
+ [ターミナル 2](ssh://monitor1-ssh#2) から TiDB サーバー 2 にログインします。
+ 次のコマンドを実行します（プロンプトが出たら `yes` と入力）。

  ```bash
  ssh -i /home/ec2-user/.ssh/pe-class-key-${LAB:REGION_NAME}.pem ${HOST_DB2_PRIVATE_IP} -y
  ```

  ```output
  Last login: Thu Sep  4 23:12:01 2025 from ip-10-90-4-66.ap-northeast-1.compute.internal

  50 package(s) needed for security, out of 63 available
  Run "sudo yum update" to apply all updates.
  ```

> [!TIP]
> TiUP は systemd を用いてデーモンプロセスを起動します。プロセスが異常終了すると、約 15 秒後に自動再起動されます。
> 再起動に使われるスクリプトは `/tidb-deploy/tidb-4000/scripts/` 配下の `run_tidb.sh` です。以下ではこのスクリプト名を変更し、15 秒後の自動再起動を防ぎます。

+ 再起動スクリプトの名前を変更します。

  ```bash
  sudo mv /tidb-deploy/tidb-4000/scripts/run_tidb.sh /tidb-deploy/tidb-4000/scripts/run_tidb2.sh
  ```

+ プロセスIDを指定して TiDB サーバーを強制終了します。以下のコマンドは出力を返しません。

  ```bash
  kill -9 `pidof tidb-server`
  ```

### 手順 4. [ターミナル 1](ssh://monitor1-ssh#1) の出力を観察

  ```output
  ...
  Thread 1: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 4 balance updated to: 1
  Thread 3: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 10 balance updated to: 10
  Thread 4: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 9 balance updated to: 1
  Thread 2: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 3 balance updated to: 4
  Thread 3: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. Error during query execution: Lost connection to MySQL server during query
  Thread 1: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. Error during query execution: Lost connection to MySQL server during query
  Thread 4: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 7 balance updated to: 1
  Thread 2: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 10 balance updated to: 2
  Thread 1: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 1 balance updated to: 8
  ...
  ```

+ `Error during query execution: Lost connection to MySQL server during query` というエラーが 2 回表示されます。これはクエリが中断されたことを示します。
+ `ctrl` + `c` でワークロードを停止します。

  ```output
  ^C
  Received interrupt signal. Stopping all threads...
  ```

> [!TIP]
> まれに、TiDB サーバーを停止してもクエリが中断されない場合があります。その際は手順 5 に従ってクラスタを復旧し、手順 4 をやり直してください。スレッド数（現在は 4）を増やすことで中断の再現性を高められます。

### 手順 5. 強制終了した TiDB サーバーを再起動

+ [ターミナル 2](ssh://monitor1-ssh#2) で再起動スクリプト名を元に戻します。

  ```bash
  sudo mv /tidb-deploy/tidb-4000/scripts/run_tidb2.sh /tidb-deploy/tidb-4000/scripts/run_tidb.sh
  sleep 15
  ```

+ [ターミナル 1](ssh://monitor1-ssh#1) でクラスタトポロジを確認します（TiDB 2、TiKV 3、TiProxy 1）。

  ```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://${LAB:HOST_PD1_PUBLIC_IP}:2379/dashboard
  Grafana URL:        http://${LAB:HOST_MONITOR1_PRIVATE_IP}:3000
  ID                 Role          Host         Ports        OS/Arch       Status  Data Dir                      Deploy Dir
  --                 ----          ----         -----        -------       ------  --------                      ----------
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:9093     alertmanager  ${LAB:HOST_MONITOR1_PRIVATE_IP}    9093/9094    linux/x86_64  Up      /tidb-data/alertmanager-9093  /tidb-deploy/alertmanager-9093
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:3000     grafana       ${LAB:HOST_MONITOR1_PRIVATE_IP}    3000         linux/x86_64  Up      -                             /tidb-deploy/grafana-3000
  ${LAB:HOST_PD1_PRIVATE_IP}:2379   pd            ${LAB:HOST_PD1_PRIVATE_IP}  2379/2380    linux/x86_64  Up|L      /tidb-data/pd-2379            /tidb-deploy/pd-2379
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:9090     prometheus    ${LAB:HOST_MONITOR1_PRIVATE_IP}    9090/12020   linux/x86_64  Up      /tidb-data/prometheus-9090    /tidb-deploy/prometheus-9090
  ${LAB:HOST_DB1_PRIVATE_IP}:4000   tidb          ${LAB:HOST_DB1_PRIVATE_IP}  4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  ${LAB:HOST_DB2_PRIVATE_IP}:4000   tidb          ${LAB:HOST_DB2_PRIVATE_IP}  4000/10080   linux/x86_64  Up      -                             /tidb-deploy/tidb-4000
  ${LAB:HOST_KV1_PRIVATE_IP}:20160   tikv          ${LAB:HOST_KV1_PRIVATE_IP}   20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_KV2_PRIVATE_IP}:20160  tikv          ${LAB:HOST_KV2_PRIVATE_IP}  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_KV3_PRIVATE_IP}:20160  tikv          ${LAB:HOST_KV3_PRIVATE_IP}  20160/20180  linux/x86_64  Up      /tidb-data/tikv-20160         /tidb-deploy/tikv-20160
  ${LAB:HOST_MONITOR1_PRIVATE_IP}:6000   tiproxy       ${LAB:HOST_MONITOR1_PRIVATE_IP}  6000/3080    linux/x86_64  Up      -                             /tidb-deploy/tiproxy-6000
  Total nodes: 10
  ```

### 手順 6. Python コード `demo_tiproxy_retry.py` を確認

  ```bash
  cat demo_tiproxy_retry.py
  ```

+ `demo_tiproxy_retry.py` は `demo_tiproxy_no_retry.py` とほぼ同じですが、以下のリトライロジックが追加されています。
+ 接続断が発生した場合に最大 5 回までリトライし、各リトライの間隔は 5 秒です。
+ 予期しないダウンが起きた際、TiProxy が不健全な TiDB を判定し、健全なサーバーへ振り替えるまでの時間として、少なくとも 25 秒（5 秒 × 5 回）を与える効果があります。

  ```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):
  ...
  ```

+ 次に、TiProxy が TiDB を不健全と判断するまでの時間について説明します。
+ 関連パラメータ（いずれも固定値）は以下の通りです。ヘルスチェック間隔は 3 秒、2 秒以内に応答がなければ失敗と見なし、直ちにリトライを開始します。最大 3 回までリトライし、リトライ間隔は 1 秒です。
  + `healthCheckInterval = 3 seconds`
  + `healthCheckMaxRetries = 3`
  + `healthCheckRetryInterval = 1 second`
  + `healthCheckTimeout = 2 seconds`
+ 図中では表記を簡略化しています。
  + `healthCheckInterval` → `Interval`
  + `healthCheckMaxRetries` → `Retry`
  + `healthCheckRetryInterval` → `RetryInterval`
  + `healthCheckTimeout` → `Timeout`
+ ヘルスチェック直前に障害が発生した「幸運な」ケースでは、およそ 10 秒で不健全判定されます。

![demo018_tidb_health_judgement_lucky_aa_001_20250911.png 85%](https://lab-static.pingcap.com/images/2025/9/11/a3e648047cd786ce44131a20e3290e0ffa7eef8d.png)

+ ヘルスチェック直後に障害が発生した「不運な」ケースでは、およそ 13 秒で不健全判定されます。

![demo018_tidb_health_judgement_unlucky_aa_001_20250911.png 85%](https://lab-static.pingcap.com/images/2025/9/11/09fa151f39fc666a9ebbdd1c67fc019255b1ee09.png)

+ `demo_tiproxy_retry.py` のリトライは最大 25 秒をカバーするため、接続は健全な TiDB サーバーへ適時に切り替わります。

### 手順 7. リトライロジック付きコードを実行

  ```bash
  python3 demo_tiproxy_retry.py
  ```

  ```output
  ...
  Thread 4: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 5 balance updated to: 6
  Thread 1: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 8 balance updated to: 2
  Thread 2: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. User 2 balance updated to: 5
  ...
  ```

### 手順 8. 再度 TiDB サーバーを強制終了

+ [ターミナル 2](ssh://monitor1-ssh#2) で再起動スクリプトをリネームします。

  ```bash
  sudo mv /tidb-deploy/tidb-4000/scripts/run_tidb.sh /tidb-deploy/tidb-4000/scripts/run_tidb2.sh
  ```

+ プロセスIDを指定して TiDB サーバーを強制終了します。以下のコマンドは出力を返しません。

  ```bash
  kill -9 `pidof tidb-server`
  ```

### 手順 9. [ターミナル 1](ssh://monitor1-ssh#1) の出力を確認

+ 手順 4 と比べ、接続断が発生するとクエリがリトライされ、クライアントがエラーを受け取る可能性が低減していることが分かります。アプリケーションは継続して動作します。
+ しばらくすると、すべてのスレッドが健全な TiDB サーバーへルーティングされます。

  ```output
  ...
  Thread 2: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. Error during query execution: Lost connection to MySQL server during query
  Attempt 1 failed: Lost connection to MySQL server during query. Retrying...
  Thread 4: Connected to TiDB server ${LAB:HOST_DB2_PRIVATE_IP}:4000. Error during query execution: Lost connection to MySQL server during query
  Attempt 1 failed: Lost connection to MySQL server during query. Retrying...
  Thread 2: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 10 balance updated to: 2
  Thread 1: Connected to TiDB server ${LAB:HOST_DB1_PRIVATE_IP}:4000. User 1 balance updated to: 8
  ...
  ```

### まとめ

+ TiDB サーバーが予期せずオフラインになっても、TiDB 自体は生き残りますが、アプリケーションとの接続は中断され得ます。アプリ側にシンプルなリトライを実装することで、継続稼働性を高められます。
