MySQLからTiDBへゼロダウンタイム移行する手順
はじめに
サービスを稼働させたまま巨大なMySQLデータベースをTiDBに移行するという課題を解決する**TiDB Data Migration (DM)**を実際に自分の手で試してみることにしました。
この記事では、ローカルのDocker環境でMySQLからTiDBへのゼロダウンタイム移行を試みる手順を解説します。の記事には、私が実際に遭遇し、解決してきたエラーとその解決策を記録しています。
最終的な環境構成
関連する全てのサービスを単一のdocker-compose.ymlファイルにまとめ、一つの共通ネットワークで管理しました。
- docker-compose.yml
以下の内容で、プロジェクトのルートディレクトリにdocker-compose.ymlを1つだけ作成します。 
services:
  # --------------------
  # 移行元のMySQL
  # --------------------
  mysql:
    build:
      context: ./src/infra/db/mysql # MySQLのDockerfileがある場所
    container_name: mysql-test
    ports:
      - "3306:3306"
    volumes:
      - ./src/infra/db/mysql/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf
    networks:
      - migration-net
  # --------------------
  # 移行先のTiDB & DMツール環境
  # --------------------
  tidb-test:
    build:
      context: ./src/infra/db/tidb # TiDB/DMツールのDockerfileがある場所
    container_name: tidb-test
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - "4000:4000"
      - "2379:2379"
      - "8261:8261"
    networks:
      - migration-net
# --- 全てのコンテナが接続する共通のネットワークを定義 ---
networks:
  migration-net:
    driver: bridge
