Logical replication과 CDC: publication/subscription, slot lag, 재동기화
Logical replication은 "복제본 하나 더"가 아니라 변경 이벤트 계약이다
PostgreSQL의 logical replication은 데이터 파일 블록을 그대로 따라가는 physical streaming replication과 다르다. 변경된 row를 논리적인 형태로 해석해서 publication이 내보내고, subscription이 받아서 subscriber 테이블에 적용한다. 이 구조 때문에 같은 major version이 아니어도 이관 경로를 만들 수 있고, 전체 DB가 아니라 일부 테이블만 복제할 수 있으며, CDC(Change Data Capture) 파이프라인의 출발점으로도 쓸 수 있다.
하지만 운영 관점에서는 장점만큼 함정도 분명하다. logical replication은 schema DDL을 자동으로 복제하지 않는다. sequence 값도 자동으로 맞춰주지 않는다. subscriber에서 같은 테이블에 직접 쓰기를 허용하면 conflict가 생길 수 있다. 그리고 replication slot이 뒤처지면 publisher의 pg_wal이 계속 보존되어 디스크 장애로 이어질 수 있다.
그래서 이 장의 목표는 CREATE PUBLICATION과 CREATE SUBSCRIPTION 문법을 외우는 것이 아니다. publication/subscription을 변경 이벤트 계약으로 보고, slot lag와 재동기화 절차까지 운영 가능한 형태로 설계하는 것이다.
기본 흐름: snapshot copy 이후 WAL change stream
Logical replication을 시작하면 보통 두 단계가 일어난다.
- subscriber가 publisher의 publication에 연결한다.
- 기존 테이블 데이터는 초기 snapshot으로 복사하고, 그 이후 publisher에서 발생한 변경은 WAL 기반 logical decoding stream으로 계속 받아 적용한다.
이를 단순화하면 다음과 같다.
여기서 중요한 운영 포인트는 replication slot이다. subscriber가 어느 LSN까지 받았고 flush했다고 확인해야 publisher가 그 이전 WAL을 정리할 수 있다. subscriber가 멈추거나 apply가 느려지면 slot의 restart_lsn이 뒤처지고, publisher는 필요한 WAL을 지우지 못한다. logical replication 장애가 자주 pg_wal 디스크 사용량 증가로 먼저 보이는 이유가 여기에 있다.
Publication 설계: 무엇을 내보낼 것인가
Publication은 publisher 데이터베이스 안에서 정의되는 변경 집합이다. 테이블 단위로 만들 수도 있고, schema 단위나 ALL TABLES로 만들 수도 있다. 기본적으로 INSERT, UPDATE, DELETE, TRUNCATE를 모두 복제하지만, 필요하면 operation을 제한할 수 있다.
예를 들어 주문과 결제 테이블의 변경만 내보내는 publication은 다음처럼 만들 수 있다.
CREATE PUBLICATION app_cdc_pub
FOR TABLE public.orders, public.payments
WITH (publish = 'insert, update, delete');운영에서는 ALL TABLES가 편해 보여도 무조건 좋은 선택은 아니다. 새 테이블이 자동으로 복제 대상이 되는 편의와, 의도하지 않은 대용량 테이블이나 민감 테이블이 CDC 경로에 들어가는 위험을 함께 판단해야 한다. 데이터 플랫폼으로 내보내는 CDC라면 보통 다음 기준을 먼저 정한다.
| 설계 항목 | 확인할 질문 |
|---|---|
| 테이블 범위 | 어떤 consumer가 어떤 테이블을 실제로 필요로 하는가? |
| DML 범위 | TRUNCATE까지 downstream에서 안전하게 처리할 수 있는가? |
| row filter | 일부 tenant, 일부 상태 row만 복제해도 되는가? |
| column list | 민감 컬럼이나 큰 payload 컬럼을 제외할 필요가 있는가? |
| partition | leaf partition 기준으로 보낼지, root table identity로 보낼지 정했는가? |
UPDATE와 DELETE를 복제하려면 replica identity도 확인해야 한다. 기본은 primary key다. primary key가 없는 테이블에서 UPDATE나 DELETE를 publication에 포함하면 publisher에서 해당 DML이 실패할 수 있다. 임시 대응으로 REPLICA IDENTITY FULL을 쓸 수 있지만, subscriber가 row를 찾을 때 비용이 커질 수 있으므로 장기 설계로는 primary key 또는 적절한 unique index를 마련하는 편이 낫다.
ALTER TABLE public.orders REPLICA IDENTITY USING INDEX orders_pkey;
-- 최후의 수단에 가깝다. 넓은 row와 높은 write rate에서는 비용을 먼저 검증한다.
ALTER TABLE public.legacy_events REPLICA IDENTITY FULL;Subscription 설계: 어디에, 어떤 방식으로 적용할 것인가
Subscription은 subscriber 쪽에서 만든다. connection string, 구독할 publication 목록, slot 생성 여부, 초기 데이터 copy 여부 같은 동작을 정의한다.
CREATE SUBSCRIPTION app_cdc_sub
CONNECTION 'host=publisher.example.internal port=5432 dbname=app user=repl_user password=... application_name=app_cdc_sub'
PUBLICATION app_cdc_pub
WITH (
create_slot = true,
copy_data = true,
enabled = true
);실무에서는 CREATE SUBSCRIPTION을 바로 운영 DB에 실행하기 전에 다음 조건을 맞춘다.
- subscriber에 대상 schema와 table이 이미 존재해야 한다.
- table 이름은 fully qualified name 기준으로 맞아야 한다.
- column은 이름으로 매칭된다. 순서는 달라도 되지만, 타입 변환 가능성과 기본값을 확인해야 한다.
- subscriber 테이블에만 있는 추가 컬럼은 default value가 있어야 안전하다.
- application이 subscriber에 직접 write하지 못하도록 권한을 분리한다.
초기 copy가 큰 테이블에서는 copy_data = true가 subscriber와 publisher 양쪽에 부하를 만든다. 특히 initial sync worker가 오래 돌면 subscription 상태가 ready가 되기 전까지 운영자가 진행률과 lock, I/O를 같이 봐야 한다. 이미 별도 dump/restore나 snapshot으로 데이터를 맞춰 놓은 경우에는 copy_data = false를 선택할 수 있지만, 그때는 시작 LSN과 데이터 일관성 검증 책임이 운영자에게 넘어온다.
Slot lag는 두 가지로 나눠 봐야 한다
Logical replication 지연을 볼 때 subscriber가 느리다로 끝내면 원인을 놓치기 쉽다. 최소한 두 관점으로 나눠야 한다.
- publisher slot 관점: slot이 얼마나 오래된 WAL을 붙잡고 있는가?
- subscriber apply 관점: subscriber가 받은 변경을 얼마나 늦게 적용하고 있는가?
Publisher에서는 pg_replication_slots와 pg_stat_replication을 함께 본다.
SELECT
slot_name,
slot_type,
plugin,
active,
wal_status,
safe_wal_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS unconfirmed_wal,
restart_lsn,
confirmed_flush_lsn
FROM pg_replication_slots
WHERE slot_type = 'logical'
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;Subscriber에서는 apply worker와 table sync 상태를 본다.
SELECT
subname,
worker_type,
relid::regclass AS rel,
received_lsn,
latest_end_lsn,
latest_end_time,
now() - latest_end_time AS apply_delay
FROM pg_stat_subscription
ORDER BY subname, worker_type;초기 동기화나 특정 table만 지연되는 상황에서는 pg_subscription_rel이 유용하다.
SELECT
s.subname,
r.srrelid::regclass AS rel,
r.srsubstate,
r.srsublsn
FROM pg_subscription_rel r
JOIN pg_subscription s ON s.oid = r.srsubid
ORDER BY s.subname, rel;srsubstate가 i, d, f, s, r 중 어디에 머무는지 보면 initialize, data copy, synchronized, ready 중 어느 단계에서 막혔는지 좁힐 수 있다.
운영 알림은 보통 다음 세 종류를 분리한다.
| 알림 | 의미 | 1차 대응 |
|---|---|---|
| inactive logical slot | consumer가 붙어 있지 않음 | 의도된 중지인지 확인, 불필요하면 slot 제거 |
| retained WAL 증가 | slot 때문에 pg_wal이 보존됨 | subscriber 상태, 네트워크, apply error 확인 |
| apply delay 증가 | subscriber 적용이 늦음 | subscriber lock, constraint error, CPU/I/O, long transaction 확인 |
max_slot_wal_keep_size는 안전장치로 볼 수 있다. 무한 WAL 보존을 막아주지만, 한도를 넘어서 필요한 WAL이 제거되면 해당 slot 기반 복제는 더 이상 이어갈 수 없고 재동기화가 필요할 수 있다. 즉 이 값은 장애를 없애는 설정이 아니라, 디스크 전체 장애와 복제 재구축 사이의 손실 경계를 정하는 설정이다.
DDL, sequence, conflict는 자동으로 사라지지 않는다
Logical replication을 운영 복제나 CDC의 기반으로 쓸 때 가장 자주 놓치는 부분은 "복제되지 않는 것"이다.
PostgreSQL 문서 기준으로 schema와 DDL 명령은 logical replication 대상이 아니다. 초기 schema는 pg_dump --schema-only 등으로 맞출 수 있지만, 이후 schema 변경은 별도 배포 절차로 맞춰야 한다. additive change는 보통 subscriber를 먼저 바꾸고 publisher를 나중에 바꾸는 편이 안전하다. 예를 들어 publisher가 새 컬럼을 포함한 변경을 보내기 시작했는데 subscriber 테이블에 그 컬럼이 없거나 변환할 수 없으면 apply가 실패한다.
Sequence도 별도 관리가 필요하다. serial이나 identity 컬럼의 값 자체는 row 데이터로 복제되지만, sequence object의 현재 값이 자동으로 따라가지는 않는다. subscriber를 read-only 분석 DB로만 쓰면 문제가 작지만, switchover나 이관 후 subscriber를 writer로 승격할 계획이라면 sequence 값을 충분히 앞으로 맞추는 절차가 필요하다.
Conflict는 subscriber에서 constraint 위반이나 row 불일치가 생길 때 발생한다. 단방향 복제라면 가장 단순한 예방책은 subscriber를 read-only로 취급하는 것이다. 양방향 또는 multi-writer 구성을 만들면 conflict resolution 정책이 별도 시스템 설계 문제가 된다. PostgreSQL 내장 logical replication은 conflict를 마법처럼 병합해 주는 도구가 아니다.
재동기화: slot을 살릴지, 테이블만 다시 맞출지 결정한다
Logical replication 장애 대응에서 가장 중요한 판단은 현재 slot으로 계속 따라잡을 수 있는가다. 아래 순서로 판단하면 불필요한 전체 재구축을 줄일 수 있다.
- publisher의 slot이 살아 있고 필요한 WAL이 남아 있는지 확인한다.
- subscriber apply worker가 떠 있는지, error로 멈췄는지 확인한다.
- 특정 테이블 sync만 실패했는지, subscription 전체가 깨졌는지 분리한다.
- DDL mismatch, missing replica identity, unique violation, 네트워크 단절 중 무엇인지 로그와 catalog로 확인한다.
- 현재 slot을 유지하며 재시작할 수 있는지, 아니면 table 또는 subscription을 다시 sync해야 하는지 결정한다.
가벼운 schema mismatch라면 subscriber schema를 맞춘 뒤 subscription을 다시 enable하는 것으로 끝날 수 있다.
ALTER SUBSCRIPTION app_cdc_sub DISABLE;
-- subscriber schema 수정 또는 충돌 row 정리
ALTER SUBSCRIPTION app_cdc_sub ENABLE;Publication에 새 테이블을 추가했거나 publication 정의를 바꿨다면 subscriber에서 refresh가 필요하다.
ALTER SUBSCRIPTION app_cdc_sub REFRESH PUBLICATION;특정 테이블만 다시 맞춰야 하는 경우에는 서비스 영향도를 따져야 한다. 작은 테이블이면 subscriber 테이블을 비우고 다시 copy하는 방식이 가능하지만, 큰 테이블에서는 별도 snapshot 적재와 검증, cutover window가 필요하다. subscription을 drop/recreate하면 동기화 정보가 사라져 다시 sync해야 하므로, 단순 재시작처럼 취급하면 안 된다.
CDC 도구가 logical decoding slot을 직접 소비하는 구조라면 재동기화 판단은 더 엄격해진다. Debezium 같은 consumer가 마지막 offset을 어디까지 안전하게 처리했는지, downstream sink가 idempotent한지, 중복 이벤트를 허용하는지까지 함께 확인해야 한다. PostgreSQL slot의 LSN만 맞아도 downstream 상태가 맞지 않으면 데이터 품질 사고가 된다.
운영 체크리스트
Logical replication을 운영에 넣기 전에는 다음 항목을 runbook으로 남겨두는 것이 좋다.
wal_level = logical,max_replication_slots,max_wal_senders가 용량에 맞게 설정되어 있는가?- publication 대상 테이블에 primary key 또는 적절한 replica identity가 있는가?
- subscriber schema 배포 순서가 정의되어 있는가?
- sequence 동기화가 필요한 전환 시나리오가 있는가?
- subscriber direct write를 막는 권한 정책이 있는가?
- logical slot별
retained_wal,active,wal_status알림이 있는가? - subscription별 apply delay와 table sync 상태를 보고 있는가?
max_slot_wal_keep_size한도 초과 시 재동기화 절차가 문서화되어 있는가?- publication 변경 후
ALTER SUBSCRIPTION ... REFRESH PUBLICATION을 누가, 언제 실행하는지 정해져 있는가? - CDC downstream에서 중복, 순서, 재처리, idempotency 정책이 정의되어 있는가?
Logical replication은 PostgreSQL 운영자가 데이터 플랫폼과 만나는 경계에 있다. DB 안에서는 WAL과 slot의 문제처럼 보이지만, 밖에서는 consumer lag, schema contract, backfill, 재처리 정책의 문제가 된다. 이 경계를 명시적으로 설계해야 장애 때 "복제가 늦다"와 "데이터가 틀렸다"를 구분할 수 있다.
References
- PostgreSQL Documentation, "Chapter 29. Logical Replication" — https://www.postgresql.org/docs/current/logical-replication.html
- PostgreSQL Documentation, "29.1. Publication" — https://www.postgresql.org/docs/current/logical-replication-publication.html
- PostgreSQL Documentation, "29.2. Subscription" — https://www.postgresql.org/docs/current/logical-replication-subscription.html
- PostgreSQL Documentation, "29.8. Restrictions" — https://www.postgresql.org/docs/current/logical-replication-restrictions.html
- PostgreSQL Documentation, "29.10. Monitoring" — https://www.postgresql.org/docs/current/logical-replication-monitoring.html
- PostgreSQL Documentation, "29.7. Conflicts" — https://www.postgresql.org/docs/current/logical-replication-conflicts.html
- PostgreSQL Documentation, "53.20. pg_replication_slots" — https://www.postgresql.org/docs/current/view-pg-replication-slots.html
- PostgreSQL Documentation, "52.55. pg_subscription_rel" — https://www.postgresql.org/docs/current/catalog-pg-subscription-rel.html