T-CREATOR

MySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現

MySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現

データベースの冗長化構成を構築したものの、アプリケーション側でフェイルオーバーのロジックを実装するのは大変ですよね。MySQL Router を使えば、アプリケーションコードを一切変更せずに、自動的なフェイルオーバーを実現できます。

本記事では、MySQL Router のセットアップから、実際のフェイルオーバー動作の確認まで、実践的な手順を詳しく解説していきます。これにより、データベース障害時にもアプリケーションが自動的に別のデータベースサーバーへ接続を切り替え、サービスの継続性を確保できるようになるでしょう。

背景

MySQL Router は、MySQL の公式ツールとして提供されている軽量プロキシです。アプリケーションとデータベースの間に配置することで、接続のルーティングや負荷分散を透過的に行います。

従来の構成では、アプリケーション自身が複数のデータベースサーバーを管理し、障害検知やフェイルオーバーのロジックを実装する必要がありました。これは開発コストが高く、バグの温床にもなりやすいものです。

MySQL Router を導入することで、これらの複雑な処理をミドルウェア層で吸収できます。アプリケーションは単一のエンドポイントに接続するだけで、背後の複雑な冗長化構成を意識する必要がなくなるのです。

以下の図は、MySQL Router を使った基本的なアーキテクチャを示しています。

mermaidflowchart TB
  app["アプリケーション"]
  router["MySQL Router"]
  primary[("Primary<br/>DB")]
  replica1[("Replica1<br/>DB")]
  replica2[("Replica2<br/>DB")]

  app -->|"単一接続<br/>localhost:6446"| router
  router -->|"読み書き"| primary
  router -.->|"読み取り専用<br/>localhost:6447"| replica1
  router -.->|"読み取り専用"| replica2
  primary -.->|"レプリケーション"| replica1
  primary -.->|"レプリケーション"| replica2

アプリケーションは MySQL ルーターの提供するポート(6446:読み書き用、6447:読み取り専用)に接続するだけで、背後のデータベース構成を意識することなく利用できます。

図で理解できる要点

  • アプリケーションは単一エンドポイント(MySQL Router)にのみ接続する
  • MySQL Router が適切なデータベースサーバーへトラフィックを振り分ける
  • Primary 障害時には自動的に Replica が昇格し、透過的にフェイルオーバーが実行される

課題

データベースの冗長化構成を運用する際、以下のような課題に直面します。

アプリケーション側での複雑な実装

複数のデータベースサーバーへの接続管理をアプリケーション側で実装すると、以下の問題が発生しやすくなります。

接続文字列の管理が煩雑になり、環境ごとに異なる設定を保持しなければなりません。開発環境、ステージング環境、本番環境それぞれで複数のデータベースホストを管理するのは、設定ミスのリスクを高めます。

障害検知のロジックを実装する必要があり、タイムアウト時間の調整や、リトライ処理、ヘルスチェックなど、考慮すべき点が多岐にわたります。これらを適切に実装しないと、障害時の切り替えが遅延したり、不要な再接続が頻発したりするでしょう。

読み書き分離の実装も大きな負担です。書き込みは Primary へ、読み取りは Replica へという振り分けロジックを、すべてのデータベースアクセス箇所で実装しなければなりません。

運用面での課題

データベースサーバーのメンテナンスやスケールアウト時にも問題が生じます。

サーバー追加時の影響として、新しい Replica を追加する際、すべてのアプリケーションインスタンスの設定を変更し、再起動する必要があります。これはダウンタイムを伴い、ビジネスに影響を与えかねません。

フェイルオーバーの手動操作も課題です。Primary 障害時に、どの Replica を新しい Primary に昇格させるかを判断し、アプリケーションの接続先を変更する作業は、人的ミスのリスクが高く、復旧時間も長くなりがちです。

以下の図は、MySQL Router 導入前後の比較を示しています。

mermaidflowchart LR
  subgraph before["MySQL Router 導入前"]
    app1["アプリ"]
    app1 --|接続管理<br />フェイルオーバー<br />負荷分散|--> db1[(DB群)]
  end

  subgraph after["MySQL Router 導入後"]
    app2["アプリ"]
    router2["MySQL<br />Router"]
    app2 --|シンプルな<br />単一接続|--> router2
    router2 --|自動管理|--> db2[(DB群)]
  end

