왜 pg_upgrade와 덤프/복원으로는 부족한가
pg_upgrade는 빠르지만 실행 중 데이터베이스를 정지시켜야 하고, 롤백이 사실상 어렵다. pg_dump/restore는 대용량에서 수 시간이 걸린다. 24/7 서비스에서 이 정지 시간(수 분~수십 분)은 그대로 장애로 이어진다. 논리 복제(logical replication)는 구버전과 신버전을 동시에 띄운 채 데이터를 실시간 동기화하고, 준비가 끝나면 애플리케이션 연결만 신버전으로 전환한다. 실제 무중단(전환 시 수 초)에 가깝고, 문제가 생기면 구버전으로 되돌리기도 쉽다.
핵심 원리: WAL을 논리적 변경으로 디코딩
논리 복제는 물리 복제와 달리 블록 단위가 아니라 행 단위(INSERT/UPDATE/DELETE)로 변경을 전달한다. 덕분에 서로 다른 메이저 버전, 심지어 다른 스키마 사이에서도 동작한다. 발행자(publisher)가 PUBLICATION으로 대상 테이블을 노출하고, 구독자(subscriber)가 SUBSCRIPTION으로 이를 당겨간다.
전제 조건 점검
| 항목 | 요구사항 |
|---|---|
| wal_level | logical |
| 기본키 | 모든 테이블에 PK 또는 REPLICA IDENTITY 필요 |
| 스키마 | 구독자에 미리 DDL 생성(논리 복제는 DDL 미전파) |
| 대상 제외 | 시퀀스 값은 별도 동기화 필요 |
발행자(구버전) 설정
# postgresql.conf (구버전, 예: PG13)
wal_level = logical
max_wal_senders = 10
max_replication_slots = 10
# 재시작 후 발행 생성
psql -h old-db -U postgres -d app -c "
CREATE PUBLICATION mig_pub FOR ALL TABLES;
"
FOR ALL TABLES는 편리하지만, 대용량 이력 테이블 등 굳이 복제할 필요 없는 것은 테이블을 명시해 초기 동기화 부하를 줄이는 편이 낫다.
구독자(신버전) 준비와 초기 동기화
먼저 신버전(예: PG16)에 스키마만 옮긴다. 데이터는 논리 복제가 채운다.
# 스키마만 덤프해서 신버전에 적용
pg_dump -h old-db -U postgres -d app \
--schema-only --no-publications --no-subscriptions \
| psql -h new-db -U postgres -d app
# 구독 생성 → 초기 복사 + 스트리밍 시작
psql -h new-db -U postgres -d app -c "
CREATE SUBSCRIPTION mig_sub
CONNECTION 'host=old-db dbname=app user=repl password=***'
PUBLICATION mig_pub
WITH (copy_data = true, streaming = true);
"
복제 지연 모니터링
전환 시점을 판단하려면 지연이 0에 수렴하는지 확인해야 한다. 발행자에서 슬롯의 지연을 바이트로 본다.
-- 발행자에서 실행
SELECT slot_name,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
) AS lag
FROM pg_replication_slots;
-- 구독자에서 워커 상태 확인
SELECT subname, received_lsn, latest_end_lsn
FROM pg_stat_subscription;
lag이 안정적으로 수 KB 이하로 유지되면 전환 준비가 된 것이다.
전환(cutover)과 시퀀스 처리
논리 복제는 시퀀스의 현재값을 복제하지 않는다. 전환 직전 쓰기를 잠시 멈추고 시퀀스를 맞춘 뒤 연결을 돌린다.
# 1) 애플리케이션 쓰기 중단(읽기 전용 or 배포 게이트)
# 2) 잔여 지연 0 확인
# 3) 시퀀스 동기화
psql -h old-db -U postgres -d app -Atc "
SELECT 'SELECT setval('||quote_literal(schemaname||'.'||sequencename)
||','||last_value||');'
FROM pg_sequences WHERE last_value IS NOT NULL;
" | psql -h new-db -U postgres -d app
# 4) 커넥션 문자열/PgBouncer를 new-db로 전환
# 5) 구독 정리
psql -h new-db -d app -c "DROP SUBSCRIPTION mig_sub;"
주의점
- 대용량 초기 복사:
copy_data중 발행자 WAL이 쌓여 디스크가 찰 수 있다. 슬롯 지연과 디스크를 함께 감시하라. - PK 없는 테이블: UPDATE/DELETE가 실패하거나 전체 행 비교로 느려진다.
REPLICA IDENTITY FULL로 우회하되 성능 저하를 감안한다. - DDL 미전파: 마이그레이션 기간 동안 스키마 변경을 동결하거나, 양쪽에 수동 적용한다.
- 롤백 대비: 전환 후에도 구버전을 일정 기간 유지하고, 필요 시 역방향 논리 복제를 걸어두면 되돌리기가 안전하다.
이 절차는 다운타임을 초 단위로 줄이면서 검증과 롤백 여지를 남긴다는 점에서, 정지가 허용되지 않는 서비스의 메이저 업그레이드에 현실적인 선택지다.