スキーマ変更のオンライン化(online DDL)の内部
巨大テーブルへの ALTER でサービスを止めずに済みます。影テーブル・トリガー捕捉・バックフィル・カットオーバーという外部 online DDL の四段構えを、gh-ost と pt-osc の違いまで内部から解き明かします。
- 1.テーブルロックを避ける外部 online DDL は、新スキーマの影(ghost)テーブルを作り、元テーブルへの変更を捕捉しつつ、既存行をチャンク単位でバックフィルし、最後に短時間のリネームでカットオーバーする四段構えで動く。
- 2.pt-osc は元テーブルにトリガーを張って変更を同期捕捉し、gh-ost はトリガーを使わず binlog を読んで非同期に変更を適用する。後者は本番テーブルへの追加負荷とロック競合を避け、スロットリングを効かせやすい。
- 3.カットオーバーの瞬間だけは原子的なリネームが必要で、ここが唯一の短いブロッキング点になる。外部キー・トリガー・一意でない主キーは適用可否を左右する前提条件になる。
なぜ ALTER はテーブルを止めるのか
ALTER TABLE でカラムを足したり索引を張ったりする操作は、素朴に実装するとテーブル全体を作り直すことに等しくなります。新しい行レイアウトの一時テーブルへ全行をコピーし、索引を構築し、入れ替える――この間、元テーブルへの書き込みを許すと整合が崩れるため、伝統的な実装はテーブルロックを取り、書き込み(しばしば読み取りも)をブロックします。数億行のテーブルでは、このコピーが数分から数時間に及び、その間サービスが止まります。
MySQL/InnoDB にはネイティブの online DDL(ALGORITHM=INPLACE/INSTANT)もあり、メタデータだけ書き換える INSTANT 操作や、DML を許しながら索引を構築する INPLACE 操作で多くのケースを無停止化できます。しかしネイティブ online DDL にも制約があります。操作によっては結局テーブル再構築が必要で、その間 WAL(先行書き込みログ) と undo が膨らみ、長時間トランザクションがレプリカ遅延を生み、途中で中断もできません。
そこで生まれたのが、DBMS の外側で同じ効果を作り出す外部 online DDL ツールです。代表が Percona の pt-online-schema-change(pt-osc) と GitHub の gh-ost で、両者とも次の四段構えで動きます。
四段構えの全体像
| 段階 | やること | 目的 |
|---|---|---|
| 影テーブル作成 | 新スキーマを当てた空のコピー(ghost テーブル)を作る | 本番テーブルに触れずに新形を用意する |
| 変更の捕捉 | コピー中に元テーブルへ来る INSERT/UPDATE/DELETE を影テーブルへ反映 | 進行中の更新を取りこぼさない |
| バックフィル | 既存行をチャンク単位で影テーブルへコピーする | 全データを新形へ移す(負荷を分散) |
| カットオーバー | 原子的なリネームで影テーブルを本物に昇格する | 一瞬で切り替え、ダウンタイムを最小化 |
肝は「コピー中も元テーブルは生きている」点です。バックフィルには時間がかかるので、その間に来る新しい変更を捕捉して影テーブルへ転写し続けなければ、コピー完了時点で両者がずれてしまいます。この変更捕捉の実装方式こそが、pt-osc と gh-ost を分ける最大の分岐点です。
pt-osc: トリガーで同期捕捉する
pt-osc は元テーブルに AFTER INSERT / AFTER UPDATE / AFTER DELETE のトリガーを仕掛けます。アプリが元テーブルへ書き込むと、その同じトランザクション内でトリガーが発火し、対応する変更を影テーブルへ即座に反映します。捕捉が同期的(書き込みと同一トランザクション)なので、原理的に取りこぼしがありません。
-- 概念図: pt-osc が張るトリガー(簡略)
CREATE TRIGGER pt_osc_ins AFTER INSERT ON tbl
FOR EACH ROW
REPLACE INTO _tbl_ghost (id, col_a, col_new) VALUES (NEW.id, NEW.col_a, ...);
-- UPDATE/DELETE 用も同様に張る
この方式は実装が素直な反面、いくつかの代償を伴います。
トリガーは元テーブルへの全書き込みのクリティカルパスに乗ります。1 回の INSERT が影テーブルへの書き込みも誘発するため、書き込みレイテンシが増え、ロック競合も増えます。さらに MySQL では 1 テーブルに同種トリガーを多重に張れない時期があり、既存のユーザートリガーと衝突しえます。負荷が上がっても、トリガーはアプリのトランザクションに食い込んでいるためスロットリングで切り離しにくいのも弱点です。
pt-osc は既存行のコピーをチャンク(主キー範囲で区切った小さなバッチ)に分け、各チャンクを INSERT ... SELECT で影テーブルへ流します。トリガーが同時進行の変更を、チャンクコピーが過去の行を担当し、両者で全データをカバーします。重複は影テーブル側の REPLACE/INSERT IGNORE による冪等適用で吸収します。
gh-ost: binlog で非同期捕捉する
gh-ost の発想は「元テーブルにいっさいトリガーを張らない」ことです。代わりに、自身をレプリカに見せかけて MySQL の binlog(行ベース、ROW フォーマット)を購読します。元テーブルへの変更はコミット後に binlog へ流れるので、gh-ost はそれを読み、対応する変更を影テーブルへ非同期に適用します(この binlog ベースの変更ストリームは 論理レプリケーションと CDC と同じ原理です)。
| 観点 | pt-osc(トリガー方式) | gh-ost(binlog 方式) |
|---|---|---|
| 変更捕捉 | AFTER トリガーで同期捕捉 | binlog を読んで非同期捕捉 |
| 本番テーブルへの追加負荷 | 全書き込みにトリガーが乗る | binlog 読み取りのみ、書き込み経路に無負荷 |
| スロットリング | 効かせにくい(Tx に食い込む) | 適用を一時停止でき柔軟 |
| 実行ホスト | 通常プライマリ上で完結 | binlog 読み取りをレプリカに逃がし、適用と影テーブルはプライマリ |
| 中断・再開 | 限定的 | テストラン・段階的カットオーバーを設計しやすい |
トリガーを外したことの利点は大きく、本番の書き込み経路にコードを差し込まないため、書き込みレイテンシを増やさず、負荷が上がれば binlog 適用をスロットリング(一時停止)できます。さらに gh-ost は binlog の読み取り元をレプリカに向けることができ、プライマリのバイナリログ読み取り負荷を逃がせます(ただしバックフィルと binlog 適用の書き込み先、すなわち影テーブルはプライマリ上にあり、最後のリネームもそこで行われます)。代償は、binlog 適用が非同期ゆえに影テーブルが元テーブルから常にわずかに遅れること、そして binlog(ROW フォーマット)が有効であることが前提になる点です。
gh-ost も既存行をチャンク単位でコピーします。難所は「過去行のコピー」と「binlog から来る新しい変更」の境界です。コピー中の行に対する UPDATE が binlog 経由で先に届くと、その後にコピーされた古い値で上書きしてしまう恐れがあります。これを避けるため、影テーブルへの適用は主キー単位で冪等(同じ行は最新が勝つよう REPLACE 相当)にし、コピーと binlog 適用を同一スレッドで直列化して順序の逆転を防ぎます。論理デコーディングと CDC の「スナップショット+ストリームの接合」と同じ問題構造です。
バックフィルの負荷制御
四段のうち最も時間がかかるのがバックフィルです。数億行を一気にコピーすればロックとレプリカ遅延を引き起こすので、両ツールともチャンク化と動的スロットリングで負荷を平準化します。
チャンクは B+Tree インデックス 上の主キー範囲(例 id BETWEEN 1000 AND 2000)で区切り、範囲スキャンで効率よく読みます。各チャンク間にはレプリカ遅延や負荷指標を見て待機を挟みます。gh-ost は**レプリカの遅延(seconds_behind)**やカスタムしきい値を監視し、超えたら自動で適用を止め、回復したら再開します。これにより「レプリケーション を遅延させない範囲で、空いた I/O を使ってじわじわ移す」制御が成立します。
バックフィルのループ(概念)
for 主キー範囲チャンク in テーブル:
if レプリカ遅延 > しきい値 or 負荷高い:
待機(スロットリング)
影テーブルへ INSERT ... SELECT(冪等)
進捗を記録
カットオーバー: 唯一の短いブロッキング点
全行のバックフィルが終わり、変更捕捉が追いついて影テーブルと元テーブルがほぼ一致したら、最後のカットオーバー(切り替え)を行います。ここだけは原子的なリネームが必要で、online DDL 全体で唯一の短いブロッキング点になります。
理想は、元テーブルと影テーブルを RENAME TABLE tbl TO _tbl_old, _tbl_ghost TO tbl; のように一回の原子的リネームで入れ替えることです。MySQL の RENAME TABLE は複数テーブルをアトミックに入れ替えられるため、その瞬間にテーブル名 tbl が指す実体だけが切り替わり、外から見える不整合の窓がありません。
カットオーバー直前にも変更は流れ込みます。リネームの瞬間に「まだ影テーブルへ未適用の変更」が残っていると取りこぼします。gh-ost はテーブルロックとリネームを組み合わせ、未適用の binlog を出し切ってから一瞬でスワップする精緻な手順(マジックテーブルやロック取得の順序付け)でこの窓を閉じます。リネームがブロックされて長引くと、その間アプリ側で待ちが発生するため、カットオーバーは負荷の低い時間帯に、短時間で完了させるのが鉄則です。
適用できる前提条件
外部 online DDL は万能ではありません。次の前提が崩れると安全に動きません。
| 前提条件 | 理由 | 崩れると |
|---|---|---|
| 一意な主キー(または一意キー)がある | チャンク分割と行の冪等適用に主キーが要る | 行を一意に特定できず実行不可 |
| 外部キーが絡まない | リネームで参照先が切り替わり整合が壊れうる | 特別な扱いが要り、既定では拒否されがち |
| (pt-osc)既存トリガーと衝突しない | AFTER トリガーを多重に張れない | 実行不可・手動回避が必要 |
| (gh-ost)ROW 形式の binlog が有効 | 変更捕捉が binlog 依存 | 捕捉できず実行不可 |
とくに外部キーは厄介です。元テーブルをリネームで退避すると、それを参照する子テーブルの外部キーが古いテーブルを指し続けたり、新テーブルへ張り替える必要が生じたりします。どちらのツールも外部キーは特別なオプション扱いで、既定では安全側に倒して拒否することが多く、設計段階での確認が不可欠です。
外部 online DDL の四段は「影テーブル作成 → 変更捕捉 → バックフィル → カットオーバー」。捕捉方式は pt-osc がトリガーで同期、gh-ost が binlog で非同期。gh-ost の利点は本番書き込み経路に無負荷でスロットリングが効くこと、代償は非同期遅延と ROW binlog 前提。唯一のブロッキングは原子的リネームによるカットオーバーで、前提条件は一意主キーの存在と外部キー・既存トリガーの非干渉、と層を分けて答えられれば十分です。
まとめ
巨大テーブルへの ALTER は、素朴には全行コピーとテーブルロックを伴い長時間サービスを止めます。外部 online DDL ツール(pt-osc / gh-ost)は、新スキーマの影テーブルを作り、コピー中の更新を捕捉し、既存行をチャンク単位でバックフィルし、最後に原子的リネームでカットオーバーするという四段構えで、停止時間をリネームの一瞬まで圧縮します。両者の本質的な違いは捕捉方式にあり、pt-osc はトリガーで同期捕捉する代わりに本番書き込みへ負荷を乗せ、gh-ost は binlog を読んで非同期捕捉することで書き込み経路を無負荷に保ちスロットリングを効かせます。どちらも一意主キーの存在を前提とし、外部キーや既存トリガーが安全性の境界を決めます。原子的なカットオーバーの一点を除けば、巨大テーブルのスキーマ進化を実質無停止で行える――それがオンライン化の正体です。
データベース Article
スキーマ変更のオンライン化(online DDL)の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
online DDL
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
pt-osc は元テーブルにトリガーを張って変更を同期捕捉し、gh-ost はトリガーを使わず binlog を読んで非同期に変更を適用する。後者は本番テーブルへの追加負荷とロック競合を避け、スロットリングを効かせやすい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「online DDL / gh-ost」に近いか確認する。
- 強みである「テーブルロックを避ける外部 online DDL は、新スキーマの影(ghost)テーブルを作り、元テーブルへの変更を捕捉しつつ、既存行をチャンク単位でバックフィルし、最後に短時間のリネームでカットオーバーする四段構えで動く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。