導入前はアプリケーションが複雑な接続管理を担っていましたが、導入後は MySQL Router がその役割を引き受け、アプリケーションはシンプルな接続だけに専念できます。

図で理解できる要点

  • 導入前はアプリケーションが接続管理・フェイルオーバー・負荷分散をすべて実装
  • 導入後は MySQL Router が複雑な処理を代行
  • アプリケーションコードが大幅に簡素化される

解決策

MySQL Router を導入することで、これらの課題を解決できます。MySQL Router は軽量なプロキシとして動作し、アプリケーションとデータベースの間で透過的にトラフィックをルーティングします。

MySQL Router の主要機能

自動フェイルオーバー機能により、Primary 障害を検知すると自動的に別のサーバーへ接続を切り替えます。アプリケーションは再接続を試みるだけで、新しい Primary へアクセスできるようになるのです。

読み書き分離も自動的に行われます。ポート 6446 への接続は読み書き可能な Primary へ、ポート 6447 への接続は読み取り専用の Replica へルーティングされます。アプリケーション側で意識する必要はありません。

動的な構成変更にも対応しており、データベースサーバーの追加や削除をアプリケーションの再起動なしで反映できます。MySQL Group Replication や InnoDB Cluster と組み合わせれば、完全自動化された冗長化環境を構築できるでしょう。

アーキテクチャ上のメリット

アプリケーションとデータベースの疎結合化が実現します。データベース構成の変更がアプリケーションに影響を与えにくくなり、それぞれを独立して進化させられます。

接続プーリング機能により、データベースへの接続数を効率的に管理できます。多数のアプリケーションインスタンスからの接続を、MySQL Router で集約し、データベースサーバーへの負荷を軽減するのです。

監視とメトリクスの一元化も可能になります。すべてのデータベース接続が MySQL Router を経由するため、トラフィックパターンやエラー率を一箇所で把握できます。

以下の図は、フェイルオーバー時の動作フローを示しています。

mermaidsequenceDiagram
  participant App as アプリケーション
  participant Router as MySQL Router
  participant Primary as Primary DB
  participant Replica as Replica DB

  App->>Router: 接続要求
  Router->>Primary: クエリ転送
  Primary-->>Router: 応答
  Router-->>App: 結果返却

  Note over Primary: 障害発生

  App->>Router: クエリ要求
  Router->>Primary: 接続試行
  Primary--xRouter: タイムアウト
  Router->>Replica: 自動切替
  Replica-->>Router: 応答
  Router-->>App: 結果返却
  Note over App: アプリは障害を<br/>ほとんど意識しない

アプリケーションからは通常のクエリ実行と変わらず、MySQL Router が障害検知と切り替えを透過的に処理します。

図で理解できる要点

  • Primary 障害時、MySQL Router が自動的に検知
  • アプリケーションの再接続時に新しい Primary へ自動ルーティング
  • アプリケーション側では特別な処理が不要

具体例

実際に MySQL Router をセットアップし、フェイルオーバー動作を確認してみましょう。以下の例では、Docker 環境で MySQL InnoDB Cluster を構築し、MySQL Router を介してアプリケーションから接続します。

前提環境

以下の環境を前提としています。

#項目内容
1OSUbuntu 22.04 LTS または macOS
2Docker20.10 以降
3Docker Compose2.0 以降
4MySQL8.0 以降
5MySQL Router8.0 以降

ステップ 1:Docker Compose 設定ファイルの作成

まず、MySQL InnoDB Cluster を構築するための Docker Compose 設定を作成します。

docker-compose.yml の基本構成

以下は、3 ノードの MySQL Cluster を構成するための設定です。

yamlversion: '3.8'

services:
  # MySQL ノード1(初期Primary)
  mysql-node1:
    image: mysql/mysql-server:8.0
    container_name: mysql-node1
    hostname: mysql-node1
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_ROOT_HOST: '%'
    ports:
      - '3306:3306'
    volumes:
      - mysql-node1-data:/var/lib/mysql
    networks:
      - mysql-cluster
    command:
      - --server-id=1
      - --log-bin=mysql-bin
      - --gtid-mode=ON
      - --enforce-gtid-consistency=ON
      - --binlog-checksum=NONE
      - --disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"

