マイグレーションがサービスを止めるのを見る
目標
マイグレーションの事故は、作業に時間がかかるから起きるのではありません。ロックを待っている間に後ろが列を作るから起きます。
このラボでは、その列づくりを自分で再現してみます。数秒で再現できます。
準備
create table big(id bigserial primary key, name text, n int);
insert into big(name,n) select 'row'||i, i from generate_series(1,300000) i;
時間の計測
psql -h 127.0.0.1 -U lab -d labdb
\timing on
2つのセッションを作る
psql -h 127.0.0.1 -U lab -d labdb \
-c "begin; select count(*) from big; select pg_sleep(8);" &
sleep 1
# 여기서 두 번째 세션
wait
ステップ
- カラム追加の所要時間 →
01-addcolumn.txt - ロックの種類 →
02-lockmode.txt - 列に並ばせる →
03-queue.txt lock_timeout→04-locktimeout.txt- インデックスが書き込みを止める →
05-index.txt CONCURRENTLY→06-concurrently.txt- 使えないインデックスを探す →
07-invalid.md - まとめ →
08-notes.md
参考
ステップ3がこのラボのすべてです。残りは、それを避ける方法です。
デフォルト値付きのカラム追加は遅いのか
30万行のテーブルを作り、デフォルト値付きのカラムを追加して、かかった時間を01-addcolumn.txtに残してください。
create table big(id bigserial primary key, name text, n int);
insert into big(name,n) select 'row'||i, i from generate_series(1,300000) i;
時間は、psqlで\timing onを有効にすると表示されます。alter table big add column status text not null default 'new';を実行してください。
ミリ秒単位の時間が表示されるはずです。PostgreSQL 11以降は、デフォルト値をメタデータにだけ書き込みます。「テーブルを書き直す」というアドバイスは古くなっています。
ではどんなロックを取得するのか
ALTER TABLEが取得するロックの種類を確認して、02-lockmode.txtに残してください。
トランザクション内でalterを実行し、コミットする前にpg_locksを見れば確認できます。
begin;
alter table big add column tmp1 int;
select mode from pg_locks where relation='big'::regclass;
rollback;
AccessExclusiveLockが表示されるはずです。最も強いロックなので、読み取りまで止めます。
後ろに来るものが列に並ぶ
長いクエリ → ALTER TABLE → 単純なSELECTの順に実行し、最後のSELECTまで止まることを03-queue.txtに残してください。
これがこのラボの核心です。
begin; select count(*) from big; select pg_sleep(8);をバックグラウンドで実行します- 1秒後に
alter table big add column q1 int;をバックグラウンドで実行します - さらに1秒後に
set lock_timeout='3s'; select count(*) from big;を実行します
3つ目はALTERとは無関係な読み取りなのに止まります。ロック要求が順番どおりに処理されるからです。
サービス全体が止まったように見えるのに、DBのメトリクスは正常です。そのため、原因を見つけるのに時間がかかります。
lock_timeoutで防ぐ
同じ状況でALTERにlock_timeoutを設定し、ALTERだけが失敗して後ろは止まらないことを04-locktimeout.txtに残してください。
set lock_timeout='2s'; alter table ...を実行します。2秒後にALTERが諦めると、キューが解消されます。
失敗したマイグレーションは再実行すればよいのですが、列に並ばせてしまった5分は取り戻せません。
statement_timeoutと混同しないでください。それは実行時間の制限で、ここで必要なのは待つ時間の制限です。
インデックス作成が書き込みを止める
書き込みトランザクションを開いたままcreate indexを試し、止まることを05-index.txtに残してください。
片方でbegin; update big set n=n where id=1; select pg_sleep(6);を実行し、もう片方でset lock_timeout='2s'; create index idx_n on big(n);を実行してみてください。
CREATE INDEXはSHAREロックなので、読み取りはできますが、書き込みは止まります。大きなテーブルだと数分かかります。
CONCURRENTLYで作成する
同じ状況でcreate index concurrentlyが成功することを示し、トランザクション内ではできないことも合わせて06-concurrently.txtに残してください。
書き込みを開いたままでも通ります。そしてbegin; create index concurrently ...;を実行すると、次のようになります。
ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
マイグレーションツールが自動でトランザクションに包むと、ここで壊れます。両方を残してください。
失敗したインデックスは残る
pg_indexから使えないインデックスを探すクエリを作って実行し、結果とともに07-invalid.mdに残してください。なぜこれを確認する必要があるのかも書いてください。
select indexrelid::regclass, indisvalid from pg_index where not indisvalid;
CONCURRENTLYが途中で失敗すると、indisvalid = falseのインデックスが残ります。削除して作り直す必要があります。
これを知らないと「インデックスを作ったのに使われない」と長く迷います。今は結果が空でも構いません。クエリに慣れることが目的です。
3つの点をまとめる
08-notes.mdに3行以上書いてください。マイグレーションの事故が起きる本当の理由、lock_timeoutが必要な理由、カラムを削除するときにデプロイを分ける理由です。
本文には대기、lock_timeout、배포が含まれている必要があります(1つ目と3つ目は、それぞれ韓国語で「待ち」「デプロイ」を意味する語です)。