TT Lab

한국어

시작하기
블로그

블로그

PostgreSQL 복제 완전 가이드 2025: Streaming Replication, Logical Decoding, Failover, Patroni 심층 분석

대화로 듣기 · 10분두 사람이 이 글을 풀어 줍니다

들어가며: 단일 DB의 위험

한 번쯤은 겪어봤을 상황

자정 새벽 2시. 알림이 울린다.

"Production DB 다운."

당신의 PostgreSQL 서버 하나가 멈췄다. 모든 서비스가 죽었다. 원인은:

복제 없는 DB는 single point of failure. 한 번의 사고가 전체 서비스를 마비시킨다.

복제가 주는 것

PostgreSQL Replication으로 얻는 것:

  1. High Availability: Primary 다운 → Replica가 승격 → 서비스 지속.
  2. Read Scaling: Read 쿼리를 여러 replica에 분산.
  3. Backup Source: Replica에서 백업 → primary 부담 없음.
  4. Geographic Distribution: 여러 지역에 데이터.
  5. Zero-Downtime Upgrade: Replica 업그레이드 후 failover.
  6. CDC (Change Data Capture): 다른 시스템으로 데이터 동기화.

이 모든 것이 WAL이라는 하나의 기반 위에 있다.

이 글에서 다룰 것

  1. WAL (Write-Ahead Log) 복습: 복제의 기반.
  2. Physical Streaming Replication: 바이트 단위 복제.
  3. Synchronous vs Asynchronous: 성능과 안정성.
  4. Hot Standby: Read replica.
  5. Replication Slots: 안전한 WAL 유지.
  6. Logical Decoding & Replication: Logical 수준.
  7. pglogical, Debezium: 실전 도구.
  8. Failover 자동화: Patroni, repmgr.
  9. PgBouncer: 연결 풀링.
  10. 실전 운영 가이드.

1. WAL: 복제의 기반

WAL 복습

(WAL 가이드의 핵심)

Write-Ahead Log: 데이터베이스의 모든 변경을 먼저 log에 기록. 이후 실제 data file에 반영.

Transaction
    ↓
WAL record (먼저!)
    ↓ (fsync로 디스크에)
데이터 파일 업데이트

이점:

WAL의 구조

PostgreSQL의 WAL:

$PGDATA/pg_wal/
├── 000000010000000100000001  (16MB)
├── 000000010000000100000002
├── 000000010000000100000003
└── archive_status/

파일 이름: <TimelineID><LogID><Segment>.

Replication의 본질

PostgreSQL 복제 = WAL 전송 + 재생:

Primary                 Standby
───────                 ───────
 Commit
  ↓
 WAL write              
  ↓
 → → Network → → →      WAL receive
                         ↓
                        WAL replay
                         ↓
                        Same state

모든 복제 방식이 이 기본 원리 위에 있다. 차이는 언제, 어떻게 전송하느냐.


2. Physical Streaming Replication

개요

PostgreSQL 9.0부터 표준이 된 streaming replication:

"Physical"의 의미: 바이트 단위 WAL. 같은 PostgreSQL 버전, 같은 플랫폼 간만 복제.

구조

┌──────────────┐              ┌──────────────┐
│   Primary    │              │   Standby    │
│              │              │              │
│ walsender ───┼──WAL stream─→│ walreceiver  │
│              │              │      ↓       │
│              │              │  startup     │
│              │              │   (replay)   │
└──────────────┘              └──────────────┘

Primary 측:

Standby 측:

설정: Primary

postgresql.conf:

# WAL 수준
wal_level = replica                # physical replication용

# 최대 WAL 송신자 수
max_wal_senders = 10

# 복제용 연결 허용
max_replication_slots = 10

# WAL archiving (권장)
archive_mode = on
archive_command = 'cp %p /archive/%f'

# 복제 대역폭 (선택)
wal_compression = on               # WAL 압축

pg_hba.conf:

host replication repluser 10.0.0.0/8 md5

Base Backup

Standby는 primary의 복사본으로 시작:

pg_basebackup \
  -h primary.example.com \
  -D /var/lib/postgresql/standby \
  -U repluser \
  -P -R \
  --wal-method=stream

옵션:

Standby 시작

standby.signal 파일이 있으면 PostgreSQL은 standby 모드로 시작.

postgresql.auto.conf:

primary_conninfo = 'host=primary.example.com user=repluser password=...'
primary_slot_name = 'standby1_slot'

이제 standby 시작:

systemctl start postgresql

