大規模環境における MySQL スキーママイグレーション:gh-ost 徹底解説

Database tutorial - IT technology blog
Database tutorial - IT technology blog

午前3時のメタデータロック:本番環境の現実

200GBの orders テーブルに null 許容の列を1つ追加しようとしています。5分ほどで終わるだろうと思っていた矢先、ALTER TABLE コマンドがメタデータロックを誘発し、すべてのクエリがフリーズします。数秒以内にコネクションプールは5,000セッションの制限に達し、ユーザーの画面には 504 Gateway Timeout が表示されます。監視ダッシュボードはただ赤いだけでなく、悲鳴を上げているかのようです。

大規模なデータセットに対する MySQL のスキーマ変更は、非常にリスクの高い操作です。MySQL 8.0 でオンライン DDL のサポートは改善されましたが、多くの構造変更には依然としてテーブル全体のコピーや制限の厳しいロックが必要です。秒間2,000回以上の書き込みが発生している場合、わずか3秒の停止でもサイト全体の障害に発展する可能性があります。私は MySQL、PostgreSQL、MongoDB などの環境でこうした失敗を経験してきましたが、MySQL のレガシーな DDL 処理はいまだにエンジニアリングにおける「インシデント」の最も一般的な原因の1つです。

なぜ標準的なマイグレーションは失敗するのか

ネイティブの ALTER TABLE は、多くの場合、隠れた一時テーブルを作成し、すべての行をコピーしてから、古いテーブルと新しいテーブルを入れ替えます。データベースはこの移行期間中、厳密な整合性を維持しなければなりません。「オンライン」フラグを立てていても、最終的な「カットオーバー(切り替え)」フェーズではテーブル名を変更するために排他的なロックが必要になります。負荷の高いシステムでは、そのわずかなロックがすべてを停滞させます。その結果、未処理のトランザクションが蓄積し、データベースが復旧するずっと前にアプリケーションサーバーがクラッシュしてしまうのです。

マイグレーション戦略の評価

コミュニティは、これらのロッキング動作を回避するためにいくつかのツールを開発してきました。それぞれに固有のトレードオフがあります。

1. ネイティブのオンライン DDL

MySQL 5.6 で導入された「組み込み」の方法です。便利ですが不透明です。I/O 使用率を簡単に制限することはできず、プロセスが 90% 進んだところで失敗した場合、ロールバック自体に数時間を要し、その間ずっと CPU を占有し続けることがあります。

2. pt-online-schema-change (Percona Toolkit)

10年間にわたり業界標準だったツールです。シャドウテーブルを作成し、データベーストリガーを使用して入力される書き込みを同期します。しかし、トリガーは諸刃の剣です。元のテーブルに対するすべての INSERTUPDATE に大きなレイテンシを加えます。高コンカレンシーな環境では、このオーバーヘッドがまさに回避しようとしていたデッドロックを引き起こすことがよくあります。

3. gh-ost (GitHub Online Schema Transformer)

GitHub は、トリガーへの依存を排除するために gh-ost を開発しました。データベースにトリガー経由でデータを同期させる代わりに、gh-ostMySQL バイナリログ (binlog) から直接変更をストリーミングします。レプリカのように振る舞い、ログストリームを静かに読み取ってゴーストテーブルに適用します。この「トリガーレス」設計により、たとえ 10 時間かかるマイグレーション中であっても、アプリケーションのパフォーマンスは安定したまま保たれます。

gh-ost はあなたに適しているか?

メリット

  • レイテンシへの影響がゼロ: トリガーがないため、アプリケーションの書き込みパスに追加のオーバーヘッドが発生しません。
  • 完全な制御: レプリケーション遅延が 500ms を超えた場合、即座にマイグレーションを一時停止できます。
  • ドライランによる検証: 本番環境に触れる前に、レプリカでマイグレーションロジック全体をテストし、スキーマが有効であることを確認できます。
  • クリーンな失敗: gh-ost プロセスが停止しても、データベースへの影響はありません。削除すべき孤立したトリガーも残りません。

