LLM WikiAccess-protected knowledge portal
← 스터디 홈
5편 · 약 15분

WAL과 복제(Streaming Replication)

데이터를 잃지 않으려면 로그를 먼저 써라

데이터베이스는 변경된 데이터를 즉시 디스크에 쓰지 않는다. 메모리(shared_buffers)의 dirty page를 디스크에 기록하는 작업은 비용이 크기 때문이다. 그러나 메모리에만 데이터를 두면 서버가 갑자기 재시작될 때 변경 내용을 잃는다. PostgreSQL이 선택한 해답이 Write-Ahead Logging(WAL) 이다. 변경 내용을 실제 데이터 파일보다 먼저 순차적으로 WAL 파일에 기록한다. 순차 쓰기는 랜덤 쓰기보다 훨씬 빠르기 때문에 성능 손실이 크지 않다. WAL이 안전하게 기록되면 메모리의 dirty page는 나중에 비동기로 플러시해도 된다. 이 원칙이 지속성(Durability) 을 보장하고, 동시에 스트리밍 복제의 토대가 된다.

WAL의 핵심 구조

LSN(Log Sequence Number)

WAL에 기록된 모든 레코드는 WAL 내에서 고유한 위치 값인 LSN을 가진다. LSN은 단조 증가하는 바이트 오프셋으로, 0/3000060처럼 슬래시로 구분된 16진수로 표현된다.

-- 현재 WAL 쓰기 위치
SELECT pg_current_wal_lsn();

-- 두 LSN 사이의 바이트 차이
SELECT pg_wal_lsn_diff('0/4000000', '0/3000060');

복제 지연, 복구 진행 상황, WAL 아카이브 위치를 모두 LSN으로 측정한다.

WAL 세그먼트 파일

WAL 레코드는 $PGDATA/pg_wal/ 디렉터리 아래에 세그먼트 파일로 저장된다. 기본 크기는 16 MB 이며, initdb --wal-segsize로 변경할 수 있다. 파일명은 24자리 16진수(000000010000000000000001 등)이다. 세그먼트가 꽉 차면 새 파일을 생성한다. 체크포인트 이후 불필요해진 세그먼트는 재사용하거나 삭제된다.

WAL 버퍼와 플러시

백엔드 프로세스는 WAL 레코드를 공유 메모리 영역인 WAL 버퍼에 먼저 쓴다. 버퍼 크기는 wal_buffers(기본 auto, shared_buffers의 약 1/32)로 설정한다. 트랜잭션이 커밋될 때 WAL 버퍼가 WAL 세그먼트 파일로 fsync된다. 이 fsync가 완료된 다음에만 클라이언트에게 커밋 성공 응답을 돌려준다.

체크포인트(Checkpoint)

체크포인트는 공유 버퍼의 dirty page를 실제 데이터 파일에 기록하는 작업이다. 체크포인트가 완료되면 그 이전 LSN까지의 WAL 세그먼트는 복구에 불필요하므로 재사용·삭제할 수 있다.

체크포인트 트리거 조건:

  • checkpoint_timeout(기본 5분) 경과
  • WAL 누적량이 max_wal_size(기본 1 GB)를 초과
  • CHECKPOINT 명령 명시적 실행
# postgresql.conf
checkpoint_timeout = 5min
max_wal_size = 1GB
checkpoint_completion_target = 0.9   # I/O를 인터벌의 90% 동안 분산

checkpoint_completion_target = 0.9는 체크포인트 I/O 버스트를 줄여 일반 쿼리 성능에 미치는 영향을 최소화한다.

full_page_writes

체크포인트 직후 처음 수정되는 페이지는 WAL에 페이지 전체(8 kB) 를 기록한다. 서버 충돌 시 부분 쓰기로 훼손된 페이지를 WAL에서 복원하기 위해서다. 기본값은 on이며, 운영 환경에서 끄면 안 된다.

WAL 수준 설정: wal_level

wal_level은 WAL에 얼마나 많은 정보를 기록할지 결정한다.

기록 범위용도
minimal충돌 복구 최소복제·아카이빙 불가
replica물리 복제 + WAL 아카이빙 지원 (기본값)스트리밍 복제, PITR
logicalreplica + 논리 디코딩 추가논리 복제, CDC

스트리밍 복제를 사용하려면 반드시 wal_level = replica 이상이어야 한다.

스트리밍 복제 아키텍처

스트리밍 복제는 WAL 레코드를 Standby 서버에 실시간으로 전송해 Primary와 데이터를 동기화하는 메커니즘이다. 세 종류의 프로세스가 핵심 역할을 한다.

  • walsender: Primary에서 WAL을 읽어 네트워크로 전송한다.
  • walreceiver: Standby에서 WAL을 수신해 로컬 pg_wal/에 기록한다.
  • startup process: Standby에서 WAL을 재생(replay)하여 데이터 파일을 최신 상태로 유지한다.
Primary 서버
Client Backend WAL Buffer
WAL Buffer pg_wal/
walsender pg_wal/
— WAL stream →
Standby 서버
walreceiver pg_wal/
pg_wal/ startup
process
Data Files
(replay)
hot_standby = on → 읽기 전용 쿼리 허용
비동기 복제 (기본): 낮은 레이턴시, 약간의 replication lag 허용 동기 복제: synchronous_commit=on, RPO=0
스트리밍 복제 아키텍처