このノードは初期 Primary として動作します。gtid-modeenforce-gtid-consistencyを有効にすることで、トランザクションの一貫性を保証します。

残りのノードの定義

ノード 2 とノード 3 も同様に定義します。

yaml# MySQL ノード2
mysql-node2:
  image: mysql/mysql-server:8.0
  container_name: mysql-node2
  hostname: mysql-node2
  environment:
    MYSQL_ROOT_PASSWORD: rootpass
    MYSQL_ROOT_HOST: '%'
  ports:
    - '3307:3306'
  volumes:
    - mysql-node2-data:/var/lib/mysql
  networks:
    - mysql-cluster
  command:
    - --server-id=2
    - --log-bin=mysql-bin
    - --gtid-mode=ON
    - --enforce-gtid-consistency=ON
    - --binlog-checksum=NONE
    - --disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"
yaml# MySQL ノード3
mysql-node3:
  image: mysql/mysql-server:8.0
  container_name: mysql-node3
  hostname: mysql-node3
  environment:
    MYSQL_ROOT_PASSWORD: rootpass
    MYSQL_ROOT_HOST: '%'
  ports:
    - '3308:3306'
  volumes:
    - mysql-node3-data:/var/lib/mysql
  networks:
    - mysql-cluster
  command:
    - --server-id=3
    - --log-bin=mysql-bin
    - --gtid-mode=ON
    - --enforce-gtid-consistency=ON
    - --binlog-checksum=NONE
    - --disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"

各ノードは異なるserver-idを持ち、ホストマシンの異なるポート(3306、3307、3308)にマッピングされています。

MySQL Router の定義

MySQL Router コンテナを追加します。

yaml# MySQL Router
mysql-router:
  image: mysql/mysql-router:8.0
  container_name: mysql-router
  hostname: mysql-router
  environment:
    MYSQL_HOST: mysql-node1
    MYSQL_PORT: 3306
    MYSQL_USER: root
    MYSQL_PASSWORD: rootpass
    MYSQL_INNODB_CLUSTER_MEMBERS: 3
  ports:
    - '6446:6446' # 読み書き用ポート
    - '6447:6447' # 読み取り専用ポート
  depends_on:
    - mysql-node1
    - mysql-node2
    - mysql-node3
  networks:
    - mysql-cluster

ポート 6446 は読み書き可能な Primary への接続、ポート 6447 は読み取り専用の Replica への接続に使用されます。

ボリュームとネットワークの定義

最後に、永続化ボリュームとネットワークを定義します。

yamlvolumes:
  mysql-node1-data:
  mysql-node2-data:
  mysql-node3-data:

networks:
  mysql-cluster:
    driver: bridge

これにより、各 MySQL ノードのデータが永続化され、コンテナ間で通信できるようになります。

ステップ 2:MySQL InnoDB Cluster の構築

Docker Compose で環境を起動した後、MySQL Shell を使って InnoDB Cluster を構成します。

コンテナの起動

ターミナルで以下のコマンドを実行し、すべてのコンテナを起動します。

bash# Docker Composeで環境を起動
docker-compose up -d
bash# コンテナの起動状態を確認
docker-compose ps

すべてのコンテナがUp状態になっていることを確認してください。

MySQL Shell でノード 1 に接続

ノード 1 でクラスタを初期化します。

bash# MySQL Shellをインストール(未インストールの場合)
# macOS
brew install mysql-shell

# Ubuntu/Debian
sudo apt-get install mysql-shell
bash# ノード1に接続
mysqlsh root@localhost:3306 --password=rootpass

MySQL Shell のプロンプトが表示されたら、JavaScript モードで作業を進めます。

InnoDB Cluster の作成

まず、各ノードがクラスタに参加できる状態か確認します。

javascript// ノード1の構成チェック
dba.checkInstanceConfiguration('root@mysql-node1:3306', {
  password: 'rootpass',
});
javascript// 必要に応じてインスタンスを構成
dba.configureInstance('root@mysql-node1:3306', {
  password: 'rootpass',
});