Primary의 WAL을 스트리밍으로 수신 + 재생. Primary와 같은 상태 유지.

Replication 확인

Primary에서:

SELECT * FROM pg_stat_replication;
 pid  | usename  | state     | sync_state | replay_lag
------+----------+-----------+------------+------------
 1234 | repluser | streaming | async      | 00:00:00.123

Standby에서:

SELECT pg_is_in_recovery();
-- t (true) → standby 모드

3. Synchronous vs Asynchronous

Asynchronous Replication (기본)

Primary가 WAL을 commit 즉시 반환. Standby는 비동기로 따라잡음.

Client: COMMIT
  ↓
Primary: WAL write → fsync → "COMMITTED"
  ↓ (비동기)
Standby: WAL 수신 → 재생

장점:

단점:

Synchronous Replication

Primary가 WAL이 standby까지 전달된 후에 commit:

Client: COMMIT
  ↓
Primary: WAL write
  ↓
Primary → Standby: WAL 전송
Standby: 수신 + 확인
  ↓
Primary: "COMMITTED" 반환

설정:

synchronous_commit = remote_write  # 또는 remote_apply
synchronous_standby_names = 'standby1'

synchronous_commit 레벨:

레벨별 보장:

레벨Primary 손실Standby 일관성Latency
off~ 1초없음가장 빠름
local없음없음빠름
remote_write없음Primary crash 시 약간의 지연중간
on없음디스크에 안정느림
remote_apply없음읽기 일관성가장 느림

성능 영향

실측 (100 ms RTT로 remote standby):

설정TPS
async10,000
remote_write5,000
on2,000
remote_apply1,500

동기 복제는 네트워크 RTT에 민감. 같은 DC 내에서만 실용적.

다중 Standby

Priority-based:

synchronous_standby_names = 'FIRST 2 (standby1, standby2, standby3)'

먼저 두 standby의 확인을 기다림.

Quorum-based:

synchronous_standby_names = 'ANY 2 (standby1, standby2, standby3)'

아무 두 standby의 확인이면 OK. 더 탄력적.

실전 선택

Async:

Sync (same DC):

Mixed:


4. Hot Standby: Read Replicas

Hot vs Warm Standby

Warm standby: Recovery 모드, 쿼리 불가. 단지 대기.

Hot standby: Recovery 중에도 읽기 쿼리 허용. PostgreSQL 9.0+ 기본.

설정

Standby의 postgresql.conf:

hot_standby = on                    # 기본 on
hot_standby_feedback = on           # Primary에 피드백
max_standby_archive_delay = 30s     # conflict 허용 시간
max_standby_streaming_delay = 30s

Read-Only 쿼리

Standby에서:

-- 가능
SELECT * FROM users WHERE id = 1;

-- 불가 (에러)
INSERT INTO users VALUES (...);
UPDATE users SET ...;

에러 메시지:

ERROR: cannot execute INSERT in a read-only transaction

Replication Conflicts

문제: Standby가 primary의 WAL을 재생 중인데, 현재 standby에서 실행 중인 쿼리가 그 변경사항과 충돌.

예시:

  1. Standby에서 SELECT * FROM users (long query).
  2. Primary에서 VACUUM users → 사용되지 않는 row 제거.
  3. Standby가 이 VACUUM WAL 재생 → long query의 snapshot 깨짐.
  4. 쿼리 취소 (query conflict).

해결 옵션:

1. max_standby_streaming_delay:

max_standby_streaming_delay = 60s

Conflict 발생 시 최대 60초 대기 후 쿼리 취소. Replication lag 증가.

2. hot_standby_feedback:

hot_standby_feedback = on

Standby가 primary에 사용 중인 snapshot 알림. Primary가 해당 row의 vacuum을 지연.

장점: Conflict 없음. 단점: Primary에 bloat 증가 가능.

3. replication slot with status update: 비슷한 효과.

Read Scaling

여러 Standby로 읽기 분산:

                Primary (write)
                   ↓↓↓ WAL
        ┌──────────┼──────────┐
        ↓          ↓          ↓
    Standby1   Standby2   Standby3
   (read)      (read)     (read)

Load balancer로 읽기 분산:

주의:

Causal Reads

동기 복제 + synchronous_commit=remote_apply:

또는 애플리케이션 수준:


5. Replication Slots

문제: WAL 정리

Primary가 WAL을 계속 생성. Disk 공간을 위해 오래된 WAL은 삭제.