복제 설정 단계

1단계: Primary 설정

# postgresql.conf
wal_level = replica
max_wal_senders = 10           # 동시에 연결 가능한 Standby 수
wal_keep_size = 256MB          # Standby 지연 시 WAL을 유지하는 크기
listen_addresses = '*'
# pg_hba.conf — Standby IP가 10.0.0.20인 경우
host  replication  replicator  10.0.0.20/32  scram-sha-256
-- 복제 전용 사용자 생성
CREATE USER replicator REPLICATION LOGIN PASSWORD 'secret';

설정 변경 후 SELECT pg_reload_conf(); 로 반영하거나 서버를 재시작한다. wal_level 변경은 반드시 재시작이 필요하다.

2단계: Standby 초기화

# Primary에서 베이스 백업을 받아 Standby 데이터 디렉터리를 초기화
pg_basebackup \
  -h primary_host \
  -U replicator \
  -D /var/lib/postgresql/17/main \
  -Fp -Xs -P -R

-R 플래그는 postgresql.auto.confprimary_conninfostandby.signal 파일을 자동 생성한다.

3단계: Standby 설정

# postgresql.conf (Standby)
hot_standby = on        # 읽기 전용 쿼리 허용
primary_conninfo = 'host=primary_host port=5432 user=replicator password=secret application_name=standby1'

$PGDATA/standby.signal 파일이 존재하면 PostgreSQL은 자동으로 Standby 모드로 기동한다.

동기 vs 비동기 복제

방식커밋 응답 시점장점단점
비동기 (기본)WAL을 로컬에 기록한 직후낮은 레이턴시Standby 지연 허용 (replication lag)
동기Standby가 WAL 수신 확인 후데이터 손실 없음 (RPO=0)레이턴시 증가, Standby 장애 시 Primary 블로킹
# postgresql.conf (Primary) — 동기 복제
synchronous_commit = on
synchronous_standby_names = 'ANY 1 (standby1, standby2)'

ANY 1은 두 Standby 중 하나라도 수신 확인하면 커밋을 응답한다. 가용성과 안전성의 균형을 맞추는 설정이다.

synchronous_commit = remote_write는 Standby OS 버퍼까지만 기다리는 중간 단계다. 완전한 fsync를 기다리지 않아 성능 손실을 줄이면서도 일반 비동기보다 안전하다.

복제 슬롯(Replication Slot)

복제 슬롯은 Primary가 특정 Standby에 필요한 WAL 세그먼트를 삭제하지 않도록 보장한다. Standby가 일시적으로 오프라인이 되어도 WAL을 보존한다.

-- Primary에서 물리 복제 슬롯 생성
SELECT pg_create_physical_replication_slot('standby1_slot');

-- 슬롯 상태 확인
SELECT slot_name, active, restart_lsn, wal_status
FROM pg_replication_slots;
# Standby postgresql.conf
primary_slot_name = 'standby1_slot'

주의: Standby가 장시간 오프라인이면 pg_wal/이 무한히 증가한다. max_slot_wal_keep_size를 설정해 디스크 고갈을 방지한다.

max_slot_wal_keep_size = 2GB

복제 모니터링

Primary에서 Standby 상태 확인

SELECT
  application_name,
  state,
  sent_lsn,
  write_lsn,
  flush_lsn,
  replay_lsn,
  write_lag,
  flush_lag,
  replay_lag,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;

replay_lag이 클수록 Standby가 Primary에 뒤처진 상태다. 임계치(예: 30초)를 넘으면 알림을 보내도록 모니터링을 구성한다.

Standby에서 수신 상태 확인

SELECT status, received_lsn, last_msg_send_time, last_msg_receipt_time
FROM pg_stat_wal_receiver;

DBA 체크리스트

  • pg_stat_replicationreplay_lag을 주기적으로 모니터링한다. 임계치 초과 시 알림을 발송한다.
  • 복제 슬롯에는 max_slot_wal_keep_size를 반드시 설정해 WAL 누적으로 인한 디스크 고갈을 막는다.
  • wal_level 변경은 서버 재시작이 필요하다. 계획된 유지보수 창에서 수행한다.
  • 동기 복제 사용 시 ANY N (standby_list) 구문으로 다중 Standby를 지정해 단일 Standby 장애가 Primary 쓰기를 블로킹하지 않도록 한다.
  • Standby를 읽기 부하 분산에 사용할 경우 hot_standby_feedback = on과 적절한 max_standby_streaming_delay를 설정해 쿼리 충돌을 줄인다.

References

  • https://www.postgresql.org/docs/current/wal-intro.html
  • https://www.postgresql.org/docs/current/wal-internals.html
  • https://www.postgresql.org/docs/current/runtime-config-wal.html
  • https://wiki.postgresql.org/wiki/Streaming_Replication
  • https://www.postgresql.org/docs/current/warm-standby.html
  • https://www.postgresql.org/docs/current/app-pg-basebackup.html
  • https://stormatics.tech/blogs/guide-on-setting-up-physical-streaming-replication-in-postgresql
  • https://devops.aibit.im/article/guide-setup-postgresql-streaming-replication