Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Managing db schema changes without downtime 原文公開日: 2018/03/22 著者: Sam Saffron — Discourseの共同創業者であり、Stack Overflowでの開発経験もあります。 後半で紹介されているgemについては先週のRailsウォッチもどうぞ。 Rails: データベーススキーマをダウンタイムなしで変更する(翻訳) Discourseのメンバーはいつも継続的開発の大ファンであり、コミットのたびにCIのテストスイートと対決しています。すべてのテスト(UI、単体、結合、スモーク)にパスすれば、自動的にコードの最新バージョンがhttps://meta.discourse.orgにデプロイされるようになっています。 私たちが継続的開発というパターンに沿って実践していることで、ソフトウェアを自分でインストールした数千人のユーザーがいつどんなときでもtests-passedバージョンに安全にアップグレードできるようになっています。 私たちは頻繁にデプロイを行っているので、デプロイ中に止まったりしないようひときわ注意を払う必要があります。アプリのデプロイ中に止まってしまうよくある原因のひとつが、データベーススキーマの変更です。 スキーマ変更に伴う問題 現在のデプロイのしくみを雑に説明すると次のような感じになります。 データベースを新しいスキーマに移行する アプリを単一のDockerイメージにバンドルする レジストリにプッシュする 古いインスタンスを止め、新しいインスタンスをpullして立ち上げる(これを繰り返す) 万が一互換性のないデータベーススキーマを作成してしまうと、古いバージョンのコードを実行している古いアプリのインスタンスが全部止まってしまうリスクが生じます。実際には数十分の停止💥という形で表面化します。 これはActiveRecordにとって特に悲惨な状況です。production環境のデータベーススキーマはキャッシュされており、スキーマでカラムのリネームや削除を行う何らかの変更が生じると、たちまち影響範囲にあるモデルであらゆるクエリが効かなくなってしまい、無効なスキーマの例外がraiseされるリスクが生じます。 この問題を克服し、停止時間を最小限にとどめてスキーマ変更を安全にデプロイできるようにするために、私たちは数年がかりでさまざまなパターンを導入してきました。 マイグレーションの情報を詳しくトラッキングする ActiveRecordにはschema_migrationsという名前のテーブルが1つあります。ここには、実行されたマイグレーションに関する情報が保存されます。 残念なことに、このテーブルに保存されている情報の量は極端に制限されています。実際、次の情報ぐらいしかありません。 connection.create_table(table_name, id: false) do |t| t.string :version, version_options end このテーブルには、実行したマイグレーションの「バージョン」を保存する孤立したカラムが1つあります。 マイグレーションが実行された時刻はこのカラムに保存されない マイグレーションの実行にかかった時間はこのカラムに保存されない マイグレーションの実行時に動かしていたRailsのバージョンに関する情報が何もない この情報の少なさ(特に「いつ実行されたのか」という情報が取れないこと)のせいで、スキーマ変更をきれいに扱えるシステムの構築が困難になっています。さらに、情報が貧弱なためにマイグレーション中に発生する奇妙でわけの分からない問題のデバッグがものすごくつらくなります。 Discourseでは、Railsにモンキーパッチを適用することでマイグレーションの情報を詳しくログ出力しています(github.com: 記事では一部省略)。 module FreedomPatches module SchemaMigrationDetails def exec_migration(conn, direction) rval = nil time = Benchmark.measure do rval = super end sql = <<SQL INSERT INTO schema_migration_details( version, hostname, name, git_version, duration, direction, rails_version, created_at ) values ( パッチのおかげで、マイグレーションがらみの情報を非常に詳しく取れるようになりました。これはマジでRailsに取り入れるべきです。 カラム削除の延期実行 マイグレーションログを詳しく取れるようになり、これまでのマイグレーションが「いつ行われた」かがすっかりわかるようになったので、カラムの削除を延期実行(defer)できるようになりました。 つまり、危険なスキーマ変更を実行する前に、新しいコードがスキーマ変更を行おうとしていることを私たちが認識することを保証できるようになったのです。 実際には、私たちはカラムの削除にはマイグレーションを使いません。その代わり、カラム削除の延期実行についてはdb/seedで面倒を見ます(github.com: 記事では一部省略)。 Migration::ColumnDropper.drop( table: ‘users’, after_migration: ‘DropEmailFromUsers’, columns: %w[ email email_always mailing_list_mode email_digests email_direct email_private_messages external_links_in_new_tab enable_quoting dynamic_favicon disable_jump_reply edit_history_public automatically_unpin_topics digest_after_days auto_track_topics_after_msecs new_topic_duration_minutes last_redirected_to_top_at これらの延期削除は、(次回のマイグレーションサイクルの)実行中に参照された特定のマイグレーションの少なくとも30分後に実行されます。新しいアプリのコードが確実に存在することで安心感を得られます。 カラム名を変更する場合は、新しくカラムを1つ作って値をそこに複製し、古いカラムをリードオンリーにしてからトリガーで延期削除をかけます。 テーブル名を変更したりテーブルを削除する場合も同じような要領で進めます。 延期削除のロジックはColumnDropperとTableDropperで見られます。 自分たち自身を信用しない アプリごとに行うさまざまな作業において、「徹底」はひとつの大きな問題です。 安全を担保するためのパターンはいろいろありますが、それでも、カラムやテーブルの削除を絶対にActiveRecordマイグレーション方式で行うべきではないことを忘れてしまったりします。 マイグレーション中に危険なスキーマ変更を絶対にやらかさないようにするために、PG gemにパッチを当てて、マイグレーションのコンテキストで特定のステートメントの利用を禁止しました。 DROP TABLEしようとしたりカラムをDROPしようとしても、例外がraiseされます。 これで、社内のベストプラクティスを無視してリスクのあるスキーマ変更をかけることが困難になりました。 == 20180321015226 DropRandomColumnFromUser: migrating ========================= — remove_column(:categories, :name) WARNING ————————————————————————————- An attempt was made to drop or rename a column in a migration SQL used was: ‘ALTER TABLE “categories” DROP “name”‘ Please use the deferred pattrn using Migration::ColumnDropper in db/seeds to drop or rename columns. Note, to minimize disruption use self.ignored_columns = [“column … Continue reading Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)