- TiDB/DM用の Dockerfile
tiupを使って、TiDBクラスタ (playground) とDM関連ツール (dm, dmctl) をすべてインストールするコンテナを作成します。 
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive \
    TZ=Asia/Tokyo
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl gnupg2 ca-certificates lsb-release bash sudo ssh \
    mariadb-client tzdata \
 && rm -rf /var/lib/apt/lists/*
RUN curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh && \
    export PATH=/root/.tiup/bin:$PATH && \
    tiup update --self && \
    tiup update playground && \
    tiup install tidb:v8.5.3 tikv:v8.5.3 pd:v8.5.3 tiflash:v8.5.3
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY source.yaml /usr/local/bin/source.yaml
COPY task.yaml /usr/local/bin/task.yaml
RUN chmod +x /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/source.yaml
RUN chmod +x /usr/local/bin/task.yaml
ENV TIDB_VERSION=v8.5.3 \
    TIDB_HOST=0.0.0.0 \
    TIDB_TAG=test-cluster \
    DB_HOST=127.0.0.1 \
    DB_PORT=4000 \
    DB_USER=root \
    DB_NAME=test \
    PATH="/root/.tiup/bin:${PATH}"
EXPOSE 4000 2379 3000
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
2-1. entrypoint.sh
TiDBに関する設定を行うシェルになります。
#!/usr/bin/env bash
set -euo pipefail
export PATH="/root/.tiup/bin:${PATH}"
# 1) TiDB playground をバックグラウンド起動
echo "[db] starting tiup playground ${TIDB_VERSION} ..."
tiup playground "${TIDB_VERSION}" --host "${TIDB_HOST}" --tag "${TIDB_TAG}" &
tiup dm-master --master-addr=127.0.0.1:8261 &
tiup dm-worker --worker-addr=127.0.0.1:8262 --join=127.0.0.1:8261 &
PLAY_PID=$!
# 2) SQLポート待機(mysqladmin ping / ポートが開くまで)
echo "[db] waiting TiDB on ${DB_HOST}:${DB_PORT} ..."
for i in $(seq 1 120); do
  if mysqladmin ping -h"${DB_HOST}" -P"${DB_PORT}" --silent 2>/dev/null; then
    echo "[db] TiDB is up"
    break
  fi
  sleep 2
done
# 3) DB作成(なければ)
echo "[db] ensuring database '${DB_NAME}' exists ..."
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -e "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
# 4) 初期DDL投入(/docker-entrypoint-initdb.d/*.sql / *.sql.gz を順次流す)
INIT_DIR="/docker-entrypoint-initdb.d"
if [ -d "${INIT_DIR}" ]; then
  shopt -s nullglob
  for f in "${INIT_DIR}"/*.sql "${INIT_DIR}"/*.sql.gz; do
    case "$f" in
      *.sql)
        echo "[db] applying: $f"
        mysql --default-character-set=utf8mb4 -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -D "${DB_NAME}" < "$f"
        ;;
      *.sql.gz)
        echo "[db] applying (gz): $f"
        gunzip -c "$f" | mysql --default-character-set=utf8mb4 -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -D "${DB_NAME}"
        ;;
    esac
  done
fi
echo "[db] init done. following playground ..."
# 5) playground をフォアグラウンド化(終了待ち)
wait "${PLAY_PID}"
- MySQL用の Dockerfile と設定
MySQLコンテナを作成するDockerfileになります。 
FROM mysql:8.0
ARG TZ=Asia/Tokyo
ENV TZ=${TZ}
ENV MYSQL_DATABASE=test \
    MYSQL_ROOT_PASSWORD=password \
    MYSQL_USER=app \
    MYSQL_PASSWORD=app_password
COPY ./data/ /docker-entrypoint-initdb.d/
COPY ./conf.d/ /etc/mysql/conf.d/
# ヘルスチェック(起動完了を検知)
HEALTHCHECK  \
  CMD mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD || exit 1
EXPOSE 3306
3-1. MySQLの設定ファイル
[mysqld]
log-bin=mysql-bin
binlog-format=ROW
gtid-mode=ON
enforce-gtid-consistency=ON
3-2. MySQLのDDL
CREATE USER 'dm_user'@'%' IDENTIFIED BY 'password';
GRANT RELOAD, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'dm_user'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER ON test_db.* TO 'dm_user'@'%';
-- テストデータ作成
CREATE TABLE test_db.users (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);
INSERT INTO test_db.users (id, name) VALUES (1, 'alice'), (2, 'bob');
- DMタスク用の設定ファイル
source.yamlのhostには、docker-compose.ymlで定義したMySQLのサービス名 (mysql) を指定します。 
source-id: "mysql-01"
enable-gtid: true
from:
  host: "mysql"
  port: 3306
  user: "dm_user" # この後作成するユーザー
  password: "password"
---
name: "mysql-to-tidb-task"
task-mode: "all"
# 移行先のTiDBクラスタ情報
target-database:
  host: "tiup-playground"
  port: 4000
  user: "root"
# 移行元のMySQLインスタンス情報
mysql-instances:
  - source-id: "mysql-01"
    block-allow-list: "global-rules" 
block-allow-list:
  global-rules:
    do-dbs: ["test_db"]
実行手順
- 環境を起動します。
 
docker-compose up -d
- 移行元のMySQLを準備します。
 
docker exec -it mysql-test /bin/bash
# --- コンテナ内 ---
mysql -u root -p password
# --- MySQLクライアント内 ---
USE test_db;
INSERT INTO test_db.users VALUES (1, 'alice'), (2, 'bob');
exit;
# --- コンテナ内 ---
exit;
- tidb-testコンテナでDMタスクを開始します。
dmctlでタスクを開始します。 
tiup dmctl --master-addr=127.0.0.1:8261 operate-source create /usr/local/bin/source.yaml
tiup dmctl --master-addr=127.0.0.1:8261 start-task /usr/local/bin/task.yaml
- MySQLのコンテナでレコード追加
2の手順でコンテナ内のMySQLクライアントにアクセスしINSERTの値を変えてレコードを追加します。 - tidb-testコンテナでMySQLクライアントにアクセスし、該当DBのテーブルにレコードが追加されたことを確認します。
 
ハマりどころと注意点
- 
ネットワークエラー (no such host):
- 原因: 複数のdocker-compose.ymlを使うと、コンテナが別々のネットワークに隔離されてしまうためです。
 - 解決策: コンテナを同じネットワークでアクセス可能にしてください。docker-compose.yamlでそれぞれのコンテナ以下の設定を行うと同じネットワークになります。
 
- networks: migration-net: driver: bridge - 
リソース不足によるエラー (Region unavailable, connection refused):
- 原因: 複数のDBコンポーネントをローカルで動かすと、PCのメモリやCPUが不足しがちです。
 - 解決策: Docker Desktopの設定で、割り当てるCPUコア数とメモリを増やしてください。 また、問題が起きたらdocker-compose down -vで環境をクリーンにしてから再起動するのが有効です。
 
 - 
DM設定ファイルのバージョン不整合:
- 原因: task.yamlやsource.yamlの書き方はDMのバージョンによって大きく異なります。
 - 解決策: エラーメッセージをよく読み、field ... not foundと出たら、そのバージョンの公式ドキュメントで正しい構文を確認してください。
 
 
まとめ
今回は、TiDB Data Migration (DM) を使って、MySQLからTiDBへのリアルタイム移行を試す環境をロー-カルに構築しました。その過程では、ネットワーク設定や設定ファイルのバージョン違いなど、多くの「ハマりどころ」がありました。
この経験から、特に以下の点が重要だと分かりました。
- 
構成はシンプルに:
複数のコンテナを連携させるときは、単一のdocker-compose.ymlにまとめると、コンテナ間のネットワーク問題を未然に防ぎやすくなります。 - 
エラーメッセージがヒント:
no such host(ホストが見つからない)やunknown field(設定項目が不明)といったエラーは、設定ファイル間の名前の不整合やツールのバージョン違いを教えてくれる重要な手がかりになります。 - 
状態をこまめに確認:
docker psやdocker logsといった基本的なコマンドでコンテナの稼働状態やログを確認することが、問題解決への一番の近道です。 
この記事で完成した環境は、TiDBの強力な移行機能を安全に試すための素晴らしい出発点です。ここからさらに、移行するテーブルを増やしてみたり、様々なデータ型で試してみたりと、自由に実験をしてみてください。
この記事が、あなたのTiDB学習の助けになれば幸いです。
Discussion