問題がなければ、クラスタを作成します。

javascript// クラスタの作成
var cluster = dba.createCluster('myCluster');
javascript// クラスタの状態確認
cluster.status();

この時点で、ノード 1 だけのシングルノードクラスタが作成されています。

残りのノードをクラスタに追加

ノード 2 とノード 3 をクラスタに参加させます。

javascript// ノード2を追加
cluster.addInstance('root@mysql-node2:3306', {
  password: 'rootpass',
});

追加処理中に、クローン方式を選択するプロンプトが表示された場合は、C(Clone)を選択すると自動的にデータが同期されます。

javascript// ノード3を追加
cluster.addInstance('root@mysql-node3:3306', {
  password: 'rootpass',
});

すべてのノードが追加されたら、再度クラスタの状態を確認しましょう。

javascript// 最終的なクラスタ状態の確認
cluster.status();

以下のような JSON 形式の出力が表示され、3 つのノードすべてがONLINE状態になっているはずです。

json{
  "clusterName": "myCluster",
  "defaultReplicaSet": {
    "name": "default",
    "primary": "mysql-node1:3306",
    "status": "OK",
    "topology": {
      "mysql-node1:3306": {
        "address": "mysql-node1:3306",
        "mode": "R/W",
        "role": "HA",
        "status": "ONLINE"
      },
      "mysql-node2:3306": {
        "address": "mysql-node2:3306",
        "mode": "R/O",
        "role": "HA",
        "status": "ONLINE"
      },
      "mysql-node3:3306": {
        "address": "mysql-node3:3306",
        "mode": "R/O",
        "role": "HA",
        "status": "ONLINE"
      }
    }
  }
}

ノード 1 がR​/​W(読み書き可能)の Primary、ノード 2 とノード 3 がR​/​O(読み取り専用)の Replica として動作しています。

ステップ 3:MySQL Router の設定

MySQL Router コンテナは既に起動していますが、InnoDB Cluster と連携するように構成する必要があります。

Router コンテナに接続して初期化

MySQL Router コンテナ内で初期化処理を実行します。

bash# MySQL Routerコンテナに入る
docker exec -it mysql-router bash
bash# MySQL Routerの初期化(コンテナ内で実行)
mysqlrouter --bootstrap root@mysql-node1:3306 \
  --user=mysqlrouter \
  --force \
  --conf-use-sockets=0

このコマンドにより、MySQL Router が InnoDB Cluster のメタデータを取得し、自動的に構成ファイルを生成します。

Router の起動と確認

初期化が完了したら、MySQL Router を起動します。

bash# MySQL Routerを起動(コンテナ内で実行)
mysqlrouter &
bash# ログでRouterの状態を確認
tail -f /var/log/mysqlrouter/mysqlrouter.log

ログにsuccessfully startedというメッセージが表示されれば、正常に起動しています。

コンテナから抜ける場合は、exitコマンドを実行してください。

ステップ 4:アプリケーションからの接続テスト

Node.js を使った簡単なアプリケーションで、MySQL Router 経由の接続をテストします。

必要なパッケージのインストール

プロジェクトディレクトリで以下のコマンドを実行します。

bash# package.jsonの初期化(未作成の場合)
yarn init -y
bash# MySQLクライアントライブラリのインストール
yarn add mysql2

接続テストスクリプトの作成

test-connection.jsというファイルを作成し、以下のコードを記述します。

javascript// mysql2ライブラリのインポート
const mysql = require('mysql2/promise');

// MySQL Routerへの接続設定
const connectionConfig = {
  host: 'localhost',
  port: 6446, // 読み書き用ポート
  user: 'root',
  password: 'rootpass',
  database: 'mysql',
};

この設定では、MySQL Router の読み書き用ポート(6446)に接続します。アプリケーションは背後のデータベースサーバーを意識する必要がありません。

接続とクエリ実行の実装

接続を確立し、簡単なクエリを実行する関数を実装します。

