LabHub
배우기 러닝패스 코스

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

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

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

마이그레이션 사고는 작업이 오래 걸려서 나는 게 아닙니다. 잠금을 기다리는 동안 뒤가 줄을 서서 납니다.

이 실습은 그 줄 세우기를 직접 만들어 봅니다. 몇 초면 재현됩니다.

준비

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

두 세션 만들기

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_timeout04-locktimeout.txt
  5. 인덱스가 쓰기를 막음 → 05-index.txt
  6. CONCURRENTLY06-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 으로 막는다

같은 상황에서 ALTERlock_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 인 인덱스가 남습니다. 지우고 다시 만들어야 합니다.

이걸 모르면 '인덱스를 만들었는데 안 쓰인다' 로 한참 헤맵니다. 지금은 결과가 비어 있어도 됩니다 — 질의를 손에 익히는 것이 목적입니다.

세 가지를 정리한다

08-notes.md 에 세 줄 이상. 마이그레이션 사고가 나는 진짜 이유, lock_timeout 이 필요한 이유, 컬럼을 지울 때 배포를 나누는 이유.

본문에 대기, lock_timeout, 배포 가 들어가야 합니다.