10장. Replication — 한 대가 죽어도 서비스는 계속된다
9장의 WAL은 복구용 일기장이자, 복제의 방송 원본이기도 합니다. WAL 스트리밍이 스탠바이로 흐르는 길, 동기/비동기의 트레이드오프, 그리고 페일오버의 순간까지.
① Physical vs Logical — 두 종류의 복제와 선택 기준 ② WAL 스트리밍이 스탠바이로 흐르고 적용되는 과정(write/flush/replay lag) ③ 동기/비동기 커밋의 차이와 쿼럼 구성 ④ 페일오버 시나리오와 replication slot의 빛과 그림자
10.1 두 종류의 복제 — Physical vs Logical
PostgreSQL의 복제는 크게 두 갈래입니다. Physical 복제는 WAL 바이트를 그대로 스트리밍해 "클러스터 전체의 완전한 쌍둥이"를 만들고, Logical 복제는 WAL을 해석(디코드)해 "행 단위 변경"만 골라 보냅니다.
| 항목 | Physical Replication | Logical Replication |
|---|---|---|
| 복제 단위 | WAL 바이트 (블록·레코드) | 논리 row 변경 (INSERT/UPDATE/DELETE) |
| 스코프 | 클러스터 전체 (모든 DB·객체) | 지정한 PUBLICATION (특정 테이블) |
| 스탠바이 쓰기 | 불가 (read-only) | 가능 (독립 primary) |
| DDL 복제 | 자동으로 함께 | 안 됨 (v17까지) — 수동 적용 |
| 메이저 버전 | 동일해야 함 | 서로 달라도 됨 → 업그레이드에 활용 |
| 주 용도 | HA, 읽기 분산, DR | CDC, 마이그레이션, 부분 복제 |
선택 기준은 간단합니다: "전체를 똑같이, 장애 시 즉시 전환" → Physical, "특정 테이블만, 다른 버전/스키마로" → Logical. 이 장은 HA의 근간인 Physical 스트리밍을 중심으로 봅니다.
10.2 WAL 스트리밍 — 커밋이 스탠바이에 닿기까지
Primary에서 커밋된 WAL은 walsender 프로세스가 네트워크로 흘려보내고, 스탠바이의 walreceiver가 받아 저장하고, startup 프로세스가 리플레이합니다. 각 단계마다 지연(lag)이 측정됩니다.
Primary에서 트랜잭션이 커밋됩니다. WAL이 로컬 pg_wal/에 fsync된 지점이 commit LSN입니다.
walsender가 WAL을 네트워크로 스트리밍하고, 스탠바이의 walreceiver가 받아서 OS에 write합니다. 여기까지의 지연이 write_lag — 주로 네트워크 상태를 반영합니다.
스탠바이가 받은 WAL을 자기 디스크에 fsync합니다. 여기까지가 flush_lag — 이 지점부터는 스탠바이가 크래시해도 WAL이 보존됩니다.
startup 프로세스가 WAL을 리플레이해 실제 데이터에 반영합니다. 여기까지가 replay_lag — 스탠바이의 읽기 쿼리에 데이터가 보이는 시점입니다.
세 지연은 항상 write_lag ≤ flush_lag ≤ replay_lag 순입니다. "복제가 느려요"라고 할 땐 어느 단계가 느린지부터 pg_stat_replication으로 분해하세요.
-- Primary에서: 스탠바이별 지연을 단계별로 분해
SELECT application_name, state, sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_bytes,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
10.3 구축 레시피 — 3줄 요약
스트리밍 복제 구축은 v12+ 기준으로 매우 단순해졌습니다. Primary에 복제 권한을 열고, pg_basebackup 한 방으로 스탠바이를 만듭니다.
# [Primary] postgresql.conf
wal_level = replica # v10+ 기본값
max_wal_senders = 10 # 스탠바이 수 + pg_basebackup 여유
max_slot_wal_keep_size = 64GB # v13+. 슬롯 폭주 방어 (필수급 권장)
# [Primary] pg_hba.conf — 복제 접속 허용
host replication replicator 10.0.0.0/16 scram-sha-256
# [Standby] 데이터 디렉터리를 통째로 복제 + 자동 설정
pg_basebackup -h primary.example -U replicator \
-D $PGDATA -X stream -R -C -S standby1_slot -P
# -R: standby.signal + primary_conninfo 자동 생성
# -C -S: replication slot 생성 후 연결
슬롯은 "스탠바이가 아직 안 받아간 WAL은 지우지 않겠다"는 약속입니다. 일시적 단절에도 재동기화 없이 catch-up할 수 있어 강력하지만, 스탠바이를 폐기하고 슬롯만 남기면 WAL이 무한 축적되어 디스크가 터집니다. max_slot_wal_keep_size(v13+)로 상한을 꼭 두세요.
10.4 동기 vs 비동기 — 커밋은 누구를 기다리는가
기본 스트리밍은 비동기입니다. Primary는 자기 WAL만 flush하면 커밋을 응답하고, 스탠바이 전송은 뒤따라갑니다. 빠르지만, Primary가 그 직후 죽으면 마지막 몇 개의 커밋이 스탠바이에 없을 수 있습니다. synchronous_standby_names를 설정하면 동기가 되어 스탠바이의 확인까지 기다립니다.
비동기: Primary는 로컬 WAL fsync만 끝나면 즉시 커밋을 응답합니다. 스탠바이 전송은 백그라운드에서 뒤따라갑니다. 빠르지만…
응답 직후 Primary가 죽고 스탠바이로 페일오버하면, 아직 전송되지 않은 마지막 커밋들이 사라질 수 있습니다. "커밋 성공을 본 고객의 주문이 없어지는" 시나리오입니다.
동기: Primary는 WAL을 보내고 스탠바이의 flush 확인(ACK)을 받은 후에야 커밋을 응답합니다. 두 서버의 디스크에 영속됐으므로 데이터 손실이 0(RPO 0)입니다.
대가도 있습니다. sync 스탠바이가 하나뿐인데 그것이 죽으면 Primary의 모든 커밋이 멈춥니다. 그래서 ANY 2 (s1, s2, s3) 같은 쿼럼 구성이 실무 권장입니다.
# postgresql.conf — 쿼럼 기반 동기 복제
synchronous_standby_names = 'ANY 2 (s1, s2, s3)' # 3대 중 아무 2대가 ACK하면 커밋
-- 정말 중요한 트랜잭션만 스탠바이 리플레이까지 기다리기
SET LOCAL synchronous_commit = remote_apply; -- "쓴 직후 스탠바이에서 읽어도 최신" 보장
10.5 페일오버 — 스탠바이가 왕좌에 오르는 순간
계획된 전환은 Switchover, 장애에 의한 전환은 Failover라고 부릅니다. 핵심 절차는 같습니다: 스탠바이를 승격(promote)시키고, 트래픽을 돌리고, 구 Primary가 복귀할 땐 pg_rewind로 새 타임라인에 맞춥니다.
평상시: 앱은 Primary에 쓰고, Primary는 WAL을 스탠바이로 스트리밍합니다. 스탠바이는 읽기 쿼리를 받으며 대기 중입니다.
💥 Primary에 장애가 발생했습니다. 스트리밍이 끊기고, 앱의 쓰기가 실패하기 시작합니다.
스탠바이에서 pg_ctl promote(또는 SELECT pg_promote())를 실행하면 리커버리를 끝내고 쓰기 가능한 새 Primary로 승격됩니다.
DNS/VIP를 전환해 트래픽을 새 Primary로 돌립니다. 이때 WAL 역사는 새 타임라인(Timeline 2)으로 갈라집니다 — 페일오버 이후의 역사를 이전과 구분하는 장치입니다.
구 Primary가 살아나도 그냥 켜면 안 됩니다(두 개의 Primary = Split-brain). pg_rewind로 Timeline 2에 맞게 되감은 뒤 스탠바이로 재합류시킵니다. 실무에선 이 전 과정을 Patroni 같은 도구로 자동화합니다.
운영에서는 Patroni(etcd 기반 리더 선출, 가장 널리 사용), repmgr, pg_auto_failover 같은 자동화 도구를 씁니다. 공통 요구사항은 Split-brain 방지, 타임라인 관리, DNS/VIP 전환입니다. 관리형 서비스(RDS 등)는 플랫폼이 대신해 줍니다.
10.6 Hot Standby의 딜레마 — 읽기 쿼리 vs 리플레이
스탠바이에서 읽기 쿼리를 받다 보면 충돌이 생깁니다. Primary에서 VACUUM으로 지워진 행을, 스탠바이의 장기 쿼리가 아직 보고 있다면? WAL 리플레이를 기다리게 하거나, 쿼리를 취소하거나 — 둘 중 하나입니다.
| 설정 | 효과 | 부작용 |
|---|---|---|
max_standby_streaming_delay = 30s | 리플레이를 늦춰 쿼리에 양보 | 복제 지연 누적 |
hot_standby_feedback = on | 스탠바이의 oldest xmin을 Primary에 전달 → 필요한 튜플의 vacuum 보류 | Primary에 Bloat 누적 (8장) |
정답은 워크로드에 따라 다릅니다: 분석 쿼리용 스탠바이는 feedback on + 긴 delay, 실시간 HA용 스탠바이는 그 반대로 설정합니다.
✍️ 이해도 체크
synchronous_standby_names)가 필요하고, 대신 커밋 지연과 "sync 스탠바이 장애 시 커밋 정지" 리스크를 감수해야 합니다. 쿼럼(ANY N)이 균형점입니다.pg_drop_replication_slot()으로 즉시 제거하고, v13+에서는 max_slot_wal_keep_size로 상한을 걸어 Primary를 보호하세요.이 장의 원문 문서: chapters/ch10_replication.md — Logical Replication(PUBLICATION/SUBSCRIPTION) 상세, Cascading 복제, 읽기 분산 패턴, 진단·운영 레시피가 담겨 있습니다.