PlanetScaleとは何か、なぜ外部キー制約をサポートしていないのか


PlanetScaleとは

PlanetScaleはMySQLのマネージドサービスです。
内部の実装には元々YouTubeのために開発されたMySQLのクラスタリングシステムであるVitessが使用されています。
Vitessの開発に携わってらっしゃる がCTOとして在籍しており、スケーラブルなデータベースを構築するためのサービスとなっています。
すでにSlack, Square, GitHubなどの企業で採用されているそうです。

この記事ではPlanetScaleのどういった点が優れているのか、これまでMySQLが抱えていた問題点をどのように解決しているのかといったことをまとめます。
その中でタイトルにもつけましたが、なぜ外部キー制約をサポートしていないのかといった点も交えて説明します。

これまでのMySQLの問題点

大量のレコードが存在するテーブルのマイグレーション

本番環境で大量のレコードが存在するテーブルに対してマイグレーションする場合は考慮事項が多いです。
本番と同様のデータでテストしたとしても本番環境はDBへのアクセスが多いため、マイグレーションの最中にかかったロックでDBアクセスが詰まってサービスが落ちる可能性もあります。
そのため、大量のレコードが存在するテーブルに変更を加える際にはロックの挙動を考慮したり、1つのtransactionが長くなりすぎないようになど気を付ける必要があります。

本番と開発環境のスキーマ乖離

本番環境はこれまでのマイグレーションの積み重ねによってスキーマが決定されています。
一方、開発環境ではDBをリセットしてスキーマを構築し直したり、積み重ね式ではなくスキーマ情報を読み込み、一括でDBスキーマをセットアップするというケースもあると思います。
こうした中で本番DBと開発DBでカラムの型情報やcollationに乖離が発生する可能性があります。
このような差分が原因で本番環境でのみバグが発生する恐れがあります。

PlanetScaleの機能

PlanetScaleは上述の課題を以下の機能によって解決しています。

ダウンタイムのなしのマイグレーション

PlanetScaleが内部実装で使用しているVitessはOnline DDLと呼ばれるスキーママイグレーションのための機能を提供しています。
これによりノンブロッキングでキャンセルやリトライ可能なマイグレーションを実現しています。
Managed, Online Schema Changes

Online DDLは次の仕組みでスキーマ変更を行なっています。
以下の図はOnline DDLツールの1つであるgh-ostから引用しています。

  • 変更するテーブルのスキーマ情報だけをコピーしたデータがない空のテーブルを作成する。この空のテーブルをghost tableと呼ぶ。
  • このghost tableに対してALTER TABLEを実行する。データがないので当然実行コストがない。
  • このスキーマ変更が有効なものかvalidationする。
  • 変更差分を解析する。
  • 元のテーブルのデータをghost tableにコピーする。コピーする際は長時間ロックが発生しないように少量のデータごとにバッチ実行される。
  • コピーしている間は元のテーブルのデータ変更を検知していて、変更があった場合はghost tableにもデータ変更を反映する。
  • ghost tableにコピーが完了した後はghostテーブルと元のテーブルをrenameし、ghost tableが元のテーブルと入れ替わる。

このような変更手順を踏むことにより、ダウンタイムなしのスキーマ変更を実現しています。

ブランチ機能

PlanetScaleでは本番環境のDBから開発用のDBをブランチとして生成できます。
この機能によってアプリケーションコードに変更を加える際の開発フローと同様の手順でDBの変更を加えることができます。また本番環境からブランチを生成するので、スキーマ情報も本番と一致したものを使用できます。
Branching

  • productionのDB branchからdevelopmentのDB branchを生成する。development branchはスキーマ情報のみコピーされており、実際のデータは複製されません。
  • development branchに変更を加え、開発環境でテストする。
  • 問題ないことを確認した後にデプロイリクエストを作成する。デプロイリクエストではスキーマ変更のdiffが生成され、レビューを受けることができる。
  • デプロイリクエストがapproveされた後に、deploy queueに追加する。
  • スキーマ変更がproductionのbranchに適応され、前述の通りダウンタイムなしで実行される。

また、コンフリクトした場合はスキーマ変更できないようになっており複数人で同時開発する場合も問題ありません。

PlanetScale also prevents schema changes with conflicts from being migrated and handles schema changes from multiple teammates.

アプリケーション開発者が普段行なっている以下の開発フローと同じ体験をデータベースの変更でも得ることができます。

  • mainブランチからdevelopmentのブランチを生成。
  • ソースコードに変更を加えてテストする。
  • プルリクエストを作成してレビューを受ける。
  • approveされるとmainブランチにmergeしてproductionにデプロイする。

データを失うことなくスキーマ変更をrevertできる

最近リリースされたばかりのRewindという機能でデータを失うことなくマイグレーションをrevertできます。
2022/03/28現在、ベータ版として提供されています。

マイグレーションをproductionにデプロイした後30分以内であれば、データ消失なしでスキーマ変更をrevertできます。

この機能をどのように実現しているかというと、VitessのVReplicationと呼ばれる機能を使用しています。

先ほどのダウンタイムなしのマイグレーションの項目で説明しましたが、PlanetScaleはghost tableを作成してスキーマを変更し、データをコピーして元のテーブルと入れ替えることでマイグレーションします。
このときにコピーしている間に元のテーブルに変更が行われた場合、Vitessが検知してghost tableにも反映します。
Rewindではこの逆を行なっています。
デプロイ後30分以内であれば、マイグレーション後に使用されなくなったスキーマとデータ情報をshadow tableとして保持しており、production table(ghost tableだったもの)に変更が行われた場合はshadow tableにも変更を加えます。
そしてrevertした際はshadow tableとproduction tableを入れ替えることによって、マイグレーション前のスキーマかつデータが失われていない状態で復元できます。


