Webエンジニアの森脇です。
PostgreSQLで、サービス稼働中に安易にALTER TABLE等を実行すると、ダウンタイムに繋がることがあります。安全にテーブル定義を変更するために、弊社で気をつけている点を紹介します。
なお、本記事の内容は PostgreSQL 9.5.4 環境で確認しています。
PostgreSQLのロックについて
参照のみのテーブルに対して、ALTER TABLEを実行した場合でもダウンタイムに繋がることがあります。原因について理解するために、PostgreSQLのロックについて簡単に紹介します。
PostgreSQLでは、SELECTでも「ACCESS SHARE」というロックを獲得します。最も弱いロックですが、ALTER TABLE等で獲得される「ACCESS EXCLUSIVE」というロックと競合します。
これは、他のトランザクションでSELECTしているテーブルに対しては、ALTER TABLEが実行できないということを意味します。
セッションA | セッションB |
---|---|
begin; | begin; |
select * from A; | |
alter table A add foo varchar; → 待ち状態になる | |
commit; | |
→ alter tableが実行される |
また、ALTER TABLE中のテーブルに対して、SELECTを行うこともできません。
セッションA | セッションB |
---|---|
begin; | begin; |
alter table A add foo varchar; | |
select * from A; → 待ち状態になる | |
commit; | |
→ selectが実行される |
ALTER TABLEを実行すると該当のテーブルに対しては、更新だけではなく参照も不可となり、サービス提供不能状態に陥る可能性があります。
安全にテーブル定義を変更するための注意点
「ACCESS SHARE」「ACCESS EXCLUSIVE」等のロックの時間を極力短くすることが、サービスの稼働に影響与えないために重要です。
トランザクションの範囲を限定する
前述したように、SELECTでもロックを取得します。バッチ処理であまり考えずにトランザクションを張ってしまうと、意図せず長時間「ACCESS SHARE」ロックを獲得してしまう可能性があります。必要な箇所のみに、トランザクションを限定できないか検討しましょう。
どうしても長時間ロックを獲得してしまう処理がある場合は、その処理が動いていない時にALTER TALBEを実行するようにします。
同一トランザクション内で、複数テーブルに対してALTER TABLEを実行しない
一つのトランザクション内で、複数のテーブルに対してALTER TABLEを実行すると、以下のようなケースでデッドロックが発生する可能性があります。
セッションA | セッションB |
---|---|
begin; | begin; |
select * from A; | |
alter table B add foo varchar; | |
select * from B; | |
alter table A add bar varchar; |
ERROR: deadlock detected DETAIL: Process 1734 waits for AccessExclusiveLock on relation 5116933 of database 5116060; blocked by process 23. Process 23 waits for AccessShareLock on relation 5116941 of
※ Railsのmigrationを利用している場合に、一つmigrationファイルに複数のテーブルに対する変更を書いてしまうと、デッドロックが発生してしまう可能性があります。
実行コマンド別の注意点
いくつかのコマンドは、レコード数が大きいテーブルに対して実行すると長時間ロックとなってしまいます。主要コマンドについて、回避策を紹介します。
default制約付きのカラム追加
カラム追加は一瞬で完了しますが、default制約を設定すると、カラム追加時に全レコードの更新が発生します。ALTER TABLEの実行時間が長くなるため、default制約を設定したい場合は、コマンドを2回に分けて実行します。
- 一度 default制約なしでカラムを追加
- その後にdefault制約を付与する
CHECK制約付きのカラム追加
CHECK制約を設定すると、同じく実行時間が長くなります。NOT VALIDオプションを指定すると、検証をスキップできるため安全にカラムを追加できます。
NOT NULL制約の追加
既存カラムに対するNOT NULL制約の追加時にもテーブルスキャンが発生し、長時間となります。回避策としては、以下のように少し複雑な手順が必要です。
- 既存テーブルをコピーし、新規テーブルを作成
- 新規テーブルのカラムに対してNOT NULL制約を追加
- テーブルをRENAMEし、新旧のテーブルを入れ替える
※ 常に書き込みが発生しているテーブルの場合は、データの整合性をとる仕組みが必要です。
Index追加
Indexの追加は書き込みをブロックしてしまいますが、CONCURRENTLYオプションを付与すると書き込みをブロックせずに追加可能です。
※ ただし、Index追加にかかる実行時間は長くなります。
VACUUM FULL / REINDEX
pg_repackを利用すると、安全に実行できます。
その他
以下の記事では他パターンも詳しく書かれていますので、是非御覧ください。
- https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/
- http://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/
まとめ
弊社ではPostgreSQLで複数のサービスを運営していますが、今回紹介した方法を利用することで基本的には無停止でサービス稼働ができています。皆様の参考になれば幸いです