LLM WikiAccess-protected knowledge portal
← 스터디 홈
7편 · 약 28분

PITR과 standby 운영: base backup, timeline, failover/rejoin

WAL 아카이브가 PITR의 기반이다

PostgreSQL의 PITR(Point-In-Time Recovery)은 두 요소로 성립한다: 베이스 백업WAL 아카이브의 연속성. 베이스 백업은 특정 시점의 물리적 스냅샷이고, WAL 아카이브는 그 이후 모든 변경 이력을 담는다. 두 요소 중 하나라도 끊기면 원하는 시점으로 되돌아갈 수 없다.

WAL 아카이브를 활성화하려면 두 가지 설정이 필요하다.

wal_level = replica          # PITR + 스트리밍 복제를 모두 지원하는 최솟값
archive_mode = on
archive_command = 'cp %p /archive/%f'

%p는 아카이브할 WAL 세그먼트의 소스 경로, %f는 대상 파일명이다. archive_command성공 시에만 종료 코드 0을 반환해야 한다. 실패했는데 0을 반환하면 PostgreSQL은 해당 세그먼트를 완료로 처리하고 pg_wal/에서 제거한다. 아카이브에는 파일이 없는데 내부 카탈로그에는 완료로 기록된다. PITR 시도 시 복구 체인이 조용히 끊긴다.

archive_timeout을 설정하면 WAL 쓰기가 없어도 일정 주기마다 세그먼트를 강제 전환한다.

archive_timeout = 300   # 5분마다 세그먼트 전환 강제

아카이브 상태는 pg_stat_archiver로 실시간 확인한다.

SELECT
  archived_count,
  last_archived_wal,
  last_archived_time,
  failed_count,
  last_failed_wal,
  last_failed_time
FROM pg_stat_archiver;

failed_count가 증가하고 있다면 archive_command를 즉시 점검해야 한다.


pg_basebackup 실전

베이스 백업은 pg_basebackup으로 수행한다. 운영 중인 PostgreSQL에서 파일 시스템 수준의 물리적 복사본을 가져온다.

pg_basebackup \
  -h localhost \
  -U replicator \
  -D /backup/base-$(date +%Y%m%d) \
  -Fp \
  -Xs \
  -P \
  -R

-Xs(WAL 스트리밍)가 핵심이다. 백업 실행 중에 발생하는 WAL 세그먼트를 스트리밍으로 가져와 백업 디렉터리의 pg_wal/에 포함한다. 덕분에 restore_command 설정 없이도 백업 직후 스탠바이를 바로 기동할 수 있다.

-R을 쓰면 스탠바이 구성 파일을 자동으로 만든다: standby.signal 파일과 postgresql.auto.confprimary_conninfo 항목.

백업 실행 시 PostgreSQL은 내부적으로 체크포인트를 수행한다. 체크포인트 완료 LSN이 베이스 백업의 시작 기준점이 된다. 이 시점 이후 WAL이 연속적으로 아카이브되어 있어야 복구가 가능하다.

옵션용도
-Fpplain 포맷: 디렉터리 그대로 복사
-Fttar 포맷: 단일 파일로 묶음
-XsWAL 스트리밍: 백업 중 발생 WAL 포함
-XpWAL 위치 지정: 별도 아카이브에서 나중에 가져옴
-Rstandby.signal + primary_conninfo 자동 생성
-j N병렬 복사로 백업 속도 향상
-zgzip 압축 (tar 포맷 전용)

복구 파라미터: PostgreSQL 12+

PostgreSQL 12부터 recovery.conf가 폐지됐다. 복구 관련 설정은 postgresql.conf(또는 postgresql.auto.conf)에 통합됐고, 복구 모드 진입은 신호 파일로 제어한다.

신호 파일역할
recovery.signal지정 시점까지 복구 후 승격
standby.signal스탠바이 루프(연속 복제 대기)

두 파일이 모두 있으면 standby.signal이 우선한다. 복구가 완료되면 recovery.signal은 자동으로 삭제된다. 파일이 남아 있으면 다음 재시작 시 다시 복구 모드로 진입한다.

핵심 복구 파라미터:

파라미터설명
restore_command아카이브에서 WAL 세그먼트를 가져오는 셸 명령
recovery_target_time복구 목표 타임스탬프
recovery_target_lsn복구 목표 LSN
recovery_target_xid복구 목표 트랜잭션 ID
recovery_target_namepg_create_restore_point()로 지정한 이름
recovery_target'immediate': 일관성 도달 즉시 중단
recovery_target_inclusive목표 트랜잭션 포함 여부 (기본값 true)
recovery_target_action복구 완료 후 동작: 'promote' / 'pause' / 'shutdown'