javascript// 接続テスト関数
async function testConnection() {
  let connection;

  try {
    // MySQL Routerへ接続
    console.log('MySQL Routerに接続中...');
    connection = await mysql.createConnection(
      connectionConfig
    );
    console.log('✓ 接続成功');

    // 現在接続しているサーバーを確認
    const [rows] = await connection.query(
      'SELECT @@hostname AS server_name'
    );
    console.log(`接続先サーバー: ${rows[0].server_name}`);
  } catch (error) {
    console.error('✗ エラー発生:', error.message);
  } finally {
    // 接続をクローズ
    if (connection) {
      await connection.end();
      console.log('接続をクローズしました');
    }
  }
}

このコードは、接続先のホスト名を取得することで、どのノードに接続されているかを確認します。

スクリプトの実行

メイン処理を追加して、スクリプトを完成させます。

javascript// メイン処理
(async () => {
  console.log('=== MySQL Router 接続テスト ===\n');
  await testConnection();
})();
bash# スクリプトを実行
node test-connection.js

正常に動作すれば、以下のような出力が表示されます。

diff=== MySQL Router 接続テスト ===

MySQL Routerに接続中...
✓ 接続成功
接続先サーバー: mysql-node1
接続をクローズしました

これにより、MySQL Router 経由で Primary ノード(mysql-node1)に接続できていることが確認できます。

ステップ 5:フェイルオーバーの動作確認

実際に Primary ノードを停止し、自動フェイルオーバーが機能するかテストします。

継続的な接続テストスクリプトの作成

test-failover.jsというファイルを作成し、一定間隔でクエリを実行し続けるスクリプトを実装します。

javascriptconst mysql = require('mysql2/promise');

// 接続プールの設定
const pool = mysql.createPool({
  host: 'localhost',
  port: 6446,
  user: 'root',
  password: 'rootpass',
  database: 'mysql',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
});

接続プールを使用することで、再接続処理が自動的に行われます。

定期的なクエリ実行の実装

1 秒ごとにクエリを実行し、接続状態を監視します。

javascript// 定期クエリ実行関数
async function runPeriodicQuery() {
  try {
    const [rows] = await pool.query(
      'SELECT @@hostname AS server, NOW() AS current_time'
    );

    const timestamp = new Date().toISOString();
    console.log(
      `[${timestamp}] ✓ サーバー: ${rows[0].server}, ` +
        `時刻: ${rows[0].current_time}`
    );
  } catch (error) {
    const timestamp = new Date().toISOString();
    console.error(
      `[${timestamp}] ✗ エラー: ${
        error.code || error.message
      }`
    );
  }
}

エラー発生時も継続して実行することで、フェイルオーバー後の復旧を確認できます。

メイン処理とインターバル設定

スクリプトを継続実行するメイン処理を実装します。

javascript// メイン処理
(async () => {
  console.log('=== フェイルオーバーテスト開始 ===');
  console.log('Ctrl+C で終了\n');

  // 1秒ごとにクエリを実行
  setInterval(runPeriodicQuery, 1000);

  // 終了時の処理
  process.on('SIGINT', async () => {
    console.log(
      '\n\nテスト終了、接続プールをクローズします...'
    );
    await pool.end();
    process.exit(0);
  });
})();
bash# フェイルオーバーテストを実行
node test-failover.js

スクリプトが実行されると、1 秒ごとに接続先サーバーと時刻が表示され続けます。

Primary ノードの停止

別のターミナルを開き、Primary ノード(mysql-node1)を停止します。

bash# mysql-node1コンテナを停止
docker stop mysql-node1

元のターミナルを見ると、以下のような出力が確認できるはずです。

yaml[2025-01-15T10:30:45.123Z]  サーバー: mysql-node1, 時刻: 2025-01-15 10:30:45
[2025-01-15T10:30:46.234Z]  サーバー: mysql-node1, 時刻: 2025-01-15 10:30:46
[2025-01-15T10:30:47.345Z]  エラー: ECONNREFUSED
[2025-01-15T10:30:48.456Z]  エラー: ECONNREFUSED
[2025-01-15T10:30:49.567Z]  サーバー: mysql-node2, 時刻: 2025-01-15 10:30:49
[2025-01-15T10:30:50.678Z]  サーバー: mysql-node2, 時刻: 2025-01-15 10:30:50