시나리오:

  1. Standby가 네트워크 단절로 한참 사라짐.
  2. Primary가 wal_keep_size 이상 WAL 생성.
  3. Primary가 오래된 WAL 삭제.
  4. Standby 복귀.
  5. 필요한 WAL이 없음 → 복제 깨짐.
  6. pg_basebackup으로 처음부터 다시 해야.

Replication Slot

Replication slot은 primary에게 "이 WAL을 standby가 확인할 때까지 보관하라"고 알림.

생성:

SELECT pg_create_physical_replication_slot('standby1_slot');

Standby 설정:

primary_slot_name = 'standby1_slot'

작동:

위험

Replication slot의 함정:

방어:

max_slot_wal_keep_size = 10GB   # PostgreSQL 13+

Slot 때문에 유지해야 할 WAL이 10GB 넘으면 slot을 강제 무효화. 복제는 깨지지만 primary는 살아남음.

Slot 관리

모니터링:

SELECT slot_name, active, confirmed_flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS retained_wal
FROM pg_replication_slots;

제거 (사용 안 하는 slot):

SELECT pg_drop_replication_slot('old_slot');

주의: Active slot은 drop 불가. Standby 먼저 분리.


6. Logical Decoding & Replication

Physical vs Logical

Physical replication:

Logical replication:

Logical Decoding

Logical decoding은 WAL을 논리적 변경 스트림으로 변환:

Physical WAL:
  XLOG_INSERT: relid=16384, tuple=...
  XLOG_HEAP_UPDATE: ...
  
Logical stream:
  BEGIN;
  INSERT INTO users (id, name) VALUES (1, 'Alice');
  COMMIT;

설정

Primary:

wal_level = logical               # physical보다 더 많은 정보

Output Plugins

Logical decoding은 플러그인 기반. WAL을 어떤 포맷으로 출력할지 결정:

pgoutput (기본):

wal2json:

decoderbufs:

test_decoding:

Native Logical Replication

PostgreSQL 10+ 기본 기능:

Publisher (publisher):

CREATE PUBLICATION mypub FOR TABLE users, orders;

Subscriber:

CREATE SUBSCRIPTION mysub
  CONNECTION 'host=primary.example.com dbname=mydb user=repluser'
  PUBLICATION mypub;

작동:

특징

선택적 복제:

-- 특정 테이블만
CREATE PUBLICATION mypub FOR TABLE users;

-- 모든 테이블
CREATE PUBLICATION mypub FOR ALL TABLES;

-- 일부 연산만
CREATE PUBLICATION mypub FOR TABLE users WITH (publish = 'insert,update');

크로스 버전:

Write on Subscriber:

Limitations

Physical replication과 달리:

  1. Sequence는 복제 안 됨: 별도 처리.
  2. TRUNCATE는 복제 안 됨 (기본).
  3. Large object (LOB)는 복제 안 됨.
  4. DDL은 복제 안 됨: CREATE TABLE 등.
  5. Materialized view refresh는 복제 안 됨.

이런 제약 때문에 Full DR (disaster recovery)에는 부적절. Physical replication이 더 적합.

용도

Logical replication이 적합한 경우:

  1. CDC (Change Data Capture): Kafka로 변경 이벤트 전송.
  2. Cross-version migration: PG 14 → PG 16.
  3. Schema selective replication.
  4. Multi-master (양방향): 복잡하지만 가능.
  5. Data warehouse 동기화: PG → BigQuery 등.

Physical이 적합한 경우:

  1. HA failover.
  2. Read replica.
  3. 완전 백업.
  4. 정확한 복제본 필요.

7. Debezium과 CDC

Debezium 소개

Debezium은 CDC의 표준 도구. PostgreSQL, MySQL, MongoDB, Oracle 등을 지원. Kafka Connect 기반.

아키텍처

PostgreSQL
    ↓ (logical decoding)
Debezium (PG connector)
    ↓ (Kafka Connect)
Apache Kafka
    ↓ (consumers)
Elasticsearch, Snowflake, 다른 DB, ...

흐름:

  1. Debezium이 PostgreSQL에 logical replication slot 생성.
  2. WAL을 logical 형태로 받음.
  3. 각 변경을 Kafka 메시지로 발행.
  4. 다운스트림 시스템이 consume.

설정

PostgreSQL:

wal_level = logical
max_replication_slots = 10
max_wal_senders = 10

Debezium connector:

{
  "name": "my-pg-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "pg.example.com",
    "database.user": "debezium",
    "database.password": "secret",
    "database.dbname": "mydb",
    "database.server.name": "my-pg",
    "plugin.name": "pgoutput",
    "table.include.list": "public.users,public.orders",
    "slot.name": "debezium_slot",
    "publication.name": "debezium_pub"
  }
}