타임존 명시 필수: recovery_target_time에 타임존 없이 쓰면 서버 타임존이 적용된다. 항상 '2026-06-29 14:00:00+09' 형식으로 지정한다.


PITR 전체 흐름

아래 다이어그램은 베이스 백업과 WAL 아카이브에서 복구 인스턴스를 만들고 승격하는 흐름을 보여준다.

Primary DB
운영 중
pg_basebackup
주기적
Base Backup
물리 스냅샷
+
WAL Archive
연속 로그
⚠ 장애 발생
Base Backup
복원
WAL 재생
restore_command
목표 시점 도달
recovery_target_*
New Primary
pg_promote()
PITR 전체 흐름: 베이스 백업 + WAL 아카이브 → 시점 복구 → 승격

1단계: 베이스 백업 복원

tar -xzf /backup/base-20260629.tar.gz -C /var/lib/postgresql/17/restored

2단계: 복구 파라미터 설정

# postgresql.conf에 추가
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-06-29 14:00:00+09'
recovery_target_action = 'promote'

3단계: recovery.signal 생성 후 기동

touch /var/lib/postgresql/17/restored/recovery.signal
pg_ctl start -D /var/lib/postgresql/17/restored

4단계: 복구 완료 확인

SELECT pg_is_in_recovery();   -- false = 승격 완료

PostgreSQL 로그에서 다음 메시지를 확인한다.

LOG:  recovery stopping before commit of transaction ..., time ...
LOG:  database system is ready to accept connections

recovery_target_action = 'pause'로 설정하면 복구가 완료 직전에 멈추고 데이터를 검증한 다음 SELECT pg_wal_replay_resume()으로 최종 승격할 수 있다.


Timeline ID와 분기

복구 또는 승격이 일어날 때마다 PostgreSQL은 타임라인 ID(TLI)를 하나 증가시킨다. WAL 세그먼트 파일명에 타임라인이 포함되어 있어, 동일 LSN에서 분기된 두 타임라인의 WAL이 충돌하지 않는다.

승격 직후 PostgreSQL은 <TLI>.history 파일을 아카이브에 기록한다.

# 타임라인 2의 히스토리 파일 (00000002.history)
1	0/5000000	no recovery target specified

이 파일이 있어야 새 스탠바이가 구 타임라인의 WAL을 재생하다가 분기 시점에서 자동으로 새 타임라인으로 전환할 수 있다.

PITR로 시간을 되돌리면 새 타임라인이 생긴다. 이 타임라인 이후의 원래 WAL은 사용하지 않는다. 둘은 같은 LSN에서 갈라진 독립적인 이력이다. "잘못 복구했다"는 것을 알았을 때 아카이브에 원래 WAL이 남아 있으면 다시 그 시점으로 돌아갈 수 있다. 이것이 타임라인이 PITR에서 중요한 이유다.


Hot Standby 운영

스탠바이가 WAL을 재생하는 동안에도 읽기 쿼리를 허용하는 모드가 Hot Standby다. hot_standby = on(기본값)으로 활성화된다.

# 스탠바이 postgresql.conf
hot_standby = on
hot_standby_feedback = on          # 프라이머리에게 스탠바이 실행 쿼리 정보 전달
max_standby_streaming_delay = 30s  # 충돌 시 WAL 재생 최대 지연 허용 시간

쿼리-복제 충돌 메커니즘

WAL 재생이 읽기 쿼리와 충돌하면(예: 쿼리가 참조 중인 행의 dead tuple을 재생이 정리하려 할 때) PostgreSQL은 max_standby_streaming_delay 동안 재생을 잠시 멈춘다. 그래도 충돌이 해소되지 않으면 쿼리를 강제 취소한다.

ERROR:  canceling statement due to conflict with recovery

hot_standby_feedback = on이면 스탠바이는 현재 실행 중인 가장 오래된 트랜잭션 ID를 프라이머리에 전달한다. 프라이머리는 그 ID가 필요로 하는 dead tuple을 VACUUM이 제거하지 않도록 보류한다. 충돌이 줄지만 프라이머리에 bloat이 약간 증가할 수 있다.

hot_standby = off이면 스탠바이는 복구만 수행하고 쿼리를 받지 않는다(Warm Standby). 읽기 분산이 불필요하다면 충돌 자체가 없어 운영이 단순해진다.


Failover와 pg_rewind로 재합류

Failover: 스탠바이 승격

프라이머리 장애가 확인되면 스탠바이를 승격한다.

