データベーススキーマをダウンタイムなしで変更する(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。Overflowでの開発経験もあります。後半で紹介されているgemについては先週のRailsウォッチもどうぞ。データベーススキーマをダウンタイムなしで変更する(翻訳)Discourseのメンバーはいつも継続的開発の大ファンであり、コミットのたびにCIのテストスイートと対決しています。すべてのテスト(UI、単体、結合、スモーク)にパスすれば、自動的にコードの最新バージョンがhttps://meta.discourse.orgにデプロイされるようになっています。私たちが継続的開発というパターンに沿って実践していることで、ソフトウェアを自分でインストールした数千人のユーザーがいつどんなときでもtests-passedバージョンに安全にアップグレードできるようになっています。私たちは頻繁にデプロイを行っているので、デプロイ中に止まったりしないようひときわ注意を払う必要があります。アプリのデプロイ中に止まってしまうよくある原因のひとつが、データベーススキーマの変更です。現在のデプロイのしくみを雑に説明すると次のような感じになります。データベースを新しいスキーマに移行するアプリを単一のDockerイメージにバンドルするレジストリにプッシュする古いインスタンスを止め、新しいインスタンスをpullして立ち上げる(これを繰り返す)万が一互換性のないデータベーススキーマを作成してしまうと、古いバージョンのコードを実行している古いアプリのインスタンスが全部止まってしまうリスクが生じます。実際には数十分の停止?という形で表面化します。これはActiveRecordにとって特に悲惨な状況です。production環境のデータベーススキーマはキャッシュされており、スキーマでカラムのリネームや削除を行う何らかの変更が生じると、たちまち影響範囲にあるモデルであらゆるクエリが効かなくなってしまい、無効なスキーマの例外がraiseされるリスクが生じます。この問題を克服し、停止時間を最小限にとどめてスキーマ変更を安全にデプロイできるようにするために、私たちは数年がかりでさまざまなパターンを導入してきました。マイグレーションの情報を詳しくトラッキングするActiveRecordにはschema_migrationsという名前のテーブルが1つあります。ここには、実行されたマイグレーションに関する情報が保存されます。残念なことに、このテーブルに保存されている情報の量は極端に制限されています。実際、次の情報ぐらいしかありません。version_optionsendこのテーブルには、実行したマイグレーションの「バージョン」を保存する孤立したカラムが1つあります。マイグレーションが実行された時刻はこのカラムに保存されないマイグレーションの実行にかかった時間はこのカラムに保存されないマイグレーションの実行時に動かしていたRailsのバージョンに関する情報が何もないこの情報の少なさ(特に「いつ実行されたのか」という情報が取れないこと)のせいで、スキーマ変更をきれいに扱えるシステムの構築が困難になっています。情報が貧弱なためにマイグレーション中に発生する奇妙でわけの分からない問題のデバッグがものすごくつらくなります。(パッチのおかげで、マイグレーションがらみの情報を非常に詳しく取れるようになりました。これはマジでRailsに取り入れるべきです。カラム削除の延期実行マイグレーションログを詳しく取れるようになり、これまでのマイグレーションが「いつ行われた」かがすっかりわかるようになったので、カラムの削除を延期実行(defer)できるようになりました。危険なスキーマ変更を実行する前に、新しいコードがスキーマ変更を行おうとしていることを私たちが認識することを保証できるようになったのです。実際には、私たちはカラムの削除にはマイグレーションを使いません。last_redirected_to_top_atこれらの延期削除は、(次回のマイグレーションサイクルの)実行中に参照された特定のマイグレーションの少なくとも30分後に実行されます。新しいアプリのコードが確実に存在することで安心感を得られます。カラム名を変更する場合は、新しくカラムを1つ作って値をそこに複製し、古いカラムをリードオンリーにしてからトリガーで延期削除をかけます。テーブル名を変更したりテーブルを削除する場合も同じような要領で進めます。延期削除のロジックはColumnDropperとTableDropperで見られます。自分たち自身を信用しないアプリごとに行うさまざまな作業において、「徹底」はひとつの大きな問題です。安全を担保するためのパターンはいろいろありますが、それでも、カラムやテーブルの削除を絶対にActiveRecordマイグレーション方式で行うべきではないことを忘れてしまったりします。gemにパッチを当てて、マイグレーションのコンテキストで特定のステートメントの利用を禁止しました。TABLEしようとしたりカラムをDROPしようとしても、例外がraiseされます。これで、社内のベストプラクティスを無視してリスクのあるスキーマ変更をかけることが困難になりました。--trace)このロジックはsafe_migrate.rbで見られます。これは最近のパターンなので、強制を行ったのはマイグレーション後まだ数日間というところです。その他の方法gemでできることもあれば、gemではできないこともあります。gemは、強制機能を提供します。このgemでは、PostgreSQLでインデックスをコンカレントに作成するかどうかなどの興味深い条件を多数設定することもできます。強制は、ActiveRecordのマイグレータにパッチを当てる形で行います。誰かが生SQLでやらかしてしまえば強制をすり抜けてしまいます。Migrationsと非常によく似たgemです。gemは、マイグレーションにタグを付与できるようにします。これを用いてデプロイ前マイグレーションやデプロイ後マイグレーションを行うことで、デプロイを改善できます。デプロイ中の停止を避けられるようにマイグレーションを管理する手法としては、これが最もシンプルです。HandcuffsはOutriggerととてもよく似たgemで、マイグレーションに複数のフェーズを定義できます。手錠さて皆さんはどうすべきでしょうか?弊社で使っているカラムやテーブルの延期削除パターンは、社内ではうまくいっていますが、まだ理想的とは言えません。データの「seed」を担当するコードはスキーマの補正やカラム削除のタイミングも行っていて、本来あるべき姿まで十分制御されてはいません。db:migrateを実行するだけで何もかも魔法のようにやってくれることです。ホスティング先がどこであろうと、スキーマのバージョンが何であろうと関係なくやって欲しいのです。いうものの、さしあたって本記事で私からおすすめしたいベストプラクティスは、さまざまなアイデアを合わせ技にすることです。どの方法も、Railsにふさわしい方法だからです。ベストプラクティスの強制はRailsがやるべき私としては、ActiveRecordに安全なスキーマ変更を強制するしくみを導入すべきだと思います。Railsを使っていれば誰しも多かれ少なかれ同じことに気づくでしょう。スキーマ変更のデプロイにおけるダウンタイムをゼロにできれば実用的です。endendさらに確実にするには、マイグレーションファイルにafter_deployフラグを追加することを例外なく強制すべきです。endendもうひとつ、SQL解析に基づく強制が最も理想的だと思います。この方法はRailsの規模で問題をややこしくする可能性があります。サポートするデータベースが1つに限られている現実的な理由はこれです。db:migrateはafter_deployマイグレーションを含むあらゆるマイグレーションを実行できるようにすべきですし、安全のために、デプロイの「ゼロダウンタイム」をないがしろにするアプリを除外できるようにすべきです。rakeタスクに新しくmigrate:preやmigrate:postを導入すべきアプリコードとの互換性を壊さないマイグレーションを実行する場合は、常に次のようにします。`after_deploy!`破壊的な操作を行う場合は、常に次のようにします。`after_deploy!`まとめ今日、ダウンタイムゼロのデプロイを「安全に」始める方法を模索している方には次をおすすめします。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。