PostgreSQL 논리적 복제 구현: 멀티 마스터 및 선택적 데이터 동기화
선택적 동기화, 업그레이드, 보고용 복사본 및 신중하게 범위가 지정된 양방향 설계를 위한 PostgreSQL 논리적 복제 설정
PostgreSQL에서 논리적 복제 구현: 멀티 마스터 및 선택적 데이터 동기화
PostgreSQL의 논리적 복제는 전체 클러스터 대신 선택된 테이블을 복사해야 할 때 유용합니다. 보고 데이터베이스, 버전 업그레이드, 지역별 읽기 복사본 및 선택적 데이터 동기화에 유용하지만, 네이티브 PostgreSQL 논리적 복제는 즉시 사용 가능한 충돌 없는 멀티 마스터 시스템이 아닙니다.
논리적 복제 vs 물리적 복제
물리적 스트리밍 복제
- 전체 데이터베이스 클러스터 복제
- 바이너리 수준 복제
- 읽기 전용 복제본
- 동일한 PostgreSQL 버전 필요
- 낮은 오버헤드
논리적 복제
- 선택적 테이블/행 복제
- 크로스 버전 호환
- 쓰기 가능한 구독자
- 높은 오버헤드
- 유연한 데이터 분산
논리적 복제 사용 사례
- 선택적 데이터 분산: 특정 테이블을 다른 지역으로 복제
- 양방향 동기화 실험: 일반적으로 애플리케이션 수준 충돌 규칙을 사용하여 데이터베이스 간 신중하게 범위가 지정된 쓰기
- 크로스 버전 업그레이드: 이전 PostgreSQL 버전에서 새 버전으로 복제
- 데이터 집계: 여러 소스의 데이터 통합
- GDPR 준수: 민감하지 않은 열만 복제
사전 요구 사항 및 설정
구성 요구 사항
게시자(소스)에서:
# postgresql.conf
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
구독자(대상)에서:
# postgresql.conf
max_replication_slots = 10
max_logical_replication_workers = 10
max_worker_processes = 16
PostgreSQL 재시작
sudo systemctl restart postgresql
네트워크 구성
데이터베이스가 통신할 수 있는지 확인:
# 구독자에서 게시자로 연결 테스트
psql -h publisher.example.com -U replication_user -d source_db
게시자에서 pg_hba.conf 구성:
# 복제 연결 허용
host source_db replication_user subscriber_ip/32 scram-sha-256
기본 논리적 복제 설정
1단계: 복제 사용자 생성
게시자에서:
CREATE ROLE replication_user WITH REPLICATION LOGIN PASSWORD 'secure_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replication_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO replication_user;
2단계: 소스 테이블 생성
게시자에서:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO users (username, email) VALUES
('alice', '[email protected]'),
('bob', '[email protected]');
3단계: 게시 생성
게시자에서:
-- 모든 테이블 게시
CREATE PUBLICATION my_publication FOR ALL TABLES;
-- 또는 특정 테이블 게시
CREATE PUBLICATION my_publication FOR TABLE users, orders;
-- 또는 게시된 테이블에 행 필터 사용
CREATE PUBLICATION active_users FOR TABLE users WHERE (status = 'active');
게시 보기:
SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;
4단계: 복제 테이블 생성
구독자에서:
-- 테이블은 동일한 구조를 가져야 함
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);
5단계: 구독 생성
구독자에서:
CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=publisher.example.com port=5432 dbname=source_db user=replication_user password=secure_password'
PUBLICATION my_publication;
6단계: 복제 확인
게시자에서:
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_replication_slots;
구독자에서:
SELECT * FROM pg_stat_subscription;
SELECT * FROM users; -- 복제된 데이터가 보여야 함
고급 구성
열 수준 복제 (PostgreSQL 15+)
특정 열만 복제:
-- 게시자에서: 민감하지 않은 열만 복제
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
credit_card VARCHAR(20), -- 복제되지 않음
created_at TIMESTAMP
);
CREATE PUBLICATION customer_basic
FOR TABLE customers (id, name, email, created_at);
행 필터링
활성 레코드만 복제:
CREATE PUBLICATION active_data
FOR TABLE orders WHERE (status IN ('pending', 'processing'));
지역별 데이터 분산:
CREATE PUBLICATION us_customers
FOR TABLE customers WHERE (country = 'US');
CREATE PUBLICATION eu_customers
FOR TABLE customers WHERE (country IN ('UK', 'DE', 'FR'));
여러 게시
-- 게시자: 여러 게시 생성
CREATE PUBLICATION oltp_data FOR TABLE users, orders;
CREATE PUBLICATION analytics_data FOR TABLE logs, metrics;
-- 구독자: 여러 게시 구독
CREATE SUBSCRIPTION multi_sub
CONNECTION 'host=publisher port=5432 dbname=mydb user=repuser password=pass'
PUBLICATION oltp_data, analytics_data;
양방향 복제 주의 사항
네이티브 논리적 복제는 양방향으로 연결될 수 있지만, PostgreSQL은 충돌하는 쓰기를 자동으로 병합하지 않습니다. 각 행에 단일 작성자가 있고, 키가 충돌할 수 없으며, 애플리케이션이 충돌을 처리할 수 있는 경우에만 이 패턴을 사용하십시오.
양방향 동기화 예제 스켈레톤
데이터베이스 A 구성:
-- 게시 생성
CREATE PUBLICATION db_a_pub FOR TABLE shared_table;
-- 데이터베이스 B 구독
CREATE SUBSCRIPTION db_a_sub
CONNECTION 'host=db-b.example.com dbname=mydb user=repuser'
PUBLICATION db_b_pub
WITH (origin = none); -- 복제 루프 방지
데이터베이스 B 구성:
-- 게시 생성
CREATE PUBLICATION db_b_pub FOR TABLE shared_table;
-- 데이터베이스 A 구독
CREATE SUBSCRIPTION db_b_sub
CONNECTION 'host=db-a.example.com dbname=mydb user=repuser'
PUBLICATION db_a_pub
WITH (origin = none);
충돌 처리
논리적 복제는 자동 "마지막 쓰기 승리" 충돌 해결을 제공하지 않습니다. 충돌하는 삽입, 업데이트 중 누락된 행, 제약 조건 위반 또는 중복 키는 데이터 문제를 해결할 때까지 적용 작업자를 중지시킬 수 있습니다.
-- 충돌 추적을 위해 복제 ID 설정
ALTER TABLE shared_table REPLICA IDENTITY FULL;
충돌 감소 전략:
- 타임스탬프 기반 소유권 확인:
updated_at및 작성자 식별자를 추가하여 애플리케이션이 오래된 쓰기를 거부할 수 있도록 합니다.
CREATE TABLE shared_table (
id SERIAL PRIMARY KEY,
data TEXT,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_shared_table_timestamp
BEFORE UPDATE ON shared_table
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
- 버전 번호 지정: 애플리케이션에서 버전을 증가시키고 오래된 버전을 기반으로 업데이트를 거부합니다.
CREATE TABLE shared_table (
id SERIAL PRIMARY KEY,
data TEXT,
version INTEGER DEFAULT 1
);
초기 데이터 동기화 옵션
옵션 1: 자동 복사 (기본값)
-- 구독자가 기존 데이터를 자동으로 복사
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (copy_data = true); -- 기본값
옵션 2: 수동 초기 동기화
대규모 데이터 세트의 경우 pg_dump 사용:
# 게시자에서 특정 테이블 덤프
pg_dump -h publisher.example.com -U postgres -d source_db \
-t users -t orders --no-owner --no-acl > initial_data.sql
# 구독자에 로드
psql -h subscriber.example.com -U postgres -d target_db < initial_data.sql
# 초기 복사 없이 구독 생성
psql -h subscriber.example.com -U postgres -d target_db -c "
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=source_db user=repuser'
PUBLICATION my_pub
WITH (copy_data = false);
"
옵션 3: 대규모 진행 중 트랜잭션 스트리밍
-- 커밋을 기다리는 대신 대규모 진행 중 트랜잭션 스트리밍
CREATE SUBSCRIPTION fast_sync
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (
copy_data = true,
streaming = on,
synchronous_commit = off
);
논리적 복제 모니터링
게시자 모니터링
-- 복제 슬롯 보기
SELECT
slot_name,
plugin,
active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_size
FROM pg_replication_slots;
-- 활성 복제 연결 보기
SELECT
pid,
usename,
application_name,
client_addr,
state,
sync_state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS send_lag
FROM pg_stat_replication;
구독자 모니터링
-- 구독 상태 보기
SELECT
subname,
pid,
received_lsn,
latest_end_lsn,
pg_size_pretty(pg_wal_lsn_diff(latest_end_lsn, received_lsn)) AS lag
FROM pg_stat_subscription;
-- 복제 오류 확인
SELECT * FROM pg_stat_subscription WHERE last_msg_receipt_time < NOW() - INTERVAL '5 minutes';
모니터링 스크립트
#!/bin/bash
# logical-replication-monitor.sh
echo "=== 게시자 상태 ==="
psql -h publisher -d mydb -c "
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots;"
echo ""
echo "=== 구독자 상태 ==="
psql -h subscriber -d mydb -c "
SELECT subname, pid,
pg_size_pretty(pg_wal_lsn_diff(latest_end_lsn, received_lsn)) AS lag
FROM pg_stat_subscription;"
문제 해결
문제 1: 구독이 데이터를 수신하지 않음
구독 상태 확인:
SELECT subname, subenabled, subconninfo FROM pg_subscription;
SELECT subname, pid, received_lsn, latest_end_lsn FROM pg_stat_subscription;
비활성화된 경우 구독 활성화:
ALTER SUBSCRIPTION my_sub ENABLE;
오류 확인:
SELECT * FROM pg_stat_subscription;
문제 2: 복제 지연 증가
느린 테이블 식별:
SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC;
작업자 프로세스 증가:
# 구독자의 postgresql.conf
max_logical_replication_workers = 20
max_worker_processes = 30
문제 3: 복제 슬롯 블로트
슬롯 사용량 확인:
SELECT
slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
비활성 슬롯 삭제:
SELECT pg_drop_replication_slot('inactive_slot_name');
문제 4: 초기 동기화 실패
초기 동기화 다시 시작:
-- 구독 삭제 및 재생성
DROP SUBSCRIPTION my_sub;
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (copy_data = true);
성능 최적화
1. 적절한 복제 ID 사용
-- 기본값: PRIMARY KEY만
ALTER TABLE my_table REPLICA IDENTITY DEFAULT;
-- 전체: 모든 열 (더 높은 오버헤드)
ALTER TABLE my_table REPLICA IDENTITY FULL;
-- 인덱스: 특정 고유 인덱스 사용
CREATE UNIQUE INDEX replica_idx ON my_table(col1, col2);
ALTER TABLE my_table REPLICA IDENTITY USING INDEX replica_idx;
2. 대규모 트랜잭션을 위한 병렬 적용
CREATE SUBSCRIPTION parallel_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (streaming = parallel);
모범 사례
- 항상 복제 ID 사용: 테이블에 PRIMARY KEY 또는 UNIQUE 제약 조건이 있는지 확인
- 복제 지연 모니터링: 복구 요구 사항 및 WAL 보존 예산에 따라 알림 설정
- 행 필터를 신중하게 사용: 게시자에서 평가되는 필터는 성능에 영향을 미침
- 충돌 계획: 양방향 쓰기를 시도하기 전에 충돌 감지 구현
- 장애 조치 절차 테스트: 데이터베이스 간 전환 연습
- 정기 유지 관리: 오래된 복제 슬롯 정리
핵심 요점
단방향 논리적 복제로 시작하십시오. 선택적 테이블 동기화, 보고용 복사본 및 많은 업그레이드 워크플로에 안정적입니다. 양방향 복제를 체크리스트 항목이 아닌 고급 설계 문제로 취급하십시오. 양쪽에 쓰기 전에 행 소유권, 키 생성 및 충돌 처리를 정의하십시오.