制約事項

  • ストレージのオーバーヘッド: 元のテーブルの少なくとも 2.1 倍のスペースが必要です。500GB のテーブルには 1TB 以上の空きディスク容量が必要です。
  • 厳格な要件: 環境で ROW ベースのバイナリログ形式を使用しており、すべてのテーブルに主キー(Primary Key)が必要です。
  • 運用の複雑さ: 独立した Go バイナリです。標準的な SQL の範囲外で権限や接続性を管理する必要があります。

インフラストラクチャの要件

安全第一です。MySQL の設定が以下の項目と一致していることを確認してください。

  1. バイナリログ形式: log_bin が有効で、binlog_formatROW に設定されている必要があります。
  2. レプリカ戦略: gh-ost はプライマリで実行できますが、レプリカを指すようにするのがより安全です。フォロワーからデータを読み取り、最終的なカットオーバーのみをプライマリで実行します。
  3. 権限: マイグレーション用ユーザーには SUPERREPLICATION CLIENTREPLICATION SLAVE 権限が必要です。
# 設定を確認する
SHOW VARIABLES LIKE 'binlog_format';
# 'ROW' が返される必要があります

実装ワークフロー

5,000万行の users テーブルに user_bio 列を追加してみましょう。

ステップ 1:バイナリのインストール

コンパイル済みのバイナリを取得します。これは単一の Go 実行ファイルです。

wget https://github.com/github/gh-ost/releases/download/v1.1.6/gh-ost-binary-linux-20231207144602.tar.gz
tar -xf gh-ost-binary-linux-20231207144602.tar.gz
sudo mv gh-ost /usr/local/bin/

ステップ 2:ドライラン

これをスキップするのはトラブルの元です。ドライランでは、1バイトも動かすことなく権限、接続性、構文を検証します。

gh-ost \
  --user="migrator" \
  --password="secret" \
  --host="db-primary.internal" \
  --allow-on-master \
  --database="prod_db" \
  --table="users" \
  --alter="ADD COLUMN user_bio TEXT" \
  --dry-run

ステップ 3:実行

ドライランが成功したら、--dry-run--execute に置き換えます。必ず --panic-flag-file を定義してください。問題が発生した場合はそのファイルを touch するだけで、gh-ost は即座に処理を中止します。

gh-ost \
  --user="migrator" \
  --password="secret" \
  --host="db-primary.internal" \
  --allow-on-master \
  --database="prod_db" \
  --table="users" \
  --alter="ADD COLUMN user_bio TEXT" \
  --panic-flag-file="/tmp/ghost.panic" \
  --execute

ステップ 4:ライブスロットリング

監視が重要です。マイグレーションの負荷により他のレプリカで遅延が発生し始めた場合、Unix ソケットを介してリアルタイムでプロセスを制限(スロットリング)できます。

# コピープロセスを一時停止する
echo throttle | nc -U /tmp/gh-ost.prod_db.users.sock

# レプリカが追いついたら再開する
echo no-throttle | nc -U /tmp/gh-ost.prod_db.users.sock

ステップ 5:カットオーバー

データのコピー後、gh-ost は入れ替えの準備をします。クエリが失われないよう、高度な2セッション型アトミックスワップを使用します。切り替えは通常 100 ミリ秒未満で完了します。

現場で学んだアドバイス

最高のツールがあっても、本番環境のマイグレーションには規律が必要です。

  • ディスク容量を監視する: 150GB のディスクに 80GB の空きがあり、テーブルが 100GB ある場合、gh-ost はディスクを一杯にしてデータベースをクラッシュさせます。
  • 自動スロットリング: --max-lag-millis=1000 を使用します。これにより、いずれかのレプリカが 1 秒以上遅延した場合、gh-ost は自動的に作業を停止します。
  • マイグレーション後のクリーンアップ: gh-ost は古いテーブルを _users_del のような名前に変更します。安全策として 24 時間は保持し、その後削除してスペースを解放します。

スキーマ変更を不安の種にする必要はありません。トリガーベースのツールから gh-ost のようなバイナリログトランスフォーマーに移行することで、オンコールのエンジニアを起こすことなくデータベースを進化させるために必要な可視性と制御が得られます。

Share: