概要
これはアクセスの多いテーブルのカラムを変更したときに、
カラムを新しく変更したはずなのに、なぜかコードは(すでに存在しない)古いカラムを参照してしまい、エラーが発生してしまう問題を解決するための方法を記したものです。
2024/02/29 追記、結論「ignored columnを使う」
ごめんなさい。こっちを参照してください。
前提条件
Rails 5.1
heroku
解決策1「heroku restart」
先日、自分のカラム の英語の命名ミスに気づき、
AnswerHistoryテーブルのweekly_continuationというカラムを、continuation_all_weekに変更しました。
その後、カラム変更に伴うコードも変更してデプロイし、
heroku run rails db:migrate
したのですが、その後しばらくSentryより、以下の報告を繰り返し繰り返し受け続けました。
PG::UndefinedColumn: ERROR: column answer_histories.weekly_continuation does not exist
つまり、DBのカラム名はきちんと変更されているにも関わらず、コードは古いカラム名を参照し続けているということです。
僕は最初この警告を、一時的な不具合だとばかり思っており、無視していました。
「ユーザーがページを遷移したり更新すれば直るやろ」と。
しかし、自分の端末でも同じ問題が起こり、かつページを遷移しても更新しても解決しなかったとき、これがサービス運営者が解決すべき障害だと気づきました。
(それもこの障害は、自分のサービスの中でももっともアクセスが多いページで起きていました....無念....)
結局このときは、heroku restartを行うことで障害を解決することができました。
「なぜheroku restartで障害を解決できたのか?」
解決しておきながら、自分はその理由がわかりませんでした。
よりベテランの開発者の方にこのことを聞くと、
どうやら、heroku run rails db:migrate
する直前にデプロイされたとき、Railsのアプリケーションサーバーがschema informationのキャッシュを持ってしまっていたがために、アプリケーションサーバーを再起動してキャッシュを消す必要がありました。
heroku restartすることでdynoが再起動したので、アプリケーションサーバーのschema informationも新しくなり障害が解決したということでした。
なので、もし教科書的に、
git push heroku
heroku run rails db:migrate
を行いカラムを変更する場合には、
追加でheroku restart
もしておくことで、
heroku のdynoと一緒に、アプリケーションサーバーも再起動されるおかげで、schema informationのキャッシュもクリアされるので、コードがきちんと新しいカラムを参照してくれるようになります。
解決策2「Procfileからリリース時にdb:migrateを行う」
今回の障害で初めて知ったのですが、
herokuではProcfileなるものを設定することで、
heroku run rails db:migrate
のようなコマンドをリリース時に自動で実行してくれるようになります。
参照: https://devcenter.heroku.com/articles/release-phase
このProcfileでdb:migrateを行うメリットとしては、「デプロイした後」ではなく**「デプロイ直前」にdb:migrateしてくれる**という点です。
herokuではデプロイ時に、自動で真新しい別のDynoを立ててくれます。
そして真新しいDynoはheroku restartされた状態と同じなため、
新しいDynoにあるアプリケーションサーバーはschema informationのキャッシュをもつこともなく、結果として、heroku restartせずとも、カラムの変更によるアプリケーションのエラーに見舞われることがないそうです。
ただしエラーが起きないのは、新しいDynoにアクセスしてきたユーザーのみで、古いDynoにアクセスしていたユーザーは、エラーに見舞われます。
Procfileからデプロイ直前に自動でdb:migrateまで行うには、次のように設定します。
release: bin/rails db:migrate
解決策3『段階的にデプロイする』
より安全にカラムを変更するときに、実務においては段階的にデプロイする手法が採用されているそうです。
db:migrateを伴うリリースだけ先にデプロイし、本番環境でdb:migrateを行ってから、
別のタイミングで、変更したカラムを利用するソースコードをリリースするという形です。
たとえば今回のように、AnswerHistoryのweekly_continuationをcontinuation_all_weekにリネームする場合には、次のように3回のデプロイします。
1, continuation_all_weekカラムを新しく追加するmigrationファイルを作成。
2, デプロイ&db:migrate。
3, weekly_continuationカラムではなく、continuation_all_weekカラムを参照するように、コードを変更する
4, デプロイ&weekly_continuationのデータをcontinuation_all_weekにコピー。
5, weekly_continuationがコードから呼ばれていない状態にする。
6, weekly_continuationカラムを削除するmigrationファイルを作成。
7, デプロイ&db:migrate。
端的にまとめると、
テーブルへのアクセスが多い場合は、古いカラムをリネームするのではなく、新しいカラムを追加して、コードが古いカラムを参照しない状態をつくった後に、古いカラムを削除することで「リネームしたように見せること」が良い、ということらしいです。
面倒かもしれませんが、いちばん安全な方法ですね。
呼ばれる頻度の低いテーブルのカラムなら、これほど気を使わなくても良いかもしれませんが、
今回僕が障害を発生させたAnswerHistoryテーブルは、1日に1万件以上も生成されるほど利用頻度の高いものだったので、以降、カラムのリネームなどを行う場合は、こちらの段階的なデプロイを採用したいと思います。
以上となります!
僕は恐ろしく無知なので、他にもアドバイスなどございましたら、ぜひ教えてください。
宣伝
障害を起こしてしまった弊英単語学習サービスです。