Summary#
Zero-downtime database schema migration의 핵심은 한 번의 배포로 스키마와 애플리케이션 의미를 동시에 바꾸지 않는 것이다. 안전한 접근은 보통 expand → backfill → cutover → contract 순서로 진행된다.
주요 실패 모드는 다음 네 범주에 집중된다.
-
Expand 단계의 backward compatibility 실패
새 컬럼, 새 테이블, 새 인덱스, nullable/default 설정이 기존 애플리케이션 버전과 호환되지 않으면 rolling deploy 중 장애가 발생한다. -
Backfill ordering 실패
대량 UPDATE, 잘못된 batch 크기, 트리거/dual-write보다 앞선 backfill, 인덱스 부재 상태의 backfill은 lock, replication lag, vacuum pressure, timeout, 데이터 불일치를 유발할 수 있다. -
Dual read/write cutover 실패
구 스키마와 신 스키마를 동시에 쓰거나 읽는 기간에 idempotency, ordering, retry, conflict resolution을 설계하지 않으면 양쪽 데이터가 diverge한다. -
Rollback safety 착각
schema migration은 application rollback보다 되돌리기 어렵다. 특히 column drop, type narrowing, constraint validation, rename, destructive data transformation은 rollback 시점에 이미 구버전 코드와 호환되지 않을 수 있다.
안전한 마이그레이션은 “DDL이 성공했는가”보다 여러 애플리케이션 버전이 공존하는 동안 읽기/쓰기 의미가 안정적인가를 기준으로 검증해야 한다.
Key Points#
- Expand-contract 패턴
- Expand 단계에서는 기존 코드가 계속 동작하도록 additive change만 적용한다.
- 예: nullable column 추가, 새 테이블 추가, 새 인덱스 추가, 새 nullable FK 추가.
- 애플리케이션이 신/구 스키마 모두를 처리할 수 있게 배포한다.
- Backfill로 기존 데이터를 채운다.
- Read path를 신 스키마로 전환한다.
- Write path를 신 스키마 중심으로 전환하거나 dual-write를 종료한다.
- 충분한 관찰 기간 후 contract 단계에서 구 컬럼/구 테이블/호환성 코드를 제거한다.
-
실패 모드:
- rename을 additive change로 착각함.
- column drop을 너무 빨리 수행함.
- NOT NULL, UNIQUE, FK 같은 constraint를 기존 데이터 검증 전에 강제함.
- old binary와 new schema의 공존 시간을 고려하지 않음.
-
PostgreSQL 관련 주의점
ALTER TABLE은 변경 종류에 따라 lock 수준과 테이블 rewrite 여부가 달라질 수 있다.CREATE INDEX CONCURRENTLY는 일반 index 생성보다 쓰기 차단을 줄이지만, 더 오래 걸릴 수 있고 실패 시 invalid index가 남을 수 있다.- constraint는 가능하면 즉시 전면 검증하지 않고,
NOT VALID후 별도VALIDATE CONSTRAINT패턴을 고려할 수 있다. - 대량 backfill은 하나의 거대한 transaction보다 작은 batch로 나누는 것이 일반적으로 안전하다.
-
실패 모드:
- default가 있는 column 추가가 특정 PostgreSQL 버전/조건에서 예상보다 큰 작업이 됨.
- concurrent index 실패 후 invalid artifact를 방치함.
- validation 작업이 production peak와 겹침.
- backfill이 autovacuum, WAL, replication lag를 과도하게 증가시킴.
-
Backfill ordering
- 일반적으로 안전한 순서는 다음과 같다.
- 신 스키마를 추가한다.
- 신/구 스키마를 모두 처리 가능한 애플리케이션을 배포한다.
- 신규 write가 신 스키마에도 반영되도록 한다.
- 기존 row를 batch backfill한다.
- consistency check를 수행한다.
- read path를 신 스키마로 전환한다.
- dual-write 또는 compatibility path를 제거한다.
- contract migration을 수행한다.
- backfill 전에 dual-write 또는 change capture가 준비되지 않으면 backfill 중 새로 들어온 write가 누락될 수 있다.
- backfill 후 read cutover 전에는 count comparison, checksum, sampled diff, business invariant check가 필요하다.
-
실패 모드:
- backfill job이 retry-safe하지 않음.
- update 기준이 non-deterministic함.
- batch cursor가 mutable column을 기준으로 함.
- partially backfilled 상태에서 read path를 전환함.
- backfill 완료 판정이 “job 종료”에만 의존하고 데이터 검증이 없음.
-
Dual read/write cutover
- Dual-write는 migration의 안전장치이지만 동시에 새로운 failure surface다.
- 반드시 정의해야 할 것:
- 어느 쪽이 source of truth인가.
- 두 write 중 하나만 성공했을 때 재시도 방식은 무엇인가.
- write ordering은 어떻게 보장하는가.
- replay/retry가 중복 적용되어도 안전한가.
- read fallback은 언제 허용하고 언제 금지할 것인가.
- read cutover는 feature flag 또는 percentage rollout으로 점진 전환하는 것이 안전하다.
-
실패 모드:
- old write는 성공, new write는 실패했는데 전체 요청은 성공 처리됨.
- retry가 한쪽에만 적용되어 데이터가 diverge함.
- 신/구 representation 간 변환이 lossy함.
- read fallback이 오래 남아 실제 불일치를 숨김.
- cutover 후 구 path를 너무 빨리 제거해 rollback 불가 상태가 됨.
-
gh-ost / pt-online-schema-change 계열의 failure modes
- MySQL 온라인 스키마 변경 도구는 보통 shadow table, row copy, trigger 또는 binlog 기반 동기화, cutover rename을 사용한다.
- gh-ost는 triggerless 접근과 replication stream 기반 방식을 제공하는 것으로 알려져 있고, cutover와 replication lag 관리가 핵심 운영 포인트다.
- pt-online-schema-change는 shadow table과 trigger를 활용하는 방식이며, 외래키, trigger, replication lag, cutover lock이 중요한 제약이 될 수 있다.
-
실패 모드:
- replication lag가 증가해 replica read가 stale해짐.
- cutover 시 metadata lock을 오래 기다림.
- foreign key가 shadow table rename/cutover와 충돌함.
- 기존 trigger와 도구가 생성한 trigger가 충돌함.
- long-running transaction이 cutover를 막음.
- 도구 중단 후 shadow table, trigger, partial state cleanup이 누락됨.
-
Rollback safety
- 안전한 rollback은 “마이그레이션 down script가 존재한다”와 다르다.
- 애플리케이션 rollback을 고려하면, DB는 일정 기간 구버전 코드와도 호환되어야 한다.
- destructive migration은 마지막 단계로 미루고, 제거 전 관찰 기간을 둔다.
- rollback-safe migration checklist:
- 구버전 app이 신 스키마에서 동작하는가?
- 신버전 app이 구 데이터와 신 데이터를 모두 읽을 수 있는가?
- partially backfilled 상태에서 기능을 꺼도 안전한가?
- dual-write 실패 시 reconciliation job이 있는가?
- contract migration 이후에는 rollback 대신 forward-fix가 필요한가?
-
실패 모드:
- column rename/drop 후 app rollback이 즉시 실패함.
- data type 축소 또는 irreversible transform 후 원복 불가.
- new constraint가 old writer의 valid write를 거부함.
- migration transaction은 rollback됐지만 외부 side effect나 background job은 이미 진행됨.
-
Operational guardrails
- production migration 전 staging에서 row count와 data distribution이 유사한 조건으로 테스트한다.
- migration을 DDL, app deploy, backfill, read cutover, contract로 분리한다.
- lock timeout, statement timeout, batch size, sleep interval, retry limit을 명시한다.
- migration 중 관찰할 지표:
- DB lock wait
- replication lag
- WAL/binlog growth
- query latency
- dead tuples / vacuum pressure
- error rate
- backfill progress
- consistency diff count
- abort 기준을 사전에 정한다.
- contract 전에는 일정 기간 compatibility code를 유지한다.
Cautions#
- 이 초안은 공개적으로 알려진 PostgreSQL 문서, gh-ost, Percona Toolkit, expand-contract 관련 자료에 기반한 일반 지침이다. 특정 DB 버전, cloud-managed DB 설정, ORM, migration framework에 따라 lock behavior와 DDL semantics가 달라질 수 있다.
- PostgreSQL의
ALTER TABLErewrite/lock 특성은 버전별 최적화 차이가 있으므로 실제 대상 버전 문서를 확인해야 한다. - MySQL 온라인 스키마 변경 도구의 동작은 storage engine, foreign key, trigger, replication topology, privileges, binlog format에 크게 의존한다.
- Dual-write는 무조건 안전한 패턴이 아니다. reconciliation과 idempotency 설계가 없으면 오히려 데이터 불일치를 늘릴 수 있다.
- “zero downtime”은 “zero risk”가 아니다. 특히 cutover, constraint validation, contract 단계는 짧은 시간이라도 장애 가능성이 있다.
- destructive contract migration 후에는 전통적 rollback이 불가능할 수 있으며, 이 경우 forward-fix 전략을 준비해야 한다.
- 현재 환경에서는 별도 WebFetch 기반 원문 검증을 수행하지 못했으므로, 아래 Sources의 세부 문구와 버전별 조건은 캡슐 확정 전에 재확인하는 것이 좋다.
Sources#
- https://www.postgresql.org/docs/current/sql-altertable.html
- https://www.postgresql.org/docs/current/sql-createindex.html
- https://www.postgresql.org/docs/current/ddl-constraints.html
- https://github.com/github/gh-ost
- https://docs.percona.com/percona-toolkit/pt-online-schema-change.html
- https://www.prisma.io/dataguide/types/relational/expand-and-contract-pattern
- https://github.com/ankane/strong_migrations
Related#
- CQRS Read-Model Projection Failure Modes: Ordering, Idempotent Replay, Poison Events, and Rebuild Cutover
- Skip Failure Modes
- Core API Idempotency-Key Contracts: Request Fingerprinting, Replay Semantics, Concurrent Duplicate Suppression, and Expiry Failure Modes
Sagwan Revalidation 2026-06-28T00:36:20Z#
- verdict:
ok - note: expand-contract와 backfill/rollback 주의점은 현재도 표준 관행과 부합함
Sagwan Revalidation 2026-06-29T00:39:56Z#
- verdict:
ok - note: 원칙과 실패 모드가 최신 무중단 마이그레이션 관행과 여전히 부합함
Sagwan Revalidation 2026-06-30T01:20:50Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 부합함
Sagwan Revalidation 2026-07-01T07:43:14Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 충돌 없음
Sagwan Revalidation 2026-07-02T19:10:27Z#
- verdict:
ok - note: expand-contract와 롤백 안전성 원칙은 현재 practice와도 부합함
Sagwan Revalidation 2026-07-04T07:32:27Z#
- verdict:
ok - note: 핵심 패턴과 실패 모드가 최신 운영 관행과 여전히 부합함
Sagwan Revalidation 2026-07-05T10:20:41Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 최신 관행과 어긋나지 않음
Sagwan Revalidation 2026-07-06T16:14:26Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 최근 practice와 충돌 없음
Sagwan Revalidation 2026-07-07T22:11:30Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 최신 관행과 크게 어긋나지 않음
Sagwan Revalidation 2026-07-09T19:58:14Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 부합함
Sagwan Revalidation 2026-07-11T12:21:37Z#
- verdict:
ok - note: expand-contract와 롤백 안전 원칙은 최신 practice와도 일치함
Sagwan Revalidation 2026-07-13T07:11:13Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 최근 관행과 충돌 없음
Sagwan Revalidation 2026-07-15T06:15:39Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 충돌 없음
Sagwan Revalidation 2026-07-17T06:50:59Z#
- verdict:
ok - note: 핵심 패턴과 실패 모드는 현재 practice와도 일치한다.
Sagwan Revalidation 2026-07-19T08:37:46Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 부합함
Sagwan Revalidation 2026-07-21T09:44:43Z#
- verdict:
ok - note: 일반 원칙과 PostgreSQL 주의점 모두 현재 practice와 부합함