(KO) PostgreSQL 복제 — WAL 파일 배송에서 논리적 Pub/Sub까지
(KO) PostgreSQL 복제 — WAL 파일 배송에서 논리적 Pub/Sub까지
섹션 제목: “(KO) PostgreSQL 복제 — WAL 파일 배송에서 논리적 Pub/Sub까지”PostgreSQL이 복제 기능을 “추가”했다기보다는, 이미 존재하던 WAL 스트림을 더 세밀하게 재활용하는 방법을 계속 발견해온 역사다. 각 시대는 크래시 복구를 위해 이미 있던 WAL 스트림을 재노출한다. 처음에는 외부 스크립트로 복사하는 16 MB 세그먼트 파일 단위로, 그 다음에는 libpq 위에서 밀어 보내는 연속 바이트 스트림으로, 마지막에는 구독자가 일반 SQL로 적용할 수 있는 디코딩된 논리적 행 변경 시퀀스로 발전했다. 관통하는 원리는 하나다. “WAL이 이미 진실이다. 더 싸고 더 선택적으로 배송하는 방법을 찾아라.”
이 문서는 복제가 주요 릴리스마다 어떻게, 왜 바뀌었는지를 추적하며 REL_18 설계에서 끝을 맺는다. 개별 서브시스템의 메커니즘은 여기서 재도출하지 않는다. 각 시대의 교차 링크로 현재 상태 모듈 문서를 따라가면 된다.
- 왜 이 서브시스템이 진화해야 했는가 — 8.x의 원초적 한계
- 타임라인 — 시대별 릴리스
- 8.x — WAL 파일 배송을 통한 웜 스탠바이 —
restore_command,pg_standby - 9.0 — 스트리밍 복제 + 핫 스탠바이 — walsender/walreceiver, 읽기 전용 스탠바이
- 9.1–9.2 — 동기 복제, 이후 캐스케이딩 —
syncrep, 스탠바이의 스탠바이 - 9.4 — 논리적 디코딩 + 복제 슬롯 — 리오더 버퍼, 스냅샷 빌더, 슬롯 장부
- 10 — 내장 논리적 복제(Pub/Sub) —
PUBLICATION/SUBSCRIPTION, pgoutput, 어플라이 워커 - 14–15 — 선택적·트랜잭션 논리 복제 — 2PC, 행 필터, 열 목록
- 17 — 페일오버 슬롯과 슬롯 동기화 —
synced슬롯, slotsync 워커 - REL_18 현재 설계 — 현재 상태와 PG19 전망
- 출처
왜 이 서브시스템이 진화해야 했는가
섹션 제목: “왜 이 서브시스템이 진화해야 했는가”WAL은 원래 크래시 복구 하나만을 위해 존재했다. 크래시 후 스타트업 프로세스는 마지막 체크포인트 이후 WAL 레코드를 순서대로 재생해 데이터 파일을 일관된 상태로 되돌린다. 이 재생 루프가 이후 복제 기능들의 전체 기반이다. 핵심 통찰은 하나였다. 크래시 서버를 복구하는 재생 루프는, WAL 사본을 공급받는 두 번째 서버에서 영구히 계속 재생할 수 있다.
그러나 8.x 시대에 WAL을 두 번째 서버로 옮기는 방법은 아카이브뿐이었다. archive_command가 완성된 16 MB 세그먼트를 어딘가에 복사하고, 스탠바이의 restore_command가 그것을 가져다 재생했다. 당시로서는 충분한 HA 구조였으나 구조적 한계가 뚜렷했다.
- 전송 단위가 세그먼트 전체다. 스탠바이는 16 MB WAL 세그먼트 전체가 채워지고 아카이브될 때까지 어떤 트랜잭션도 볼 수 없었다. 조용한 시스템이라면 복제 지연이 몇 분에 달했다.
- 스탠바이는 복구 중에 아무 일도 할 수 없었다. 복구 중인 서버는 연결을 전혀 받지 못했다. 레플리카는 읽기 용량을 전혀 생산하지 못하는 냉비(cold spare)였다.
- 피드백이 없었다. 프라이머리는 세그먼트를 아카이브로 눈감고 밀어 넣었다. 어느 스탠바이가 얼마나 소비했는지, 재활용하려는 세그먼트가 아직 필요한지 알 방법이 없었다.
- 물리 복사, 전부 아니면 전무. 클러스터 전체를 바이트 단위로, 같은 버전·같은 아키텍처의 스탠바이에 복제했다. 테이블 하나, 행 일부, 메이저 버전 간 복제, 스탠바이의 쓰기 허용은 불가능했다.
이후 각 시대는 이 한계 중 하나씩을 공략한다. 스트리밍은 세분성 문제와 냉비 낭비를 공략한다. 슬롯은 눈먼 배송 문제를 공략한다. 논리적 디코딩과 Pub/Sub는 “물리적·전부 아니면 전무” 제약을 공략한다. 17 시대 작업은 슬롯이 열어 놓은 페일오버 내구성 공백을 공략한다.
타임라인
섹션 제목: “타임라인”timeline
title PostgreSQL 복제 진화
section 파일 시대
8.2 / 8.3 : 연속 아카이빙<br/>restore_command : pg_standby contrib<br/>웜 스탠바이
section 스트리밍 시대
9.0 : 스트리밍 복제<br/>walsender / walreceiver : 핫 스탠바이<br/>스탠바이에서 읽기 전용 쿼리
9.1 : 동기 복제<br/>synchronous_standby_names : pg_basebackup<br/>스트리밍 전용 셋업
9.2 : 캐스케이딩 복제<br/>스탠바이가 스탠바이에게 : 스트리밍 전용 스탠바이<br/>아카이브 불필요
section 논리적 시대
9.4 : 논리적 디코딩<br/>리오더 버퍼 + 스냅샷 빌더 : 복제 슬롯<br/>물리적·논리적 모두
section Pub/Sub 시대
10 : 내장 논리적 복제<br/>PUBLICATION / SUBSCRIPTION : pgoutput 플러그인<br/>어플라이 + 테이블싱크 워커
14 : 2PC 디코딩<br/>출력 플러그인 PREPARE : 진행 중 트랜잭션 스트리밍<br/>대형 트랜잭션
15 : 행 필터 + 열 목록<br/>선택적 복제 : 2PC 구독<br/>병렬 어플라이(14/16)
section 복원력 시대
17 : 페일오버 슬롯<br/>failover = true : 슬롯 동기화<br/>스탠바이의 slotsync 워커
18 : 현재 상태<br/>REL_18_STABLE : (PG19 예정: 논리 DDL / 시퀀스 확장)
8.x — WAL 파일 배송을 통한 웜 스탠바이
섹션 제목: “8.x — WAL 파일 배송을 통한 웜 스탠바이”릴리스: 연속 아카이빙과 포인트-인-타임 복구는 8.0에 추가됐다. 웜 스탠바이 패턴 — 아카이브된 세그먼트를 지속적으로 재생하며 영구히 복구 상태에 머무는 서버 — 은 8.2에서 실용화됐고, pg_standby contrib 헬퍼가 8.3에 추가됐다.
변화 내용. PostgreSQL 8.0은 archive_command를 도입했다. WAL 세그먼트가 채워지면 서버는 운영자가 제공한 셸 명령을 실행해 그 16 MB 파일을 내구성 있는 어딘가(NFS 마운트, 다른 호스트, 테이프)에 복사한다. 스탠바이의 recovery.conf에는 대응하는 restore_command가 있다. 이 서버는 영구 복구 상태를 유지하면서 WAL이 소진될 때마다 그 명령으로 다음 세그먼트를 가져다 재생한다. pg_standby(8.3)는 스마트한 restore_command로서 다음 세그먼트가 나타날 때까지 기다리며 소비된 세그먼트를 정리했다.
이유. WAL은 7.1부터 크래시 복구 전용으로 존재했다. 8.x는 처음으로 WAL을 복제 수송 수단으로 의도적으로 재활용했다. 복구 재생 루프는 이미 있었고 이미 정확했다. 웜 스탠바이는 단지 그 루프가 절대로 끝나지 않도록 만든 것이다.
구조적 형태 — 이전 → 이후.
flowchart LR
subgraph Primary8x["프라이머리 8.x"]
BE["백엔드<br/>WAL 기록"] --> WALSEG["WAL 세그먼트<br/>16 MB 파일 채움"]
WALSEG -->|archive_command| ARCH["아카이브<br/>NFS / scp / 테이프"]
end
subgraph Standby8x["웜 스탠바이 8.x"]
REST["restore_command<br/>pg_standby"] --> STARTUP["스타트업 프로세스<br/>redo 루프"]
STARTUP --> DATA["데이터 파일<br/>일관성 유지"]
end
ARCH -.->|다음 16 MB 세그먼트<br/>폴링| REST
STARTUP -.->|연결 불가| X(("읽기<br/>차단"))
다이어그램에서 핵심 속성이 드러난다. 전송 단위는 세그먼트 파일 전체이고, 두 서버 사이 연결은 대역 외 아카이브다. PostgreSQL 프로세스끼리는 직접 통신하지 않는다. 스탠바이는 순수 복구 상태라 클라이언트 연결을 전부 거부한다. 프라이머리-스탠바이 간 소켓도, 피드백 채널도, 스탠바이를 쿼리할 방법도 없다.
왜 바뀌어야 했는가. 세 가지 고통이 지배적이었다. 세그먼트 채움 시간에 묶인 분 단위 지연, 읽기 용량을 전혀 생산하지 못하는 스탠바이, 아무도 소비했는지 모르고 세그먼트를 배송하는 프라이머리. 다음 두 릴리스가 이 셋을 동시에 공략했다.
교차 링크: 이 시대가 도입한 세그먼트/아카이브 메커니즘은 오늘날에도 스트리밍의 폴백 경로로 남아 있으며, postgres-archiving-walsummary.md와 postgres-xlog-wal.md에 문서화돼 있다.
9.0 — 스트리밍 복제 + 핫 스탠바이
섹션 제목: “9.0 — 스트리밍 복제 + 핫 스탠바이”릴리스: PostgreSQL 9.0 — 이 전체 호弧에서 단연 가장 중요한 릴리스. 두 가지 보완적 기능을 동시에 출시해 냉비 파일 배송 스탠바이를 실시간·쿼리 가능·저지연 레플리카로 바꿨다.
스트리밍 복제 변화. 16 MB 세그먼트가 채워지고 아카이브될 때까지 기다리는 대신, 스탠바이가 프라이머리에 복제 연결을 열어 WAL이 생성되는 즉시 레코드 단위로 스트리밍 받는다. 두 프로세스가 새로 생겼다.
- 프라이머리의 walsender — 복제 연결마다 포크되는 특수 백엔드. WAL을 읽어 소켓으로 밀어 보낸다.
- 스탠바이의 walreceiver — 프라이머리 walsender에 연결해 WAL 스트림을 수신하고, 로컬 WAL에 쓴 뒤 스타트업 프로세스를 깨워 재생시킨다.
이것이 src/backend/replication/walsender.c와 src/backend/replication/walreceiver.c의 탄생이다. 두 파일은 REL_18에서도 여전히 수송 핵심이다. 복제 프로토콜에는 와이어 프로토콜 위에 새 메시지 타입(START_REPLICATION, XLogData/'w' 바이트 스트림, 킵얼라이브)이 추가됐다.
핫 스탠바이 변화. 별도의 동등하게 큰 변화도 있었다. 복구 중인 서버가 읽기 전용 연결을 받아 쿼리를 실행할 수 있게 됐다. 이를 위해 복구 재생 루프가 프라이머리의 진행 중인 트랜잭션 스냅샷을 게시하고(스탠바이가 어느 XID가 가시적인지 알 수 있도록), 복구 충돌(스탠바이의 쿼리 대 그 쿼리가 필요한 행을 청소하는 VACUUM)을 관리하고, max_standby_streaming_delay와 hot_standby_feedback으로 그 충돌을 조정하도록 가르쳐야 했다.
이유. 스트리밍은 세분성 한계를 없앤다(지연이 분에서 밀리초로 줄었다). 외부 아카이브 의존도 사라진다. 핫 스탠바이는 냉비 낭비를 없앤다. 레플리카가 읽기 트래픽을 처리하면서 HA 하드웨어가 읽기 확장 하드웨어가 됐다.
구조적 형태 — 이전 → 이후.
flowchart LR
subgraph Primary90["프라이머리 9.0"]
BE["백엔드"] --> WAL["로컬 WAL"]
WAL --> WS["walsender<br/>연결마다"]
end
subgraph Standby90["핫 스탠바이 9.0"]
WR["walreceiver"] --> SWAL["스탠바이 WAL"]
SWAL --> SU["스타트업 프로세스<br/>연속 redo"]
SU --> SDATA["데이터 파일"]
RO["읽기 전용<br/>백엔드"] --> SDATA
end
WS -->|libpq 복제 연결<br/>XLogData 스트림| WR
WR -.->|스탠바이 응답<br/>flush/apply LSN| WS
8.x 다이어그램과 비교하면, 점선의 대역 외 아카이브가 직접 소켓(walsender → walreceiver)으로 교체됐다. 전송 단위는 16 MB 파일이 아니라 WAL 레코드다. 피드백 채널이 생겼다(스탠바이가 수신/플러시/적용 LSN을 walsender에게 보고한다). RO 읽기 전용 백엔드가 스탠바이 데이터 파일에 붙어 있다. 아카이브 경로는 스탠바이가 너무 뒤처져서 프라이머리가 필요한 세그먼트를 재활용한 경우의 폴백으로만 남는다.
왜 더 진화해야 했는가. 9.0 스트리밍은 비동기였다. 프라이머리는 스탠바이를 기다리지 않고 커밋했다. 프라이머리 크래시 시 와이어를 건너지 못한 마지막 커밋 몇 건이 손실될 수 있었다. 토폴로지도 평면적이었다. 스탠바이 N개가 모두 프라이머리에 직접 연결하면 walsender N개가 전부 프라이머리 WAL을 읽었다. 다음 두 릴리스가 이 둘을 모두 고쳤다.
교차 링크: 현대적 walsender/walreceiver 수송, 복제 프로토콜 메시지, 스탠바이 응답 피드백은 postgres-wal-sender-receiver.md에, 복구 redo와 핫 스탠바이 충돌 처리는 postgres-recovery-redo.md에 문서화돼 있다.
9.1–9.2 — 동기 복제, 이후 캐스케이딩
섹션 제목: “9.1–9.2 — 동기 복제, 이후 캐스케이딩”두 릴리스는 9.0이 만든 토폴로지를 강화한다. 9.1은 내구성 보증을 추가하고, 9.2는 프라이머리에 부하를 쌓지 않고 팬아웃을 추가한다.
9.1 — 동기 복제
섹션 제목: “9.1 — 동기 복제”변화 내용. 9.1은 synchronous_standby_names와 이와 연동하는 synchronous_commit 수준을 추가했다. 스탠바이가 동기로 지정되면, 프라이머리의 커밋 트랜잭션은 스탠바이가 커밋 WAL 수신(그리고 수준에 따라 플러시 또는 적용)을 확인할 때까지 클라이언트에게 성공을 반환하지 않는다. 커밋하는 백엔드가 커밋 레코드를 쓴 뒤 대기 큐에서 블록되고, 스탠바이의 응답 LSN이 커밋 LSN을 지나면 walsender가 깨우는 방식으로 구현된다. 이것이 src/backend/replication/syncrep.c의 탄생이며 REL_18에서도 기능의 핵심으로 남아 있다.
이유. 9.0 스트리밍은 비동기였다. 프라이머리 크래시 시 와이어를 건너지 못한 마지막 커밋 몇 건이 조용히 사라질 수 있었다. “클라이언트가 커밋 성공을 받았다면, 프라이머리 장애 후에도 살아남는다”는 보증이 필요한 워크로드에는 답이 없었다. 동기 복제는 커밋 레이턴시 왕복 하나를 내주고 페일오버 시 데이터 손실 제로를 얻는다.
비용은 synchronous_commit으로 세밀하게 조정할 수 있다. off(로컬 플러시도 기다리지 않음), local(로컬 플러시만 기다림), remote_write(스탠바이가 수신해 OS에 씀), on(스탠바이가 디스크에 플러시), 나중에 추가된 remote_apply(스탠바이가 재생했으므로 스탠바이 읽기에서 보임). 내구성은 클러스터 전체 모드가 아니라 트랜잭션별 다이얼이 됐다.
9.2 — 캐스케이딩 복제
섹션 제목: “9.2 — 캐스케이딩 복제”변화 내용. 9.2는 스탠바이가 자체 walsender를 실행해 WAL을 더 하위 스탠바이에게 공급할 수 있게 했다. 스탠바이는 더 이상 리프가 아니라 복제 트리의 내부 노드가 될 수 있다. 9.2는 또한 스탠바이가 아카이브 없이 스트리밍만으로 운영되도록 했고, pg_basebackup(9.1 도입)으로 스탠바이 클론이 수동 파일시스템 복사 + pg_start_backup/pg_stop_backup 절차 대신 내장 명령 하나로 끝나게 됐다.
이유. 평면적인 9.0 토폴로지에서는 스탠바이마다 프라이머리에 직접 연결했다. 보고용 레플리카 10개면 프라이머리에 walsender 10개가 WAL을 읽고 네트워크 대역폭을 소비했다. 캐스케이딩은 트리를 만들 수 있게 한다. 프라이머리가 스탠바이 두 개에게 공급하고 각각이 다섯 개에게 공급하면, 프라이머리의 팬아웃 비용이 제한되고 하위 대역폭은 중간 노드로 분산된다.
구조적 형태 — 이전 → 이후.
flowchart TB
subgraph Flat["9.0 — 평면 팬아웃"]
P0["프라이머리<br/>walsender N개"]
P0 --> A0["스탠바이 A"]
P0 --> B0["스탠바이 B"]
P0 --> C0["스탠바이 C"]
end
subgraph Casc["9.2 — 캐스케이딩 트리"]
P1["프라이머리<br/>walsender 2개"]
P1 --> M1["스탠바이<br/>캐스케이딩: 자체 walsender"]
P1 --> M2["스탠바이<br/>캐스케이딩: 자체 walsender"]
M1 --> L1["리프 스탠바이"]
M1 --> L2["리프 스탠바이"]
M2 --> L3["리프 스탠바이"]
M2 --> L4["리프 스탠바이"]
end
평면 구조는 walsender N개를 프라이머리에 집중시킨다. 캐스케이딩 구조는 프라이머리를 두 개로 고정하고 나머지 팬아웃 비용을 내부 노드로 분산시킨다.
왜 더 진화해야 했는가. 지금까지는 모두 물리적이다. 바이트 단위 복사, 클러스터 전체, 같은 버전, 스탠바이에서 읽기 전용. 테이블 하나를 복제하거나, 데이터를 변환하거나, 메이저 버전 간 복제하거나, PostgreSQL 외부 소비자에게 공급하는 것은 불가능했다. WAL은 배송됐지만 결코 해석되지 않았다. 9.4가 그것을 열었다.
교차 링크: 동기 커밋 대기 큐,
synchronous_standby_names문법(FIRST/ANY쿼럼 세트),synchronous_commit수준은 postgres-synchronous-replication.md에, 깨우기를 구동하는 스탠바이 응답 피드백은 postgres-wal-sender-receiver.md에 있다.
9.4 — 논리적 디코딩 + 복제 슬롯
섹션 제목: “9.4 — 논리적 디코딩 + 복제 슬롯”릴리스: PostgreSQL 9.4 — 개념적 전환점. 이전까지 “WAL”은 그대로 재생하는 불투명 바이트 스트림이었다. 9.4는 WAL을 논리적 행 변경 스트림으로 디코딩하는 것을 가능하게 하고, 연속 소비자가 안전하게 사용할 수 있는 장부 객체를 도입했다.
복제 슬롯
섹션 제목: “복제 슬롯”변화 내용. 복제 슬롯은 특정 소비자가 얼마나 진행했는지와 서버가 그 소비자를 위해 어떤 자원을 유지해야 하는지를 기록하는 작은 영속 서버 상태 조각이다. 두 종류가 있다.
- 물리적 슬롯은 스탠바이가 아직 수신하지 못한 WAL 세그먼트를 프라이머리가 재활용하지 못하게 막는다(
restart_lsn). 8.x/9.0에서 프라이머리가 스탠바이가 여전히 필요한 세그먼트를 재활용해 아카이브로 강제 폴백되던 경쟁 조건을 닫는다. - 논리적 슬롯은
catalog_xmin— 카탈로그 행 버전을 유지해야 하는 가장 오래된 트랜잭션 ID — 도 고정한다. 논리적 소비자가 그 WAL이 작성될 당시 카탈로그를 조회해 오래된 WAL을 디코딩할 수 있게 하기 위해서다.
이것이 src/backend/replication/slot.c의 탄생이며, REL_18에서도 슬롯 기계가 된다. 8.x 시대의 “눈감고 배송, 누군가 필요했기를 기도하라” 문제를 드디어 해결한 것이 슬롯이다. 서버는 이제 누가 소비하고 있고 얼마나 뒤처졌는지를 알며, 필요한 것을 버리기를 거부한다(소비자가 죽으면 WAL이 무한 증가하는 비용이 있다. 나중에 max_slot_wal_keep_size가 추가된 이유다).
논리적 디코딩
섹션 제목: “논리적 디코딩”변화 내용. 논리적 디코딩은 물리적 WAL을 읽어 트랜잭션별로, 커밋 순서대로 논리적 변경(어느 테이블에 어느 열 값으로 INSERT/UPDATE/DELETE가 발생했는지)의 시퀀스를 재구성한다. 세 가지 어려운 문제를 해결해야 했고, 각각이 모듈이 됐다.
- 트랜잭션 재조립 — 동시 트랜잭션의 WAL 레코드는 디스크에 뒤섞여 있지만, 논리적 소비자는 트랜잭션 전체를 커밋 순서대로 받고 싶다. 리오더 버퍼(
src/backend/replication/logical/reorderbuffer.c)는 XID별 변경을 버퍼링하고 커밋 시 방출한다. - 행의 의미 파악 — 힙 튜플을 이름 있는 열 값으로 되돌리려면 그 WAL이 기록될 당시 카탈로그가 필요하다. 현재 카탈로그는 그 사이 바뀌었을 수 있다. 스냅샷 빌더(
src/backend/replication/logical/snapbuild.c)는 카탈로그 변경 트랜잭션을 관찰해 과거 카탈로그 스냅샷을 구성한다.catalog_xmin(슬롯이 고정)은 필요한 카탈로그 행이 여전히 존재함을 보증한다. - 플러그인 출력 — 디코딩된 스트림은 소비자가 원하는 방식으로 포매팅하는 출력 플러그인에게 전달된다. 9.4는 프레임워크와
test_decodingcontrib 플러그인만 출시했다. 내장 플러그인은 10에서 등장한다.
src/backend/replication/logical/decode.c는 이 모든 것의 하단에 있는 WAL 레코드-논리적 변경 변환기다.
이유. 이 단일 릴리스가 모든 “물리적 전용” 제약을 원칙적으로 한꺼번에 제거했다. 논리적 변경 스트림은 한 테이블로 필터링하거나, 변환하거나, 메이저 버전 간에 배송하거나, PostgreSQL이 아닌 소비자(메시지 큐, 분석 싱크)에게 공급할 수 있다. 9.4는 아직 그 위에 엔드투엔드 복제 제품을 출시하지 않았다. 전체 기반만 만들었다.
구조적 형태 — 물리적 vs. 논리적 소비.
flowchart LR
WAL["디스크의 WAL<br/>뒤섞인 레코드"]
subgraph Physical["물리적 경로 (9.0)"]
WAL --> WSP["walsender<br/>물리적"]
WSP --> WRP["walreceiver"]
WRP --> REDOP["스타트업 redo<br/>그대로 재생"]
end
subgraph Logical["논리적 경로 (9.4)"]
WAL --> DEC["decode.c<br/>레코드→변경"]
DEC --> RB["리오더 버퍼<br/>XID별, 커밋 순서"]
SB["스냅샷 빌더<br/>과거 카탈로그"] -.-> RB
SLOT["논리적 슬롯<br/>restart_lsn + catalog_xmin"] -.-> DEC
RB --> PLUG["출력 플러그인<br/>test_decoding (9.4)"]
PLUG --> CONS["임의 소비자<br/>SQL / 큐 / 앱"]
end
물리적 경로는 불투명 바이트를 재생한다. 논리적 경로는 그것을 이름 있는 변경으로 디코딩하고, 트랜잭션별로 재정렬하고, 과거 카탈로그로 의미를 입히고, 플러그인 포매터에게 넘긴다. 슬롯은 필요한 WAL과 카탈로그 행을 살려두어 전체를 보증한다.
왜 더 진화해야 했는가. 9.4는 변경 스트림과 contrib 플러그인을 줬지만, 구독자도, “이 테이블을 복제하라”고 선언하는 DDL도, 반대쪽에서 변경을 적용하는 워커도 없었다. 실제 복제 파이프라인을 구축하려면 여전히 익스텐션(pglogical 등)을 조립해야 했다. 10이 그것을 일급 시민으로 만들었다.
교차 링크: 리오더 버퍼, 스냅샷 빌더, 디코딩 프레임워크는 postgres-logical-decoding.md에, 슬롯 라이프사이클,
restart_lsn,catalog_xmin보존은 postgres-replication-slots.md에 있다.
10 — 내장 논리적 복제(Pub/Sub)
섹션 제목: “10 — 내장 논리적 복제(Pub/Sub)”릴리스: PostgreSQL 10 — 9.4의 디코딩 기반을 엔드투엔드 SQL 관리 복제 제품으로 변환한 릴리스. 코어 서버에 기본 내장됐다.
변화 내용. 10은 전체 Pub/Sub 인터페이스와 그 뒤의 기계를 추가했다.
CREATE PUBLICATION— 소스에서 논리적 변경 스트림으로 노출할 테이블 집합(그리고 어느 연산: insert/update/delete)을 이름 붙여 선언한다.CREATE SUBSCRIPTION— 대상에서 퍼블리셔에 연결하고, 거기에 논리적 복제 슬롯을 만들고, 변경을 당겨 적용하기 시작한다.- pgoutput(
src/backend/replication/pgoutput/pgoutput.c) — 9.4가test_decoding을 위해 남겨둔 자리를 마침내 채운 내장 출력 플러그인. 구독자가 이해하는 컴팩트 바이너리 논리 복제 프로토콜을 사용하며, 퍼블리케이션을 인식해 구독된 퍼블리케이션의 테이블 변경만 방출한다. - 어플라이 워커 + 테이블 싱크 워커 — 구독자 쪽에서 백그라운드 어플라이 워커가 pgoutput 스트림을 수신해 각 변경을 일반 힙 연산으로 실행자에 넘겨 적용한다. 정상 상태 적용이 시작되기 전, 테이블별 테이블 싱크 워커가 기존 테이블 내용을 복사(초기
COPY)하고 어플라이 위치까지 따라잡는다.src/backend/replication/logical/worker.c와tablesync.c에 있다. - 논리적 복제 런처(
launcher.c) —pg_subscription을 감시하며 구독이 생성·삭제될 때 어플라이 워커를 시작·종료하는 슈퍼바이저 백그라운드 워커.
이유. 10 이전에는 논리적 복제를 구축하려면 9.4 기본 요소 위에 익스텐션(pglogical)을 조립해야 했다. 자체 출력 플러그인, 자체 적용 프로세스, 자체 DDL이 필요했다. 10은 모든 것을 코어 SQL 객체와 코어 워커로 표준화했다. 9.4가 해제한 제약들이 사용 가능해졌다. 테이블 일부 복제, 메이저 버전 간 복제(구독자가 더 새로운 릴리스일 수 있다), 여러 퍼블리셔를 하나의 구독자로 통합. 물리적 “전부 아니면 전무, 같은 버전, 읽기 전용” 모델에 마침내 일급 시민 논리적 대응물이 생겼다.
구조적 형태 — 물리적 스트림 vs. 논리적 Pub/Sub.
flowchart LR
subgraph Pub["퍼블리셔 (PG 10)"]
PWAL["WAL"] --> PDEC["논리적 디코딩<br/>decode + reorder"]
PSLOT["논리적 슬롯<br/>구독마다"] -.-> PDEC
PDEC --> PGO["pgoutput<br/>퍼블리케이션 필터"]
PUBDEF["CREATE PUBLICATION<br/>테이블 집합"] -.-> PGO
PGO --> PWS["walsender<br/>논리적 모드"]
end
subgraph Sub["구독자 (PG 10)"]
LAUN["런처<br/>pg_subscription 감시"] --> APPLY["어플라이 워커"]
LAUN --> TSYNC["테이블 싱크 워커<br/>초기 COPY"]
PWS -->|논리 복제 프로토콜| APPLY
APPLY --> EXEC["실행자<br/>힙 insert/update/delete"]
TSYNC --> EXEC
EXEC --> SDATA["구독자 테이블<br/>쓰기 가능, 자체 인덱스"]
end
walsender는 재사용된다. 그러나 이제는 논리적 모드로 pgoutput을 구동하며, 반대쪽 끝에서 런처가 감시하는 어플라이 워커가 디코딩된 변경을 일반 실행자로 완전히 쓰기 가능한 테이블에 재생한다. 구독자는 바이트 단위 미러가 아니라 독립된 실제 데이터베이스다.
왜 더 진화해야 했는가. 10의 Pub/Sub는 세 가지 점에서 거칠었다. 행 전체·테이블 전체를 복제했다(행 필터 없음, 열 투영 없음). 커밋된 트랜잭션만 복제할 수 있었다(PREPARE/2PC 없음, 매우 큰 트랜잭션은 보내기 전에 완전히 버퍼링해야 했다). 어플라이 쪽은 구독당 단일 스레드였다. 14와 15가 이 공백들을 메웠다.
교차 링크: pgoutput의 프로토콜, 메시지 타입, 퍼블리케이션 인식은 postgres-pgoutput.md에, 런처, 어플라이 워커, 테이블 싱크 상태 머신은 postgres-logical-replication-apply.md에 있다.
14–15 — 선택적·트랜잭션 논리 복제
섹션 제목: “14–15 — 선택적·트랜잭션 논리 복제”10의 Pub/Sub 시스템은 완전하지만 거칠었다. 14와 15는 두 축에서 이를 다듬는다. 트랜잭션 의미론(진행 중 트랜잭션과 2PC 트랜잭션 스트리밍)과 선택성(원하는 행과 열만 복제).
14 — 2PC 디코딩과 진행 중 트랜잭션 스트리밍
섹션 제목: “14 — 2PC 디코딩과 진행 중 트랜잭션 스트리밍”변화 내용. 트랜잭션 흐름에 관한 두 가지 개선이 이뤄졌다.
- 대형 진행 중 트랜잭션 스트리밍. 이전에는 리오더 버퍼가 pgoutput이 방출하기 전에 트랜잭션 전체를 커밋까지 보유해야 했다. 거대한 트랜잭션은 퍼블리셔 메모리 급증(또는 디스크 스필)과 긴 정지를 일으켰다. 14는 디코더가 아직 열린 트랜잭션의 변경을 구독자에게 스트리밍할 수 있게 했다. 구독자는 그것을 버퍼링하고 커밋 시에만 구체화한다. 퍼블리셔 메모리를
logical_decoding_work_mem으로 제한할 수 있게 됐다. - 출력 플러그인 수준의 2PC 디코딩. 디코딩 프레임워크와 pgoutput이
PREPARE TRANSACTION을 최종COMMIT PREPARED만 보는 것이 아니라 별도 이벤트로 디코딩·방출하는 능력을 갖췄다(two_phase출력 플러그인 옵션,pgoutput.c에서 여전히 볼 수 있다).
이유. 대형 트랜잭션의 메모리 압박은 실제 운영 위험이었다. 참된 2PC 지원은 분산 트랜잭션을 충실히 복제하기 위한 전제 조건이다. 준비 상태가 구독자에 전달되어야 하며, 단순히 최종 커밋만 전달해서는 안 된다.
15 — 행 필터, 열 목록, 2PC 구독
섹션 제목: “15 — 행 필터, 열 목록, 2PC 구독”변화 내용. 15는 퍼블리케이션을 진정으로 선택적인 선언으로 바꾸고 구독자 쪽 2PC 이야기를 완성했다.
- 행 필터.
CREATE PUBLICATION ... WHERE (expr)으로 퍼블리케이션이 조건에 맞는 행만 방출하게 한다. pgoutput은 방출 전에 행마다 필터를 평가한다.RowFilterPubAction기계와 액션별ExprState배열이pgoutput.c에 있다. 액션(insert/update/delete)마다 독립적으로 필터링할 수 있다. - 열 목록.
CREATE PUBLICATION ... (col1, col2)으로 퍼블리케이션이 행의 열 투영만 방출하게 한다. 구독자가 더 좁은 스키마를 가지거나 폭넓고 미사용 열을 배송하지 않을 수 있다. - 2PC 구독.
CREATE SUBSCRIPTION ... WITH (two_phase = true)로 구독자가 14에서 퍼블리셔에게 방출 방법을 가르쳤던 준비 트랜잭션 이벤트를 처리하게 된다. 퍼블리셔의 PREPARE가 구독자의 PREPARE가 되고, COMMIT PREPARED도 넘어온다.
이유. 행 필터와 열 목록은 논리적 복제를 실제 데이터 배포 패턴에서 사용 가능하게 만드는 것이다. 테넌트별 샤딩(tenant_id 필터), 보고용 레플리카에 민감하지 않은 열만 복제, 다운스트림 시스템에 좁은 슬라이스 공급. 2PC 구독은 분산 트랜잭션 워크로드의 정확성 보증을 확장한다.
병렬 어플라이(applyparallelworker.c)도 이 작업과 함께 등장했다(14/16 창에서 개선 추가). 구독자가 큰 스트리밍 트랜잭션을 단일 어플라이 워커로 직렬화하는 대신 전용 워커에서 적용할 수 있게 됐다.
왜 더 진화해야 했는가. 논리적 복제는 이제 풍부한 선택성과 트랜잭션 충실도를 갖췄다. 그러나 구조적 내구성 공백이 남아 있었다. 논리적 슬롯은 프라이머리에만 존재했다. 프라이머리가 실패하고 물리적 스탠바이가 승격되면, 슬롯이 새 프라이머리에 존재하지 않았다. 모든 구독자의 복제 위치가 사라지고 구독이 끊겼다. 17이 그 공백을 닫았다.
교차 링크: 출력 플러그인 내부의 행 필터·열 목록 평가는 postgres-pgoutput.md에, 스트리밍 및 2PC 어플라이(병렬 어플라이 워커 포함)는 postgres-logical-replication-apply.md에 있다.
17 — 페일오버 슬롯과 슬롯 동기화
섹션 제목: “17 — 페일오버 슬롯과 슬롯 동기화”릴리스: PostgreSQL 17 — 논리적 복제가 물리적 페일오버에서 살아남도록 만든 릴리스. 슬롯이 프라이머리 전용 상태로 도입된 9.4 이후 열려 있던 마지막 구조적 공백을 닫았다.
변화 내용. 17은 논리적 슬롯에 페일오버 표시를 하고 물리적 스탠바이에 동기화해, 그 스탠바이가 승격될 때 슬롯이 이미 거기 있어서 구독자가 재연결해 중단된 곳부터 계속할 수 있게 하는 기능을 추가했다.
- 페일오버 슬롯.
failover = true로 생성(또는 변경)된 논리적 슬롯에 플래그가 설정돼 시스템이 스탠바이에게 동기화해야 함을 안다. - 슬롯 동기화. 물리적 스탠바이의 새 slotsync 워커(
src/backend/replication/logical/slotsync.c)가 주기적으로 프라이머리에서 페일오버 슬롯 상태를 가져와synced슬롯을 로컬에 만들거나 전진시킨다.sync_replication_slotsGUC나pg_sync_replication_slots()로 구동된다. 스탠바이 사본은restart_lsn과catalog_xmin을 프라이머리에 맞춰 추적한다. 미래에 승격될 인스턴스에 필요한 WAL과 카탈로그 행을 정확히 보존한다.
동기화된 슬롯은 스탠바이가 실제로 수신한 것과 프라이머리의 물리 복제가 확인한 것을 절대로 앞서지 않도록 억제된다. 그렇지 않으면 구독자가 승격된 스탠바이가 받지 못한 데이터를 지나쳐 재개할 수 있다. slotsync.c 저작권 헤더(2024-2025)가 이것이 17 시대 개발임을 나타낸다.
이유. 17 이전에는 “논리적 복제 + 물리적 스탠바이를 통한 HA”가 모순이었다. 스탠바이 승격이 죽은 프라이머리에만 있던 슬롯을 파괴했기 때문에 모든 구독자의 위치가 사라졌다. 운영자들은 취약한 외부 스크립팅으로 대처했다. 페일오버 슬롯은 위치를 승격 후에도 내구적으로 만든다. 논리적 구독자는 물리적 스탠바이가 항상 그래왔던 것처럼 프라이머리 페일오버를 타고 넘어간다.
구조적 형태 — 페일오버에 걸친 슬롯 내구성.
flowchart LR
subgraph Before["17 이전 — 페일오버 시 슬롯 소멸"]
P0["프라이머리<br/>논리적 슬롯"] -->|물리 WAL| S0["스탠바이<br/>슬롯 없음"]
P0 -->|논리| SUB0["구독자"]
S0 -.->|승격| S0P["새 프라이머리<br/>슬롯 소멸"]
S0P -.->|구독<br/>끊김| SUB0
end
subgraph After["PG 17 — 동기화된 페일오버 슬롯"]
P1["프라이머리<br/>failover=true 슬롯"] -->|물리 WAL| S1["스탠바이<br/>slotsync 워커"]
P1 -.->|슬롯 상태| S1
S1 --> SYN["synced 슬롯<br/>restart_lsn이 프라이머리 추적"]
P1 -->|논리| SUB1["구독자"]
S1 -.->|승격| S1P["새 프라이머리<br/>슬롯 존재"]
S1P -->|구독자 재개| SUB1
end
“이전” 토폴로지에서 스탠바이는 데이터를 가지지만 슬롯은 없어 승격 시 구독자가 끊긴다. “이후” 토폴로지에서 slotsync 워커가 줄곧 페일오버 슬롯을 미러링해왔다. 승격된 스탠바이에는 이미 준비된 슬롯이 있어 구독자가 재연결하고 계속 진행한다.
교차 링크: 슬롯 내부 —
restart_lsn,catalog_xmin, 지속성,synced/failover플래그 — 는 postgres-replication-slots.md에, 소비 어플라이 쪽은 postgres-logical-replication-apply.md에 있다.
REL_18 현재 설계
섹션 제목: “REL_18 현재 설계”REL_18_STABLE(이 문서가 추적하는 소스)에서 위의 모든 것은 하나의 설계의 레이어로 공존한다. 디렉터리 구조가 역사를 반영한다.
- 물리적 스트리밍 수송 —
src/backend/replication/walsender.c와walreceiver.c가 물리적 WAL 스트림(9.0)과 논리적 모드의 pgoutput 변경 스트림(10) 모두를 운반한다. 모든 종류의 복제에 공통 수송이다. postgres-wal-sender-receiver.md 참고. - 동기 커밋 —
src/backend/replication/syncrep.c가FIRST/ANY쿼럼 세트와 함께 트랜잭션별 내구성 다이얼(9.1)을 제공한다. postgres-synchronous-replication.md 참고. - 복제 슬롯 —
src/backend/replication/slot.c가 물리적·논리적 소비자 모두를 위한 자원 보존(9.4)을 제공한다. 이제failover/synced플래그와 스탠바이 동기화(17)도 포함한다. postgres-replication-slots.md 참고. - 논리적 디코딩 —
src/backend/replication/logical/가 디코딩 프레임워크, 리오더 버퍼, 스냅샷 빌더(9.4)와 스트리밍·2PC 지원(14)을 담는다. postgres-logical-decoding.md 참고. - 내장 Pub/Sub — 같은
logical/디렉터리가 어플라이 워커, 런처, 테이블 싱크, 병렬 어플라이, 슬롯 싱크 워커(10, 14/15, 17)를 담고,src/backend/replication/pgoutput/pgoutput.c가 행 필터·열 목록(15)을 가진 퍼블리케이션 인식 출력 플러그인이다. postgres-logical-replication-apply.md와 postgres-pgoutput.md 참고.
최종 효과는 이렇다. 현대 클러스터는 같은 프라이머리에서 HA용 물리 스탠바이와 선택적 데이터 배포용 논리 구독자를 동시에 운영할 수 있다. 필요한 곳에 동기 내구성을 쓸 수 있고, 논리적 위치는 이제 물리적 페일오버에서도 살아남는다. 10년에 걸친 호弧 — 거친 파일 배송 → 바이트 스트리밍 → 디코딩된 변경 스트림 → 선택적 Pub/Sub → 페일오버 내구 슬롯 — 는 다중 출력 레이어를 가진 단일 WAL 재사용 아키텍처로 수렴했다.
PG19 전망. REL_18 이후 개발은 물리 복제 대비 아직 공백이 있는 논리 쪽을 계속 확장한다. 스키마 변경과 시퀀스 진행이 수동 개입 없이 전파되는 더 넓은 DDL과 시퀀스 처리, 슬롯 동기화 강화, 충돌 감지 개선이 방향이다. 이것은 전망 메모일 뿐이다. REL_18 설계가 현재의 권위 있는 상태다.
릴리스 노트(기능 귀속):
- PostgreSQL 8.3 릴리스 노트 —
pg_standbycontrib, 웜 스탠바이 개선. - PostgreSQL 9.0 릴리스 노트 — 스트리밍 복제, 핫 스탠바이.
- PostgreSQL 9.1 릴리스 노트 — 동기 복제,
pg_basebackup. - PostgreSQL 9.2 릴리스 노트 — 캐스케이딩 복제, 스트리밍 전용 스탠바이.
- PostgreSQL 9.4 릴리스 노트 — 논리적 디코딩, 복제 슬롯.
- PostgreSQL 10 릴리스 노트 — 내장 논리적 복제,
PUBLICATION/SUBSCRIPTION, pgoutput. - PostgreSQL 14 릴리스 노트 — 2PC 디코딩, 진행 중 트랜잭션 스트리밍.
- PostgreSQL 15 릴리스 노트 — 퍼블리케이션 행 필터·열 목록, 2PC 구독.
- PostgreSQL 17 릴리스 노트 — 논리 복제 페일오버 슬롯과 슬롯 동기화.
현재 상태 모듈 문서(메커니즘 — 여기서 재도출하지 않는다):
- postgres-wal-sender-receiver.md — 스트리밍 수송.
- postgres-synchronous-replication.md —
synchronous_commit과 대기 큐. - postgres-replication-slots.md —
restart_lsn,catalog_xmin, failover/synced 슬롯. - postgres-logical-decoding.md — 리오더 버퍼, 스냅샷 빌더, 디코딩 프레임워크.
- postgres-logical-replication-apply.md — 런처, 어플라이 워커, 테이블 싱크, 병렬 어플라이.
- postgres-pgoutput.md — 내장 출력 플러그인, 행 필터, 열 목록.
- postgres-overview-replication-ha.md — 복제/HA 서브시스템 개요.
핵심 소스 파일(REL_18_STABLE, 커밋 273fe94 기준):
src/backend/replication/walsender.c,walreceiver.c— 스트리밍 수송(9.0+).src/backend/replication/syncrep.c— 동기 복제(9.1+).src/backend/replication/slot.c,slotfuncs.c— 복제 슬롯(9.4+).src/backend/replication/logical/decode.c,reorderbuffer.c,snapbuild.c— 논리적 디코딩(9.4+).src/backend/replication/logical/worker.c,launcher.c,tablesync.c,applyparallelworker.c— 어플라이 쪽(10, 14/15).src/backend/replication/logical/slotsync.c— 슬롯 동기화(17).src/backend/replication/pgoutput/pgoutput.c— 내장 출력 플러그인(10), 행 필터/열 목록(15).