数秒のエラーの後、自動的に mysql-node2 へフェイルオーバーされていることが確認できます。アプリケーションコードは一切変更していないにもかかわらず、MySQL Router が透過的に接続先を切り替えたのです。

フェイルオーバー動作の詳細確認

MySQL Shell で現在のクラスタ状態を確認してみましょう。

bash# ノード2に接続(現在のPrimary)
mysqlsh root@localhost:3307 --password=rootpass
javascript// クラスタの状態を確認
var cluster = dba.getCluster();
cluster.status();

出力を見ると、mysql-node2 が新しい Primary として昇格し、mysql-node1 がOFFLINEまたは(MISSING)状態になっていることが分かります。

以下の図は、フェイルオーバー前後の状態遷移を示しています。

mermaidstateDiagram-v2
  [*] --> Normal: 通常運用

  state Normal {
    [*] --> Node1Primary
    Node1Primary: Node1 (Primary)
    Node2Replica: Node2 (Replica)
    Node3Replica: Node3 (Replica)
  }

  Normal --> Detecting: Node1障害検知

  state Detecting {
    [*] --> HealthCheck
    HealthCheck: ヘルスチェック失敗
    HealthCheck --> Election
    Election: 新Primary選出
  }

  Detecting --> Failover: 自動切替

  state Failover {
    [*] --> Node2Primary
    Node2Primary: Node2 (Primary)
    Node1Offline: Node1 (OFFLINE)
    Node3Replica2: Node3 (Replica)
  }

  Failover --> [*]: 復旧完了

このように、MySQL Group Replication が障害を検知すると、自動的に新しい Primary を選出し、MySQL Router がその変更を検知して接続先を切り替えます。

図で理解できる要点

  • 障害検知からフェイルオーバーまで数秒以内に完了
  • 新 Primary の選出は Quorum ベースで自動実行
  • MySQL Router がメタデータの変更を検知し、即座にルーティングを更新

ステップ 6:読み取り専用接続の確認

MySQL Router は、読み書き用(6446)と読み取り専用用(6447)の 2 つのポートを提供しています。読み取り専用ポートを使用した場合の動作も確認しましょう。

読み取り専用接続スクリプトの作成

test-readonly.jsファイルを作成します。

javascriptconst mysql = require('mysql2/promise');

// 読み取り専用ポートへの接続設定
const readOnlyConfig = {
  host: 'localhost',
  port: 6447, // 読み取り専用ポート
  user: 'root',
  password: 'rootpass',
  database: 'mysql',
};

ポート番号を 6447 に変更することで、Replica サーバーへの接続が行われます。

複数回の接続テスト実装

複数回接続を試み、接続先が Replica ノード間で分散されることを確認します。

javascript// 読み取り専用接続テスト
async function testReadOnlyConnection() {
  console.log('=== 読み取り専用接続テスト ===\n');

  // 5回接続を試行
  for (let i = 1; i <= 5; i++) {
    let connection;

    try {
      connection = await mysql.createConnection(
        readOnlyConfig
      );

      const [rows] = await connection.query(
        'SELECT @@hostname AS server, @@read_only AS is_readonly'
      );

      console.log(
        `試行 ${i}: サーバー=${rows[0].server}, ` +
          `読み取り専用=${
            rows[0].is_readonly === 1 ? 'Yes' : 'No'
          }`
      );
    } catch (error) {
      console.error(`試行 ${i}: エラー - ${error.message}`);
    } finally {
      if (connection) await connection.end();
    }

    // 次の接続まで少し待機
    await new Promise((resolve) =>
      setTimeout(resolve, 500)
    );
  }
}

// 実行
testReadOnlyConnection();
bash# 読み取り専用接続テストを実行
node test-readonly.js

以下のような出力が表示されます。

yaml=== 読み取り専用接続テスト ===

試行 1: サーバー=mysql-node2, 読み取り専用=Yes
試行 2: サーバー=mysql-node3, 読み取り専用=Yes
試行 3: サーバー=mysql-node2, 読み取り専用=Yes
試行 4: サーバー=mysql-node3, 読み取り専用=Yes
試行 5: サーバー=mysql-node2, 読み取り専用=Yes