이벤트 포맷

Debezium의 변경 이벤트 (JSON):

{
  "before": null,
  "after": {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com"
  },
  "source": {
    "version": "2.5.0.Final",
    "connector": "postgresql",
    "name": "my-pg",
    "ts_ms": 1704153600000,
    "snapshot": "false",
    "db": "mydb",
    "schema": "public",
    "table": "users",
    "txId": 12345,
    "lsn": 123456789
  },
  "op": "c",
  "ts_ms": 1704153600123
}

op:

활용

1. Microservice 간 데이터 동기화:

2. 검색 인덱스 업데이트:

3. 데이터 웨어하우스:

4. Cache invalidation:

5. Audit logging:

주의사항

Schema evolution: 테이블 구조 변경 시 consumer가 적응해야.

Order guarantees:

Exactly-once vs At-least-once: 보통 at-least-once, 응용 계층에서 idempotent 처리.

WAL retention: Debezium이 뒤처지면 WAL 쌓임.


8. Failover Automation

Manual Failover

단계:

  1. Primary 죽음을 감지.
  2. Standby를 primary로 승격:
SELECT pg_promote();
  1. Application의 connection string 변경.
  2. 다른 standby들의 primary 변경.

문제: 사람이 해야. 수 분~수십 분 다운타임.

Patroni

Patroni: PostgreSQL HA의 사실상 표준. Zalando가 개발.

아키텍처:

          DCS (Distributed Configuration Store)
           (etcd, Consul, ZooKeeper)
                     ↑ ↓
        ┌────────────┼────────────┐
        ↓            ↓            ↓
   Patroni       Patroni      Patroni
   + PG 1       + PG 2        + PG 3
  (leader)   (replica)     (replica)

작동:

Patroni Configuration

scope: my-pg-cluster
namespace: /db/
name: node1

restapi:
  listen: 0.0.0.0:8008

etcd:
  hosts: etcd1:2379,etcd2:2379,etcd3:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 100
        shared_buffers: 4GB
  
  initdb:
    - encoding: UTF8
    - data-checksums

postgresql:
  listen: 0.0.0.0:5432
  data_dir: /var/lib/postgresql/data
  authentication:
    replication:
      username: repluser
      password: secret
    superuser:
      username: postgres
      password: secret

Failover 시나리오

Primary가 죽으면:

  1. 다른 노드들이 primary의 healthcheck 실패를 감지 (~10초).
  2. Standby 중 가장 최신 것 선택 (lag 기준).
  3. 해당 standby가 primary로 승격 (pg_promote).
  4. 다른 standby들이 새 primary를 가리키도록 재설정.
  5. Virtual IP 또는 DNS 업데이트.

보통 30초~2분 내 완료.

Split-Brain 방지

문제: 네트워크 파티션으로 두 개의 primary가 동시에 존재할 수 있음.

Patroni의 해결:

pg_rewind

Old primary를 new primary의 standby로 다시 추가:

pg_rewind --target-pgdata=/var/lib/postgresql/data \
          --source-server='host=new-primary user=repluser'

WAL이 분기된 이후만 재동기화. 전체 pg_basebackup보다 빠름.

Patroni가 자동으로 처리.

대안 도구

repmgr: Patroni 전에 널리 쓰였음. 여전히 사용.

pg_auto_failover: Citus Data (현재 Microsoft) 개발. 단순함.

Stolon: Sorint.lab 개발. Kubernetes 친화적.

CloudNativePG: Kubernetes operator. 현대적.

선택:


9. PgBouncer: 연결 풀링

왜 필요한가

PostgreSQL의 연결은 비싸다:

문제: 수천 개의 웹 서버 → 수천 개 연결 → 메모리 부족, 성능 저하.

PostgreSQL의 권장 연결 수: 일반적으로 CPU 수의 2~4배.

해결: Connection pooling.

PgBouncer

PgBouncer는 가장 널리 쓰이는 PostgreSQL 연결 풀러. 가볍고 빠름.

아키텍처:

[App1] [App2] [App3] [App4] ... [App100]
   ↓      ↓      ↓      ↓          ↓
   connections (to PgBouncer)
              ↓
         PgBouncer
              ↓
   few connections (to PG)
              ↓
         PostgreSQL

수백 개의 App 연결 → 수십 개의 PG 연결.

Pool Modes

Session pooling:

Transaction pooling:

Statement pooling:

실전 선택: Transaction pooling이 표준.

설정

pgbouncer.ini:

[databases]
mydb = host=pg.example.com port=5432 dbname=mydb

[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 5

10,000 client 연결 → 20 PG 연결.

통계

psql -h localhost -p 6432 -d pgbouncer
SHOW POOLS;
SHOW CLIENTS;
SHOW SERVERS;
SHOW STATS;

각 풀의 사용률, 대기 시간 등.

대안

pgcat: Rust로 작성. 멀티 primary, 부하 분산.

odyssey: Yandex 개발. PgBouncer보다 더 많은 기능.

application-level pooling: HikariCP (Java), node-pg-pool 등. 단일 프로세스 내.

실전 아키텍처

Client → PgBouncer (session pool) → PgBouncer (transaction pool) → PostgreSQL

두 단계의 pooling으로 세션 기능과 효율을 모두 확보.


10. 실전 운영

모니터링

중요 메트릭:

  1. Replication lag:
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) 
FROM pg_stat_replication;
  1. Checkpoint 활동:
SELECT * FROM pg_stat_bgwriter;
  1. Cache hit ratio:
SELECT sum(blks_hit) / (sum(blks_hit) + sum(blks_read))::float 
FROM pg_stat_database;
  1. Long-running queries:
SELECT pid, state, query_start, query 
FROM pg_stat_activity 
WHERE state != 'idle' 
ORDER BY query_start;
  1. Connections:
SELECT count(*), state FROM pg_stat_activity GROUP BY state;

Tools:

Backup 전략

1. pg_basebackup:

2. WAL archiving:

3. Logical backup (pg_dump):

4. PITR (Point-In-Time Recovery):

# Base backup
pg_basebackup -D /backup/base -P

# WAL archive
archive_command = 'cp %p /backup/wal/%f'

# 복구 시
restore_command = 'cp /backup/wal/%f %p'
recovery_target_time = '2025-04-15 10:00:00'

5. Streaming replica as backup:

Upgrade 전략

Major upgrade (PG 15 → 16):

1. pg_upgrade:

2. Logical replication:

3. Blue-green:

장애 대응

증상: Primary 다운

  1. Patroni가 자동 failover (~30초).
  2. Application reconnect (connection pool 재설정).
  3. 모니터링 알림 확인.
  4. 원인 분석.

증상: Replication lag 급증

  1. Standby 부하 확인.
  2. Network latency 체크.
  3. Primary의 write 스파이크?
  4. hot_standby_feedback 영향?

해결:

증상: Connection 한계

FATAL: sorry, too many clients already
  1. pg_stat_activity 확인.
  2. Idle connection 많으면 pool 조정.
  3. max_connections 증가.
  4. PgBouncer 도입.

Performance Tuning

shared_buffers: RAM의 25%.

effective_cache_size: RAM의 50-75%.

work_mem: 쿼리당 메모리. 너무 크면 OOM.

maintenance_work_mem: VACUUM, INDEX 용.

wal_compression: 대역폭 절약.

checkpoint_timeout: 기본 5분, 15분으로 증가 가능.

일반적 실수

1. synchronous_standby_names 설정 후 standby 제거 없이 primary 사용 중지:

2. Replication slot 방치:

3. Connection 누수:

4. Vacuum 방치:

5. Logical replication으로 DR 구축:


퀴즈로 복습하기

Q1. Physical replication과 logical replication의 핵심 차이는?

A.

두 방식은 서로 다른 수준에서 동작하며 매우 다른 용도를 가진다.

Physical Streaming Replication:

"WAL을 바이트 단위로 복제".

  • 무엇이 복제되는가: 모든 파일시스템 변경, block 수준.
  • 정확도: Primary와 bit-perfect 동일한 복제본.
  • 대상: 전체 데이터베이스 클러스터 (모든 DB, 모든 테이블).
  • 버전 제약: 같은 PostgreSQL 버전, 같은 플랫폼.
  • WAL level: replica 이상.

작동:

Primary WAL record:
  XLOG_HEAP_INSERT rel=16384 blkno=123 offset=5 data=...

Standby replays exactly:
  rel=16384 blkno=123 offset=5 → write data=...

특징:

  • 속도: 매우 빠름. 단순 byte copy + replay.
  • Overhead: 낮음.
  • DDL 자동 복제: CREATE TABLE 포함.
  • 모든 것이 복제: sequences, temp tables metadata, 전부.

용도:

  • High Availability (HA): Primary 장애 시 failover.
  • Read replicas: 읽기 부하 분산.
  • Disaster recovery.
  • Backup source.

