23 Jan 2024

MySQL オンラインスキーマ変更ツールの spirit と gh-ost

MySQL のスキーマ変更は、データ量が大きくなると一般的に時間がかかり、その間の DML の実行にも影響が出る。このような状況でサービスを止めずにスキーマ変更をするためのツールとして github/gh-ost が有名だが、cashapp/spirit は gh-ost をベースにして Cash App チーム (旧 Square、現 Block, Inc.) のニーズに合わせて作られた新しい [1] オンラインスキーマ変更ツールだ。

[1] 紹介ブログ Introducing Spirit | Cash App Code Blog が書かれたのが 2023 年 9 月。

spirit の特徴

基本的なアプローチは gh-ost も spirit も同じだが、より大きなデータサイズ [2] のユースケースに対応した機能が追加されている

  • データコピーを並列に行う
    • gh-ost は一並列のため、この規模のデータコピーには数週間という水準の時間がかかる
  • 処理途中からの resume に対応
    • 現代的なインフラはおおよそ k8s 上に構築されており、disposable な作りになっていた方が扱いやすい
    • 前述のように数日程度では済まない長い時間がかかる。この規模になると中断後、最初から処理を再開するのはかなり運用負荷が高い
    • この機能によりマイグレーション中だけ DB インスタンスをスケールアップしておき、一時中断して戻すなど柔軟な対応も可能になる

README にはベンチマークも記載されている。

  • Aurora v3 上の 10TB の finch.xfers テーブルのマイグレーションが 2.7 日で完了
    • gh-ost だと 10 日でも完了せずキャンセルした
  • テーブルはアイドル状態だったので、実際のワークロードではより差が開くと予想される
    • m1 mac 上でのマイクロベンチで比較
    • finch.balances (800MB/1M rows), idle load では spirit は gh-ost の約 2 倍高速
    • 同じテーブルでベンチマークツールでクエリを発行しながらマイグレーションしたところ、差は 8.5 倍まで広がった

[2] spirit は 10 TiB 規模のテーブルのマイグレーションを 5 日以内に完了させる のが性能の目安になっている。対して gh-ost や pt-ost は ~1TiB 程度の規模が想定されていて、かつそれでも 1 週間以上かかることもある という水準らしい。

gh-ost の実装

gh-ost はおおまかに次の戦略でオンラインスキーマ変更を実現している。

gh-ost: GitHub's online schema migration tool for MySQL - The GitHub Blogから引用
  • セットアップ
  • データコピー
    • 元テーブルのデータを ghost デーブルにコピーする
    • この時 gh-ost は binlog を購読し、ghost テーブルが最新の状態に常に(極力小さいラグで)追従するようにしている
  • カットオーバー
    • ghost テーブルと元テーブルを RENAME TABLE 文で入れ替える

主要な処理はコントローラー的役割の logic.Migrator.Migrate から順に呼び出されている。コードリーディングはここから始めると良い。

データコピー

EventStreamer が実際に binlog を読み込む部分は go-mysql-org/go-mysqlreplication パッケージを利用している。

カットオーバー

ghost テーブルが元テーブルに追いついたら、両テーブルの切り替えを行う。この作業をカットオーバーと呼ぶ。

gh-ost がそれ以前のツールよりも優れているのは、binlog 購読によるキャッチアップがトリガーなどによる実装に比べて高速低コストにできることで、そのおかげでカットオーバー時のロック取得が短時間で済むことだと思われる。

gh-ost のカットオーバーは次のように少々複雑な手順を踏む。

詳細は次の issue にドキュメント化されている。Describing safe, blocking, atomic, pure-mysql cut-over phase · Issue #82 · github/gh-ost

これは MySQL 8.0.13 以前はテーブルロック中にリネームができなかったことに起因している。MySQL 8.0.13 以降はこの制約がないので、ロック・リネーム・アンロックというシンプルなフローで良くなるが、gh-ost は古い MySQL バージョンをサポートしているのでまだ実装されていない。

MySQL :: MySQL 8.0 Reference Manual :: 13.1.36 RENAME TABLE Statement

As of MySQL 8.0.13, you can rename tables locked with a LOCK TABLES statement, provided that they are locked with a WRITE lock or are the product of renaming WRITE-locked tables from earlier steps in a multiple-table rename operation.

なお以前はリネームのステップが (1) 元テーブルを一時テーブルにリネーム (2) ghostテーブルを元テーブルにリネームと 2 ステップに分かれていたらしい [3]。(1) と (2) の間に何らかの理由でロックを取っていたセッションが死ぬと、元テーブルがなくなった状態になってしまい事故となる。現行アルゴリズムはリネームがアトミックに行われるのでこの懸念はない。またこの点に限らず、フローのどの時点でクラッシュしても、データ一貫性が壊れる事故は起きなくなっている。

[3] Describing safe, blocking, pure-mysql cut-over phase · Issue #65 · github/gh-ost

spirit の実装

基本的なアプローチは gh-ost と同じで、実装もセットアップ、データコピー、カットオーバーの流れでされている。ただし以下のアイデアがデータコピーの実装に追加されている。

  • 並列化によるデータコピーの短縮
  • データコピーの途中再開

また MySQL 8 以降を原則サポート対象としていたり、gh-ost にある細かなオプションの対応が省かれていることで、コア部分以外のコード量が少なくすっきりしている。全体的にドキュメントも充実している。もしスキーマ変更ツールの実装を読んでみたい人がいれば、先に spirit から読むと理解しやすいと思う。

migration.Runner.Run がコントローラー的なメソッドで、ここから順に追っていくと良い。また ghost テーブルは spirit では New Table と呼ばれている。

データコピー

spirit では row.Copier クラスが担当する。

binlog の購読は gh-ost 同様 go-mysql-org/go-mysql を利用している。ただし replication ではなく canal というよりハイレベルなライブラリを使っている。

  • canal はある MySQL サーバーの変更を別のデータスストアに反映させるために使うことができる、汎用的なツール

カットオーバー

デフォルトでは MySQL 8 をサポート対象としているので、gh-ost に比べ シンプルでストレートな実装 になっている。

フォールバックとして gh-ost と同様のアルゴリズム も実装されている。

ALGORITHM=INSTANT の試行

spirit にも gh-ost にも、処理に入る前にまずは ALGORITHM=INSTANT でのスキーマ変更ができないかチェックする仕組みが入っている。

MySQL 8 から導入された ALGORITHM=INSTANT は、メタデータの更新だけで ALTER 文が完了するため、非常に高速。ただし全てのスキーマ変更操作で可能なわけではなく制約がある。

spirit も gh-ost も、重くリスクの高い作業を開始する前に ALGORITHM=INSTANT を試みるのは理にかなっている。またこの機能は spirit 側から gh-ost に 提案・移植 してくれたらしい。

PR