詳しくはPlanetScaleのブログ記事: Behind the scenes: How we built Rewindをご参照ください。

なぜ外部キー制約をサポートしていないのか

PlanetScaleは外部キー制約をサポートしていません。
Operating without foreign key constraints

理由の1つはVitess(Online DDL)の設計上、外部キー制約をつけることが難しいためです。
Online DDL: why FOREIGN KEYs are not supported

次のスキーマを考えます。(Vitessの記事をそのまま引用)
personテーブルはcountryテーブルの外部キー制約を持っています。

CREATE TABLE country (
  id INT NOT NULL,
  name VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE person (
  id INT NOT NULL,
  country_id INT NOT NULL,
  name VARCHAR(255) NOT NULL,
  PRIMARY KEY(id),
  KEY country_idx (country_id),
  CONSTRAINT person_country_fk FOREIGN KEY (country_id) REFERENCES country(id) ON DELETE NO ACTION
);

このpersonテーブルのマイグレーションを行い際に、ghost tableに外部キー含めてコピーすることはSET FOREIGN_KEY_CHECKS=0をつけることで可能です。参照

一方でその後countryレコードを削除することを考えてみると、person_oldテーブルの外部キー制約があるために失敗します。参照

これを解消するために以下の処理が考えられますが、いずれもVitessが解決しようとしていたことを達成できません。

ゴーストテーブルと入れ替える前に元のテーブル(person_old)から外部キー制約を削除する

これを行うためにはALTER TABLE person_old DROP FOREIGN KEY person_country_fkを実行する必要があります。
ALTER TABLEの実行でロックがかかってしまうため、ダウンタイムなしでマイグレーションするVitessの設計と矛盾します。

ゴーストテーブルと入れ替えが完了した後に、元のテーブルを削除する

MySQLではDROP TABLEするとバッファプールと適応ハッシュインデックスにロックがかかるため、本番環境では障害の原因となる可能性があります。
MySQL 8.0.23のリリースでこのバグは解消されましたが、それ以前のバージョンを使用している場合には問題となります。
また、Vitessを使用する必要のあるALTER TABLEを行う余裕がない環境ではDROP TABLEも同様に実行するのが難しいだろうとも記載されています。

In my personal experience, if you can’t afford to run a straight ALTER on a table, it’s likely you can’t afford to DROP it.

その他Vitessで外部キー制約を付与するための方法について考慮されていますが、耐障害性なども考慮すると難しいことが述べられています。
興味のある方は先ほどのVitessの記事をご参照ください。

外部キー制約をつけることによるデメリット

一方でPlanetScaleを使用していない通常のアプリケーションでも、外部キー制約をつけることによるデメリットはあります。

CREATE TABLE parent_table (
id INT NOT NULL,
PRIMARY KEY (id)
);

CREATE TABLE child_table (
id INT NOT NULL,
parent_id INT,
PRIMARY KEY (id),
KEY parent_id_idx (parent_id),
CONSTRAINT `child_parent_fk` FOREIGN KEY (parent_id) REFERENCES parent_table(id)
ON DELETE CASCADE
);

この例のように親テーブルに対してON DELETE CASCADEON DELETE SET NULLの外部キー制約をつけると、親テーブルのレコードが削除された際に子テーブルに対しても削除や更新処理が実行されます。
もし子テーブルのレコードが大量に存在した場合、親テーブルのレコードを1つだけ削除したつもりが、意図せず大量の子テーブルのレコードに対して更新処理が実行されます。そのため長時間のロックが発生してしまう可能性があります。
こうした副作用はアプリケーションのメンテナンス性を下げる要因となる可能性があります。

ON DELETE NO ACTIONとした場合は子テーブルを削除せずに親テーブルを削除するとエラーとなり実行できません。
なのでアプリケーション側で先に子テーブルを削除する処理が必要ですが、アプリケーションの処理の実行順序がデータベースの制約に依存する形になります。
この例のように親と子の関係であれば、大きな問題にはなりません。
しかし、子テーブルに紐づく孫テーブル、さらにその孫テーブルの外部キー制約が追加されるとアプリケーションの処理もどんどん複雑になっていきます。

外部キー制約をつけない場合

では、外部キー制約をつけないことによるデメリットはどうでしょうか。
当然親テーブルに紐づかない子テーブルのゴミレコードが残ったままになることです。
ただ、これらのレコードは親テーブルが前提で存在するため、これらのレコードへのアクセスがなくなるだけでアプリケーションは問題なく動作するでしょう。

子テーブルのゴミレコードは必ずしも即時で削除する必要はなく、トラフィックが少ない時間帯にバッチで削除しても特に問題ないはずです。(もちろんtransactionが長くなりすぎないように一度に大量の削除は行わないようにする)

こういった点からも大量のデータが存在するケースでは外部キー制約でデータベースに副作用を付与するよりもアプリケーション側で制御するのが適しています。

最後に

PlanetScaleの優れている点や機能について簡単にまとめました。
アプリケーション開発と同じようにデータベースを変更できる素晴らしいサービスだと思います。
公式docにはその他、技術的に面白い内容が記載されています。(スキーママイグレーションツールの比較など)
興味持った方はぜひ見てみてください。