Logical Replication:

"WAL을 SQL 수준 변경으로 디코딩 후 복제".

  • 무엇이 복제되는가: Row 수준 변경 (INSERT/UPDATE/DELETE).
  • 정확도: 논리적으로 동일, 물리적으론 다름.
  • 대상: 선택적. 특정 DB, 테이블, 또는 column.
  • 버전 제약: 다른 버전 간 가능 (PG 14 → PG 16).
  • WAL level: logical.

작동:

Primary WAL (logical):
  BEGIN;
  INSERT INTO users (id, name) VALUES (1, 'Alice');
  UPDATE users SET email='alice@new.com' WHERE id=1;
  COMMIT;

Subscriber applies:
  실제 SQL 실행.

특징:

  • 속도: Physical보다 느림. SQL 실행 필요.
  • Overhead: 더 높음. Decoding + 재실행.
  • DDL 복제 안 됨: CREATE TABLE 수동으로.
  • 선택적: 특정 테이블만 복제.

용도:

  • CDC (Change Data Capture): Kafka, Debezium.
  • Cross-version migration: PG upgrade.
  • Selective replication: 일부 테이블만.
  • Heterogeneous replication: PG → MySQL, 등.

구체적 비교:

항목PhysicalLogical
정확도Byte-perfectRow-level
버전동일 필수다른 버전 가능
플랫폼동일 필수다른 OS 가능
선택적불가가능
DDL자동수동
Sequences복제복제 안 됨
Large Objects복제복제 안 됨
성능빠름느림
용도HA, DRCDC, migration

Pros & Cons:

Physical Pros:

  • ✅ 완전 복제 (DDL, sequence 등 모두).
  • ✅ 높은 성능.
  • ✅ 단순한 설정.
  • ✅ 검증된 기술 (2010년부터).

Physical Cons:

  • ❌ 버전 강제.
  • ❌ All-or-nothing (전체 DB).
  • ❌ Cross-DB 불가.

Logical Pros:

  • ✅ 선택적.
  • ✅ Cross-version.
  • ✅ Subscriber write 가능.
  • ✅ CDC 이벤트로 활용.

Logical Cons:

  • ❌ DDL 수동.
  • ❌ 더 느림.
  • ❌ 제약 많음 (sequence, LOB).
  • ❌ 설정 복잡.
  • ❌ Schema 변경 시 주의.

실전 선택 가이드:

Physical:

  • HA 클러스터 (Patroni + streaming replication).
  • Read replica for BI.
  • DR site.
  • 백업 소스.
  • 간단한 설정 원할 때.

Logical:

  • Microservice 간 데이터 동기화.
  • 데이터 웨어하우스 수집 (ETL).
  • PG major version upgrade (zero-downtime).
  • 특정 테이블만 복제.
  • Multi-master (복잡하지만 가능).

Combined:

대부분의 production 환경은 둘 다 사용:

                Primary
                   ↓
      ┌────────────┼────────────┐
      ↓            ↓            ↓
  Physical     Physical     Logical
  Standby1     Standby2    Subscriber
  (HA)         (Read)      (→ Kafka)
  • Physical로 HA + read scaling.
  • Logical로 외부 시스템 피드.

예시 아키텍처:

E-commerce:

  • Physical replication: 2 standbys (1 sync + 1 async).
  • Logical: orders 테이블 → Kafka → 데이터 웨어하우스.
  • Logical: users 테이블 → Elasticsearch.

SaaS Multi-tenant:

  • Physical: HA + DR.
  • Logical: 특정 tenant schema를 전용 DB로 이동.

미래:

PostgreSQL 16+에서는 logical replication이 크게 개선:

  • Parallel apply: 복수 worker.
  • Binary mode: 빠른 전송.
  • Failover 개선.
  • Sequences 복제 (PG 16+).

여전히 physical이 HA의 기본이지만, logical이 점점 더 능력있어지고 있다.

교훈:

"Replication의 종류가 아닌, 해결하려는 문제" 가 선택을 결정한다.

  • "정확히 같은 복사본": Physical.
  • "변경 이벤트 스트림": Logical.
  • "둘 다": 조합 사용.

이 구분을 이해하면 올바른 도구를 선택할 수 있다. 많은 엔지니어가 "logical이 새것이니 항상 좋다"고 오해하지만, physical이 여전히 대부분의 HA 시나리오에 최선이다.

두 도구가 모두 있어서 행운이다. 상황에 맞게 쓰는 것이 PostgreSQL의 유연성이다.

