Postgres: ALTER TABLE は lock_timeout を付けて短く失敗させ、リトライする
ALTER TABLE が長いクエリの終わりを待っている間、後から来た普通の SELECT や UPDATE まで全部その後ろに並んで止まる。SET lock_timeout で数秒で諦めさせ、ループでやり直す。
何が起きるか
ALTER TABLE orders ADD COLUMN ... のような DDL は、テーブルに最も強い ACCESS EXCLUSIVE ロックを取る。列の追加そのものは一瞬で終わる場合でも、ロックを取るまでが問題になる。
- 分析用の長い SELECT や、閉じ忘れのトランザクションが
ordersを読んでいる。 ALTER TABLEが来て、そのクエリが終わるのを待つ(ロック待ち)。- その後に来た普通のクエリも、待っている
ALTERと競合するので、ALTERの後ろに並ぶ。 - 結果として、
ordersを触るすべてのリクエストが止まり、接続プールが埋まって本番が固まる。
「マイグレーションは一瞬で終わるはず」と思っていても、ロック待ちの行列で数秒〜数分の障害になる。
やり方
sql
SET lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN note text;- ロックが 5 秒で取れなければ
ALTERはエラーで終わり、並んでいたクエリはすぐ流れ出す。 - 失敗したら少し待ってやり直す。マイグレーションツール側やシェルで、数回のリトライを組む。
bash
for i in 1 2 3 4 5; do
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \
-c "SET lock_timeout = '5s'; ALTER TABLE orders ADD COLUMN note text;" && break
echo "lock timeout, retry $i"; sleep 10
done注意点
- 長いトランザクションが原因のことが多い。
pg_stat_activityでxact_startが古い接続やidle in transactionを先に確認する。 - ロール単位で既定値を入れる方法は Postgres: statement_timeout はアプリ用ロールに ALTER ROLE で設定する を参照。
- 制約の追加は、
lock_timeoutに加えて Postgres: 大きなテーブルへの CHECK / 外部キーの追加は NOT VALID と VALIDATE の 2 段階に分ける にするとロックを短くできる。 - インデックスは
CREATE INDEX CONCURRENTLYを使う(PostgreSQL: 失敗した CREATE INDEX CONCURRENTLY は INVALID なインデックスを残す に注意)。 - 既定値付きの列追加は Postgres 11 以降なら書き換えなしで速いが、ロック待ちの問題はバージョンに関係なく起きる。