接続先が mysql-node2 と mysql-node3 の間でラウンドロビン方式で振り分けられていることが確認できます。これにより、読み取り負荷が Replica ノード間で分散されるのです。

ステップ 7:本番環境向けの設定最適化

開発環境でのテストが完了したら、本番環境に向けて設定を最適化しましょう。

MySQL Router の詳細設定

MySQL Router の設定ファイル(mysqlrouter.conf)をカスタマイズすることで、より細かい制御が可能になります。

ini[DEFAULT]
# ログレベルの設定
logging_folder = /var/log/mysqlrouter
plugin_folder = /usr/lib/mysqlrouter

# ログレベル(DEBUG, INFO, WARNING, ERROR)
level = INFO

[logger]
# ログローテーション設定
level = INFO

ログレベルを適切に設定することで、トラブルシューティングが容易になります。本番環境ではINFO、デバッグ時はDEBUGが推奨されます。

接続タイムアウトの設定

接続とクエリのタイムアウト値を調整します。

ini[routing:primary]
# 読み書き用ルーティング
bind_address = 0.0.0.0
bind_port = 6446
destinations = metadata-cache://myCluster/default?role=PRIMARY
routing_strategy = first-available

# タイムアウト設定(秒)
connect_timeout = 5
client_connect_timeout = 10
ini[routing:secondary]
# 読み取り専用ルーティング
bind_address = 0.0.0.0
bind_port = 6447
destinations = metadata-cache://myCluster/default?role=SECONDARY
routing_strategy = round-robin-with-fallback

# タイムアウト設定
connect_timeout = 5
client_connect_timeout = 10

routing_strategyround-robin-with-fallbackに設定することで、Replica ノードに障害が発生した場合、自動的に他の Replica へフォールバックします。

メタデータキャッシュの設定

クラスタ構成情報のキャッシュ設定を調整します。

ini[metadata_cache:myCluster]
# メタデータの更新間隔(秒)
ttl = 5

# クラスタへの接続情報
bootstrap_server_addresses = mysql-node1:3306,mysql-node2:3306,mysql-node3:3306
user = mysql_router_user
metadata_cluster = myCluster

# 障害検知の設定
# ヘルスチェック間隔(ミリ秒)
use_gr_notifications = 1

ttlを短く設定すると、クラスタ構成の変更がより早く反映されますが、メタデータサーバーへの負荷が増加します。通常は 5〜10 秒が推奨されます。

接続プールの最適化

アプリケーション側の接続プール設定も重要です。

javascript// 本番環境向け接続プール設定
const productionPool = mysql.createPool({
  host: 'localhost',
  port: 6446,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,

  // プール設定
  waitForConnections: true,
  connectionLimit: 100, // 最大接続数
  maxIdle: 10, // アイドル接続の最大数
  idleTimeout: 60000, // アイドルタイムアウト(60秒)
  queueLimit: 0, // キュー制限なし
  enableKeepAlive: true, // キープアライブ有効化
  keepAliveInitialDelay: 10000, // キープアライブ初期遅延(10秒)
});

接続プールを適切に設定することで、データベースへの負荷を最小限に抑えつつ、高いパフォーマンスを維持できます。

エラー処理とトラブルシューティング

MySQL Router を使用する際に発生する可能性のある代表的なエラーと対処法を紹介します。

エラー 1:接続拒否エラー(ECONNREFUSED)

エラーコード: ECONNREFUSED

エラーメッセージ:

javascriptError: connect ECONNREFUSED 127.0.0.1:6446
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16)

発生条件:

  • MySQL Router が起動していない
  • ファイアウォールでポート 6446/6447 がブロックされている
  • Docker 環境でポートマッピングが正しく設定されていない

解決方法:

  1. MySQL Router の起動状態を確認
bash# Docker環境の場合
docker ps | grep mysql-router

# プロセスの確認
ps aux | grep mysqlrouter
  1. ポートのリスニング状態を確認
bash# ポート6446と6447がリッスンしているか確認
netstat -an | grep 644
  1. ログファイルでエラーを確認