Q2. Synchronous replication을 cross-region에 쓰면 안 되는 이유는?

A.

Network latency의 물리적 한계 때문이다.

Synchronous replication의 작동:

Client가 COMMIT 실행하면:

  1. Primary가 local WAL write.
  2. Primary가 standby에게 WAL 전송.
  3. Standby가 WAL 수신 (또는 write, fsync, replay — 레벨에 따라).
  4. Standby가 ACK 반환.
  5. Primary가 ACK 받을 때까지 대기.
  6. Primary가 client에게 COMMITTED 반환.

핵심: Commit마다 최소 한 번의 네트워크 왕복이 필요. 즉 RTT (Round-Trip Time) 만큼 추가 latency.

지리적 거리별 RTT:

거리대략 RTT예시
같은 rack0.1 ms같은 switch
같은 DC0.5-1 ms같은 건물
같은 metro1-5 ms서울 ↔ 경기
같은 나라10-50 ms서울 ↔ 부산
cross-continent100-200 ms서울 ↔ 프랑크푸르트
지구 반대편200-300 ms서울 ↔ 상파울루

물리적 최소 RTT:

  • 빛의 속도 (~200,000 km/s in fiber).
  • 서울 ↔ 뉴욕 = 11,000 km.
  • 왕복 = 22,000 km.
  • 최소 RTT = 110 ms (이론상).
  • 실제로는 라우팅, 혼잡 등으로 150-200 ms.

영향 계산:

Sync replication 적용:

Same DC (1 ms RTT):

  • Normal commit: ~1 ms (local fsync).
  • Sync commit: 1 ms + 1 ms RTT = 2 ms.
  • 2배 느림. 허용 가능.

Cross-region (150 ms RTT):

  • Normal commit: ~1 ms.
  • Sync commit: 1 ms + 150 ms RTT = 151 ms.
  • 150배 느림. 재앙.

TPS 계산:

Single-threaded workload:

Local:

  • Commit당 2 ms.
  • TPS = 1000 / 2 = 500.

Cross-region:

  • Commit당 151 ms.
  • TPS = 1000 / 151 = 6.6.

75배 TPS 감소. 일반 OLTP는 사용 불가.

병렬 트랜잭션:

Concurrent 워크로드:

Local (100 connections):

  • 이론상 TPS = 100 × 500 = 50,000.

Cross-region (100 connections):

  • 이론상 TPS = 100 × 6.6 = 660.
  • 하지만 connection 수보다 network pipeline이 병목.
  • 실측: 수백 ~ 수천 TPS.

대규모 OLTP에는 완전히 부적합.

실제 측정 예시:

간단한 벤치마크 (pgbench):

설정TPS
No replication10,000
Sync, same DC8,000
Sync, cross-DC (같은 region)3,000
Sync, cross-region (100 ms)500
Sync, cross-region (200 ms)200

cross-region sync는 TPS를 50배 이상 감소.

User experience 영향:

Local: 사용자가 "Save" 클릭 → 즉시 응답.

Cross-region sync:

  • 150 ms 대기 (최소).
  • 추가 네트워크 hop.
  • 200-300 ms 전체 응답 시간.
  • 사용자가 느림을 인식.

연쇄 트랜잭션 (여러 API 호출)이면:

  • 10 API × 300 ms = 3 초.
  • 허용 불가.

대안들:

1. Async replication:

  • Primary는 즉시 commit.
  • Standby는 뒤따라감.
  • Data loss risk: Primary 장애 시 unreplicated 변경 손실.
  • 대부분의 cross-region 시나리오에 적합.

2. Same-region sync + cross-region async:

Primary (Seoul)
  ├─ Sync Standby (Seoul DC2)
  └─ Async Standby (Frankfurt)
  • Local: sync (durability).
  • Cross-region: async (performance).
  • Best of both worlds.

3. Semi-synchronous (quorum-based):

synchronous_standby_names = 'ANY 1 (standby1, standby2, standby3)'

가까운 것 하나만 sync면 OK. 다른 것들은 optional.

4. Logical replication for cross-region:

  • Async 기본.
  • 일부 데이터만 선택적.
  • Cross-region에 더 적합.

5. Multi-master (조심):

  • 각 region에 write 가능.
  • 복잡, conflict 관리 필요.
  • BDR, pglogical 등 상용 솔루션.
  • 대부분 시나리오에 오버킬.

6. CDN / Edge caching:

  • Read를 edge에서.
  • Write는 여전히 central.

