논리 복제(logical replication)를 실제 운영에서 굴려본 사람이라면 한 번쯤 겪는다. HA 구성 잘 짜놨고, 프라이머리가 죽어도 스탠바이로 넘어가는 건 문제 없다. 근데 넘어가고 나서 CDC 파이프라인이 조용히 멈춰있다. Debezium 로그를 보면 "replication slot does not exist". 이유는 단순하다 — 복제 슬롯은 프라이머리에만 있었고, 스탠바이가 승격되는 순간 그 슬롯의 커밋 위치 정보가 통째로 증발한 거다.
PostgreSQL 17에서 failover slots가 정식으로 들어왔고, 18에서 1년치 버그 패치가 얹혀서 이제 프로덕션에서 쓸만해졌다. 이번 글은 그 세팅을 처음부터 끝까지 밟아보는 실무 가이드다. RDS를 쓰는 팀에게도 해당되는 이야기가 있어서 뒤에서 같이 다룬다.
왜 이게 문제였나
이해가 빠르려면 왜 예전엔 안 됐는지부터 보는 게 좋다.
논리 복제 슬롯은 "이 구독자가 어디까지 읽었는지"를 프라이머리의 WAL 상에 붙잡아두는 마커다. 프라이머리에서 WAL이 계속 흐르면서 슬롯 위치가 밀리고, 구독자가 그걸 따라 읽어간다. 여기서 문제는, 이 슬롯이 물리 복제로 스탠바이에 자동으로 복사되지 않는다는 점이었다. WAL은 넘어가지만 슬롯 메타데이터는 안 넘어간다.
그래서 스탠바이가 승격되면 이런 일이 벌어진다.
- 구독자(Debezium, pg_recvlogical, 다른 postgres 등)는 "slot my_slot에서 다시 읽을게" 하고 붙는다
- 승격된 새 프라이머리에는 my_slot이 없다
- 구독자는 실패한다. 재생성하면 위치가 초기화되면서 데이터 중복이나 유실이 발생한다
수많은 팀이 이 문제를 자체적으로 해결해왔다. 슬롯 위치를 주기적으로 스탠바이에 밀어넣는 스크립트를 짜거나, pg_failover_slots 익스텐션을 쓰거나, 승격 후 잠깐 CDC를 멈추고 부트스트랩을 다시 하거나. 다 아프다.
17의 failover 슬롯이 하는 일
17부터는 슬롯 자체에 failover = true 플래그를 붙일 수 있게 됐다. 이 플래그가 붙은 슬롯은 스탠바이가 프라이머리로부터 주기적으로 동기화한다. 동기화는 스탠바이의 백그라운드 워커(slotsync worker)가 담당한다.
이렇게 하면 스탠바이가 승격됐을 때 슬롯이 이미 그 위치에 살아있다. 구독자는 재접속만 하면 그대로 이어서 읽는다. 이론상 그렇다. 실제로는 몇 가지 조건을 만족시켜야 한다.
최소 세팅
프라이머리 postgresql.conf부터 손을 대야 한다.
# 프라이머리
wal_level = logical
max_replication_slots = 20 # 넉넉하게
max_wal_senders = 20
스탠바이 쪽은 slotsync 관련 파라미터가 새로 추가됐다.
# 스탠바이
sync_replication_slots = on
primary_slot_name = 'standby_slot_1' # 물리 복제 슬롯 이름
hot_standby_feedback = on
hot_standby_feedback = on은 필수다. 이게 없으면 스탠바이의 슬롯이 프라이머리보다 뒤처지는 순간 프라이머리의 WAL이 잘려나가고, 동기화가 깨진다.
프라이머리에는 스탠바이 전용 물리 복제 슬롯을 만들어둬야 한다. 그래야 스탠바이의 hot_standby_feedback이 프라이머리에 도달한다.
-- 프라이머리에서
SELECT pg_create_physical_replication_slot('standby_slot_1');
논리 슬롯 생성
여기부터가 핵심이다. 이미 논리 슬롯을 쓰고 있다면 재생성 없이는 failover 플래그를 켤 수 없다. 이건 아프다. 하지만 방법이 없다.
새로 만드는 경우는 이렇게.
SELECT pg_create_logical_replication_slot(
'cdc_orders',
'pgoutput',
false, -- temporary
true -- twophase
);
-- 17 이후엔 failover 플래그를 별도로 세팅
ALTER_REPLICATION_SLOT cdc_orders (failover);
CREATE SUBSCRIPTION으로 구독자 쪽에서 만드는 경우엔 옵션 하나 붙이면 된다.
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=primary.internal ...'
PUBLICATION pub_orders
WITH (failover = true, copy_data = false);
동기화가 되고 있는지 확인
세팅 후 스탠바이에서 확인해야 할 뷰가 두 개다.
-- 스탠바이에서
SELECT slot_name, failover, synced, confirmed_flush_lsn
FROM pg_replication_slots;
SELECT * FROM pg_stat_replication_slots;
synced = t이고 confirmed_flush_lsn이 프라이머리의 값과 근접해있으면 정상이다. 여기서 프라이머리와의 차이(랙)를 모니터링해야 한다. 우리 팀은 5분 이상 벌어지면 알림이 오도록 걸어놨다.
동기화가 안 되는 경우 흔한 원인들:
- 스탠바이의 hot_standby_feedback이 꺼져있음
- 프라이머리의 물리 슬롯이 없거나 이름이 안 맞음
- 스탠바이가 프라이머리보다 뒤처져서 slotsync 워커가 대기 중
- pg_hba.conf에서 replication 커넥션이 막혀있음 (당연한 얘긴데 놓치기 쉽다)
승격 시나리오 리허설
실제로 이게 되는지 확인하려면 스테이징에서 페일오버를 해봐야 한다. 우리는 이런 순서로 검증한다.
- CDC 구독자를 붙여서 데이터가 흐르는 상태로 둔다
- 프라이머리에 부하를 주고 몇 분 지속시킨다
- 스탠바이에서
pg_replication_slots의confirmed_flush_lsn이 계속 밀리는지 확인 - 프라이머리를 강제 종료 (
kill -9또는 EC2 stop) - Patroni 또는 pg_auto_failover가 스탠바이를 승격시키게 둔다
- 승격 완료 후 구독자를 새 프라이머리로 재접속
- 데이터가 이어서 흐르는지, 중복/유실 없는지 확인
이 시나리오에서 특히 봐야 할 게, 승격 직전 몇 초간의 트랜잭션이 슬롯 동기화에 반영됐는지다. slotsync는 주기적(기본 10초)으로 돌기 때문에, 그 사이에 있던 변경분은 이미 스탠바이로 넘어왔지만 슬롯이 아직 그 위치까지 안 밀려있을 수 있다. 이 경우 구독자는 몇 초 분량을 다시 읽게 된다. 멱등하게 짜뒀다면 문제 없다. 안 그러면 여기서 아프다.
RDS/Aurora 쓴다면
AWS RDS for PostgreSQL은 17.4부터 이 기능을 지원한다. 다만 몇 가지 제약이 있다.
- 리드 리플리카 승격 시 동기화된 슬롯이 자동으로 사용 가능 상태가 되지만, 승격 후
pg_sync_replication_slots()를 한 번 호출해야 최종 상태가 맞춰진다 - Aurora PostgreSQL은 아키텍처가 달라서(스토리지 공유형) 이 방식 대신 자체 CDC 스트리밍을 쓴다. 이번 글의 세팅은 해당 없음
- 파라미터 그룹에서
sync_replication_slots를 켜야 하고, RDS 콘솔에서 리부트 필요
RDS 쓰는 팀은 승격 러너에 SELECT pg_sync_replication_slots(); 한 줄만 추가해두면 실전에서 큰 사고를 막을 수 있다.
아직 남은 아쉬움
이걸 다 세팅해도, 커스텀 output plugin을 쓴다면 failover 지원 여부는 플러그인 구현에 달렸다. pgoutput은 기본 지원, wal2json은 최근 버전이 지원, 그 외는 확인해야 한다.
그리고 다중 스탠바이 환경에서 어떤 스탠바이로 페일오버하냐에 따라 슬롯 상태가 조금씩 다르다. 여러 스탠바이 모두에 sync_replication_slots = on을 걸어두면 동기화 부하가 프라이머리에 쌓인다. 우리 팀은 대상 스탠바이 하나에만 켜두고, 다른 하나는 순수 읽기용으로 분리했다.
정리
failover 슬롯은 CDC 파이프라인의 오래된 통증을 대부분 해결한다. 하지만 "켜기만 하면 되는" 기능은 아니고, 물리 복제 슬롯, hot_standby_feedback, output plugin 호환성, 승격 러너 등이 다 맞물려야 한다. 처음 세팅할 땐 스테이징에서 페일오버를 최소 세 번은 돌려보길 권한다.
다음에는 pg_failover_slots 익스텐션(16 이하 버전용)과의 마이그레이션 이야기를 다뤄보려고 한다. 아직 17로 못 넘어간 팀에게 필요할 것 같아서.
추가 리소스
'IT > DB 운영' 카테고리의 다른 글
| Postgres 18 async I/O, io_uring 켜봤다가 반나절 날린 이야기 (0) | 2026.08.18 |
|---|---|
| PgBouncer transaction pool에서 prepared statement 쓰기, 이젠 진짜 된다 (0) | 2026.08.18 |
| PgBouncer vs pgcat vs Supavisor, 2026년에 뭘 쓸까 (0) | 2026.08.06 |
| Redis vs Valkey, 지금 갈아탈 때인가 (1) | 2026.08.02 |
| PgBouncer transaction pool로 갈아탄 다음 날 새벽에 벌어진 일 (0) | 2026.07.31 |