# pg_ctl 방식
pg_ctl promote -D /var/lib/postgresql/17/main

# SQL 함수 방식 (PostgreSQL 12+)
SELECT pg_promote();

승격 후 다른 스탠바이가 있다면 primary_conninfo를 새 프라이머리로 갱신한다.

# 기존 스탠바이의 postgresql.auto.conf 수정 후 리로드
primary_conninfo = 'host=new-primary port=5432 user=replicator'
pg_ctl reload -D /var/lib/postgresql/17/replica2

pg_rewind: 구 프라이머리 재합류

구 프라이머리는 새 프라이머리와 WAL이 분기된 상태다. primary_conninfo만 바꿔도 타임라인이 다르므로 복제에 참가할 수 없다. 이때 pg_rewind를 쓴다.

pg_rewind는 두 인스턴스의 분기 LSN을 찾아 분기 이후 변경된 데이터 블록만 새 프라이머리에서 가져온다. 전체 베이스 백업을 다시 받는 것보다 훨씬 빠르다.

사전 요건:

# postgresql.conf — 미리 설정해 두어야 한다
wal_log_hints = on      # data_checksums가 없을 때 필요
# 또는
data_checksums = on     # initdb 시에만 활성화 가능

두 옵션 중 하나가 있어야 pg_rewind가 변경 블록을 식별할 수 있다. data_checksums는 클러스터 초기화 시에만 설정 가능하다.

pg_rewind 실행:

# 1. 구 프라이머리 클린 셧다운 확인
pg_ctl stop -D /var/lib/postgresql/17/old-primary

# 2. pg_rewind 실행
pg_rewind \
  --target-pgdata=/var/lib/postgresql/17/old-primary \
  --source-server="host=new-primary port=5432 user=replicator" \
  --progress

# 3. 스탠바이 신호 파일 + primary_conninfo 설정
touch /var/lib/postgresql/17/old-primary/standby.signal
cat >> /var/lib/postgresql/17/old-primary/postgresql.auto.conf <<EOF
primary_conninfo = 'host=new-primary port=5432 user=replicator'
EOF

# 4. 기동
pg_ctl start -D /var/lib/postgresql/17/old-primary

클린 셧다운 필수: pg_rewind는 구 프라이머리가 정상 종료된 상태여야 한다. 크래시 상태라면 먼저 PostgreSQL을 기동해 크래시 복구를 완료한 뒤 즉시 정상 종료한다. 그 후 pg_rewind를 실행한다.


운영 체크리스트

  • archive_command가 실패 시 반드시 비-0 종료 코드를 반환하는가?
  • pg_stat_archiver.failed_count 증가를 감지하는 알림이 있는가?
  • 베이스 백업과 WAL 아카이브 사이의 LSN 연속성을 주기적으로 검증하는가?
  • recovery_target_time에 타임존을 명시적으로 지정했는가?
  • wal_log_hints = on 또는 data_checksums = on이 설정되어 pg_rewind 사용이 가능한가?
  • max_slot_wal_keep_size를 설정해 스탠바이 단절 시 디스크 고갈을 방지하는가?
  • PITR 복구 리허설을 마지막으로 실행한 날짜와 성공 여부를 기록하고 있는가?
  • failover → pg_rewind 재합류 절차가 문서화되어 팀 전체가 실행할 수 있는가?

References

  • PostgreSQL Documentation, "Continuous Archiving and Point-in-Time Recovery" — https://www.postgresql.org/docs/current/continuous-archiving.html
  • PostgreSQL Documentation, "pg_basebackup" — https://www.postgresql.org/docs/current/app-pgbasebackup.html
  • PostgreSQL Documentation, "pg_rewind" — https://www.postgresql.org/docs/current/app-pgrewind.html
  • PostgreSQL Documentation, "Hot Standby" — https://www.postgresql.org/docs/current/hot-standby.html
  • PostgreSQL Documentation, "Recovery Target Settings" — https://www.postgresql.org/docs/current/recovery-target-settings.html
  • PostgreSQL Documentation, "pg_stat_archiver" — https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ARCHIVER-VIEW
  • Crunchy Data, "Point-in-Time Recovery with PostgreSQL" — https://www.crunchydata.com/blog/point-in-time-recovery-with-postgresql
  • Cybertec, "PITR and pg_rewind in PostgreSQL" — https://www.cybertec-postgresql.com/en/pitr-point-in-time-recovery-for-postgresql/
  • pgBackRest Documentation, "User Guide" — https://pgbackrest.org/user-guide.html
  • PostgreSQL Wiki, "Timelines" — https://wiki.postgresql.org/wiki/Timelines