bash# MySQL Routerのログを確認
tail -f /var/log/mysqlrouter/mysqlrouter.log

エラー 2:メタデータ取得エラー

エラーコード: Error 1045 (28000): Access denied for user

エラーメッセージ:

sqlERROR [routing] Failed fetching metadata: Access denied for user 'root'@'mysql-router' (using password: YES)

発生条件:

  • MySQL Router がクラスタのメタデータにアクセスする権限がない
  • パスワードが間違っている
  • ユーザーのホスト制限により接続が拒否されている

解決方法:

  1. 専用のルーターユーザーを作成
sql-- MySQL Shellまたはmysqlクライアントで実行
CREATE USER 'mysql_router_user'@'%'
  IDENTIFIED BY 'secure_password';

GRANT SELECT, EXECUTE ON mysql_innodb_cluster_metadata.*
  TO 'mysql_router_user'@'%';

GRANT SELECT ON performance_schema.replication_group_members
  TO 'mysql_router_user'@'%';

GRANT SELECT ON performance_schema.replication_group_member_stats
  TO 'mysql_router_user'@'%';

FLUSH PRIVILEGES;
  1. MySQL Router を再初期化
bashmysqlrouter --bootstrap root@mysql-node1:3306 \
  --user=mysql_router_user \
  --force

エラー 3:フェイルオーバー失敗

エラーコード: Error 2003 (HY000): Can't connect to MySQL server

エラーメッセージ:

cssERROR [routing] Can't connect to MySQL server on 'mysql-node1' (111)
All destinations down for routing

発生条件:

  • すべてのノードがダウンしている
  • Quorum が失われ、新しい Primary を選出できない
  • ネットワーク分断が発生している

解決方法:

  1. クラスタの状態を確認
javascript// MySQL Shellで実行
var cluster = dba.getCluster();
cluster.status();
  1. Quorum が失われている場合は強制的に復旧
javascript// 強制的にQuorumを再構成(慎重に実行)
cluster.forceQuorumUsingPartitionOf('mysql-node2:3306');
  1. ダウンしたノードを再起動
bash# Docker環境の場合
docker start mysql-node1
  1. ノードをクラスタに再参加
javascript// ノードを再起動後、自動的に再参加
// 必要に応じて手動で再参加
cluster.rejoinInstance('root@mysql-node1:3306', {
  password: 'rootpass',
});

これらのエラー対処法を理解しておくことで、本番環境でのトラブル発生時にも迅速に対応できるようになります。

まとめ

MySQL Router を導入することで、アプリケーションコードを変更することなく、データベースの透過的なフェイルオーバーを実現できます。

本記事では、Docker Compose を使った MySQL InnoDB Cluster の構築から、MySQL Router のセットアップ、実際のフェイルオーバー動作確認まで、段階的に解説しました。重要なポイントを振り返ってみましょう。

アーキテクチャの利点として、アプリケーションとデータベースの疎結合化が挙げられます。MySQL Router がプロキシとして機能することで、データベース構成の変更がアプリケーションに影響を与えにくくなりました。

自動フェイルオーバーにより、Primary 障害時にも数秒以内に別のノードへ自動切り替えが行われ、サービスの継続性が保たれます。開発者が複雑なフェイルオーバーロジックを実装する必要はありません。

読み書き分離も透過的に実現され、ポート 6446 は読み書き可能な Primary へ、ポート 6447 は読み取り専用の Replica へ自動的にルーティングされます。これにより、読み取り負荷を複数の Replica に分散でき、システム全体のパフォーマンスが向上するでしょう。

運用面のメリットも大きく、MySQL Shell を使ったクラスタ管理により、ノードの追加や削除、状態確認が容易になりました。ログとメトリクスの一元管理により、トラブルシューティングも効率化されます。

本番環境への適用時は、接続プールの最適化、タイムアウト設定の調整、適切なログレベルの設定など、細かなチューニングを行うことで、より安定した運用が可能になります。エラー処理についても理解を深め、障害発生時に迅速に対応できる体制を整えておきましょう。

MySQL Router は、高可用性を求められる本番環境において、開発生産性と運用効率の両方を向上させる強力なツールです。ぜひ実際の環境で試してみてください。

関連リンク