7. Event-driven architecture:

  • Sync replication 없이.
  • Events로 eventual consistency.

언제 cross-region sync가 필요한가:

거의 없다. 예외:

  • 금융 거래: 돈 이동의 강한 일관성.
  • 법적 요구: 두 장소에 동시 기록.
  • Latency가 덜 중요: 배치 작업.

이 경우에도:

  • Network 품질 투자 (전용선, Direct Connect).
  • Application 차원 최적화 (batching).
  • 사용자 기대 관리 (느림 감수).

PostgreSQL의 기본 권장:

공식 문서는 다음을 권장:

  • Sync: same DC 또는 저지연 링크.
  • Async: cross-region.
  • 혼합: 중요한 데이터는 sync.

CAP 정리 관점:

  • Sync across continents: Consistency + Partition tolerance, but not Availability (WAN 지연으로).
  • Async: Availability + Partition tolerance, eventual consistency.

분산 시스템의 근본 법칙. 물리적 거리는 속도 한계를 만든다.

특수 기술:

TrueTime (Google Spanner):

  • 원자 시계 + GPS로 global consistency.
  • Sync replication보다 영리.
  • 비쌈: 전용 하드웨어.
  • PostgreSQL에는 없음.

Cockroach, YugabyteDB:

  • PostgreSQL-compatible, 분산 기본.
  • Raft/Paxos 기반.
  • Cross-region에 최적화.
  • 단, 기능 제약.

교훈:

"강한 일관성의 비용은 네트워크 latency".

지리적으로 분산된 서비스에서:

  • 모든 것을 sync로 보호하려 하면 사용 불가.
  • Async + smart design이 현실적 답.
  • 일관성이 정말 필요한 곳만 sync.

"같은 region에 여러 AZ" 가 sweet spot:

  • RTT ~1 ms.
  • 전용 백본 (AWS Direct Connect 등).
  • AZ 간 sync 복제.
  • Cross-region async 복제.

PostgreSQL의 replication이 유연한 것은 이 선택을 개발자에게 맡기기 때문. 하지만 물리학은 맡기지 않는다. Cross-region sync는 대부분의 경우 잘못된 선택이다.

이를 이해하면 분산 데이터베이스 설계의 핵심을 아는 것이다.


마치며: 복제의 예술

핵심 정리

  1. WAL: 복제의 기반. 모든 방식의 공통점.
  2. Physical streaming: HA와 read scaling의 표준.
  3. Sync vs Async: Latency와 durability의 trade-off.
  4. Hot standby: Read replica.
  5. Replication slots: Safe WAL retention (with risks).
  6. Logical replication: Selective, cross-version, CDC.
  7. Patroni: 자동 failover의 표준.
  8. PgBouncer: Connection 관리.

실전 아키텍처

Production PostgreSQL stack:

                 Application
                      ↓
                  PgBouncer (pooling)
                      ↓
               ┌──────┴──────┐
               ↓             ↓
           Primary      ← Patroni → Standby
              │ (sync replication)
              │
              ├─→ Async standby (read)
              ├─→ Async standby (DR, cross-region)
              └─→ Logical replication (→ Kafka, DW)
                      ↓
                  Monitoring
                  (Prometheus + Grafana)
                      ↓
                  Backup
                  (pg_basebackup + WAL archive)

운영 원칙

  1. 모니터링 먼저: Replication lag, connections, slow queries.
  2. 자동 failover: 사람 개입 최소화.
  3. 정기 backup: WAL archive + base backup.
  4. Capacity planning: Disk, memory, connections.
  5. Upgrade 테스트: Staging 환경 필수.
  6. 장애 대응 리허설: 연습 안 하면 실전에서 망함.

마지막 교훈

PostgreSQL은 가장 완성된 오픈소스 DB 중 하나다. 복제 기능도 수십 년간의 진화 결과다:

현재 PostgreSQL은 Teradata, Oracle 같은 상용 DB와 견줄 만한 복제 기능을 가진다.

이 글의 지식은 production DB 운영자에게 필수다:

이 모든 질문의 답이 WAL, replication, connection pool의 작동 원리에 있다.

당신의 PostgreSQL이 프로덕션에서 돌아간다면, 이 지식이 당신의 서비스를 지킨다. 새벽 2시 알림이 울렸을 때, 당황하지 않고 차분히 진단하고 대응할 수 있는 힘은 이해에서 온다.

PostgreSQL은 좋은 도구다. 하지만 좋은 엔지니어가 좋은 운영을 만든다. 이 글이 그 시작이 되기를.


참고 자료

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다