こんにちは、Yakumo兼コネクト支援チームの@ueokandeです。 サイボウズには体験入部という制度があり、数週間〜数ヶ月の期間、他チームの業務を体験できます。 自分もこの制度を使い、1ヶ月ほどGaroon開発チームを体験してきました。
自分はこの期間で、Garoonの大規模なデータベースを安全にマイグレーションするための仕組みの設計と、そのプロトタイプを実装しました。
背景
Garoonはサービスのアップデートと同時に、データベースのマイグレーションを実行します。 ここでいうマイグレーションは、主に2つの処理があります。
- テーブルスキーマの更新。ALTER TABLEによるカラムの追加、削除など。
- データの変換。既存レコードのデータ編集など。
Garoonはメンテナンスウィンドウを設けてバージョンアップを実施します。 このバージョンアップが毎回確実に成功すればいいのですが、実際はそれを保証するのが難しいです。
Garoonのデータベース (MySQL) は、cybozu.com全体でテラバイト級のデータサイズになります。 アップデート時にむやみにマイグレーションを実行すると、以下の理由でマイグレーションが失敗するケースがあります。
- 巨大なテーブルに対するALTER TABLEがメンテナンスウィンドウ内で終わらない
- 巨大なテーブルに対するデータコンバートが、メンテナンスウィンドウ内に終わらない
- 古いデータに、コンバートに失敗するデータがある
現在はメンテナンスウィンドウ内に確実にマイグレーションが終了するように、本番環境のバックアップデータを使ったリハーサルを実施しています。 しかし、毎回のバージョンアップでテラバイト級のバックアップデータをリストアしてリハーサル環境を構築するのは大変で、リハーサル環境そのものがメンテナンス困難になってきています。 そこでGaroon開発チームでは、リハーサル環境を使わずに、安全にデータベースマイグレーションをする仕組みに取り組んでいます。
この体験入部期間では、この仕組みの設計とそのプロトタイプ実装をしました。
安全にマイグレーションをする仕組み
データベースのマイグレーションを確実に終わらせるために、メンテナンスウィンドウではなく、事前に裏でデータコンバートを実行します。 あらかじめコンバート先のスキーマを持つ空のテーブルを作成しておき、お客様に影響がないよう徐々にデータをコピーとコンバートを実施します。 そしてバージョンアップ時に、コンバート後のテーブルをリネームして新しいテーブルに切り替えることで、マイグレーション終了です。
もしデータコンバートが失敗するデータを含まれていたとしても、アップデートの前に知ることができます。 また格納先のテーブルはマイグレーション後のスキーマを持っているので、ALTER TABLEで時間がかかるということもありません。 以降、コンバート後の格納先のテーブルを shadowedテーブル と呼ぶことにします。
shadowedテーブルの作成から、アップデートが完了までの流れは以下のとおりです。
- shadowedテーブルの作成
- TRIGGERの作成
- データコンバートの開始
- shadowedテーブルから元テーブルに切り替え
ステップ1. 2. 3. はダウンタイムなしにバックグラウンドで実施できます。 そしでメンテナンスウィンドウでステップ4.を実行して、アップデート後のGaroonはデータコンバート済みのテーブルを参照します。
どこまでコンバートが進んだかを判断するために、コンバート対象のテーブルに 単調増加するカラム が必要です。
これはAUTO_INCREMENT属性のカラムを使います(以降は id
カラムとします)。
shadowedテーブルのレコードは、変換元のデータと同じ id
を持ちます。
それぞれのステップについて詳しく説明していきます。
1. shadowedテーブルの作成
shadowedテーブルは、変換後のテーブルスキーマを持ちます。
このとき元テーブルに外部キー制約があっても、この時点ではshadowedテーブルに外部キー制約を付けません
(CREATE TABLE ... LIKE
構文を使うと外部キー制約なしで、同じスキーマのテーブルを作成できます)。
そしてshadowedテーブルに、元データの変更があったかを示す converted
カラムを追加します。
converted
はデータ変換後は1がセットされ、元データに変更があれば0になります。
ここでは todo
テーブルのタイムスタンプ created_at
を、int(11)
から timestamp
型に変換する例を考えます。
この場合CREATE TABLE ... LIKE
構文で新たなテーブルを作成できます。
そこにALTER TABLE
構文で、カラムの型を変更して、converted
カラムを追加します。
2. TRIGGERの作成
続いて元テーブルの更新、削除時を知らせるTRIGGERの作成です。
元データの BEFORE DELETE
と BEFORE UPDATE
時に、shadowedテーブルに converted=0
をセットします。
このTRIGGERによりshadowedテーブルにあるレコードが、古いデータということに気づくことができます。
たとえばtodo
テーブルに対しては、以下の構文でそれぞれのTRIGGERを作成できます。
CREATE TRIGGER update_todo_replica BEFORE UPDATE ON todo FOR EACH ROW BEGIN UPDATE todo_shadowed SET `converted`=0 WHERE id = NEW.id; END; CREATE TRIGGER delete_todo_replica BEFORE DELETE ON todo FOR EACH ROW BEGIN UPDATE todo_shadowed SET `converted`=0 WHERE id = OLD.id; END;
3. データコンバートの開始
shadowedテーブルとTRIGGERを作成したら、データコンバートをスタートできます。 データコンバートはバックグラウンドで実行し、お客様の利用に影響が無いように少しずつ処理を進めます。 この処理では以下の2つの条件を満たすまで、コンバートを繰り返します。
- 元テーブルとshadowedテーブルの
max(id)
が一致する - shadowedテーブルから
converted=0
なレコードが存在しない。
もし条件1.を満たさないときは、元テーブルに新しいデータが追加されていることになります。
その場合はshadowedテーブルの max(id)
より大きなIDから、条件1.を満たすまでデータコンバートしてshadowedテーブルにINSERTし続けます。
INSERTするときは元データと同じ id
を使い、converted=1
に設定します。
もし条件2.を満たさないときは、元テーブルのデータが更新または削除されたことになります。
同じ id
を持つデータを再変換してshadowedテーブルをREPLACEするか、削除されている場合はshadowedテーブルからDELETEします。
REPLACEするときは同じ id
を使い、再びconverted=1
をセットします。
この処理を繰り返すことで、最終的に上記の条件を満たすことになり、そのときshadowedテーブルにはデータコンバート済みのデータのみが存在します。
4. テーブルの切り替え
いよいよGaroonアップデートの時間です。 テーブルの切り替えはメンテナンスウィンドウ内で実行されます。
まずテーブルの切り替え前に、上記のコンバートを確実に終了します。 このとき残りのデータ件数は少ないので即座に終了します。 それが終わると、shadowedテーブルを元のテーブル名にリネームして、元テーブルの外部キー制約を新しいテーブルに設定します。 このときforeign_key_checksを無効化して、外部キー制約のチェックをスキップします。
foreign_key_checksが有効になっていると、他のテーブルから外部キー制約で参照されているテーブルは削除できません。 またマイグレーション対象が外部キー制約を持っていたとしても、キーそのものをコンバートしない限りはマイグレーション後も外部キー制約が守れられます。 そのためforeign_key_checksを無効化しても、安全にテーブルを切り替えることができます。
TRIGGERを削除して、新しいテーブルに切り替えると、バージョンアップ完了です。
このときconverted
カラムは残り続けますが、内部のテーブル再構築を避けるためにカラムは削除しません。
制約事項
この仕組みにもいくつかの制限があります。 現段階では設計や実装の容易化のために、以下の条件は制約事項としました。
- 外部キー自体のコンバートはできません。shadowedテーブルには外部キー制約を設定しないため、たとえば外部キー制約が設定されている
user_id
の値が変わる場合は、参照先のデータがあるか保証できません。 - MySQL 8.0未満はサポートしません。MySQLにはサーバー再起動時にAUTO_INCREMENTがリセットされる不具合(#199)があり、その場合は正常にコンバートできません。この不具合はMySQL 8.0で修正されました。
おわりに
この仕組みがうまく動くと、安全にデータコンバートを実行できるだけでなく、ゼロダウンタイムにもつなげる事ができます。 今回はプロトタイプ実装だけで終わりましたが、本番データのコンバート時間や、データベースの負荷計測などの課題も残っています。 そこは未来のGaroon開発チームに託して、自分は一旦Yakumoチームに戻ります。
以上で自分のGaroon体験入部の報告は終わりです。 体験入部ではGaroonという製品に向き合えただけでなく、Garoon開発チームの文化や開発フローについても知る、貴重な体験となりました。 Garoon開発チームの皆さんありがとうございました!