TT Lab
はじめる
学ぶ 学習パス コース

スキーマ変更 — デプロイでサービスを止めない

マイグレーションがサービスを止めるのを見る

TT Labで続きを見る

目標

マイグレーションの事故は、作業に時間がかかるから起きるのではありません。ロックを待っている間に後ろが列を作るから起きます。

このラボでは、その列づくりを自分で再現してみます。数秒で再現できます。

準備

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

ステップ

  1. カラム追加の所要時間 → 01-addcolumn.txt
  2. ロックの種類 → 02-lockmode.txt
  3. 列に並ばせる → 03-queue.txt
  4. lock_timeout → 04-locktimeout.txt
  5. インデックスが書き込みを止める → 05-index.txt
  6. CONCURRENTLY → 06-concurrently.txt
  7. 使えないインデックスを探す → 07-invalid.md
  8. まとめ → 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に残してください。

これがこのラボの核心です。

  1. begin; select count(*) from big; select pg_sleep(8);をバックグラウンドで実行します
  2. 1秒後にalter table big add column q1 int;をバックグラウンドで実行します
  3. さらに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つ目は、それぞれ韓国語で「待ち」「デプロイ」を意味する語です)。