11장. 백업과 PITR — "어제 오후 1시 59분으로 되돌려 주세요"
실수로 DROP TABLE을 날린 순간에도 살아날 길은 있습니다. 베이스 백업 + WAL 아카이브의 조합으로 원하는 시점을 정확히 재현하는 Point-In-Time Recovery를 배웁니다.
① 논리 백업(pg_dump) vs 물리 백업(pg_basebackup)의 역할 분담 ② 연속 WAL 아카이브 파이프라인 구성 ③ PITR — 특정 시점으로 복구하는 타임라인과 절차 ④ archive 실패의 함정과 DR 드릴(RPO/RTO)
11.1 백업의 두 축 — SQL로 다시 만들기 vs 바이트로 되살리기
PostgreSQL 백업은 성격이 전혀 다른 두 축으로 나뉩니다. 논리 백업은 "테이블을 다시 만들 수 있는 SQL"을 뽑아내고, 물리 백업은 "데이터 디렉터리 자체"를 바이트 단위로 복사합니다.
| 구분 | 논리 백업 (pg_dump) | 물리 백업 (pg_basebackup) |
|---|---|---|
| 단위 | SQL 문 (DDL + COPY) | 데이터 파일 (8KB 페이지) |
| 복구 시간 | 큰 DB에서 매우 느림 (인덱스 재생성) | 빠름 (복사 + WAL 리플레이) |
| 메이저 버전 업그레이드 | 가능 | 불가 (같은 major만) |
| 객체 선택 | 테이블·스키마 단위 가능 | 클러스터 전체만 |
| PITR | 불가 | 가능 (+WAL 아카이브) |
논리 백업은 "테이블 하나를 실수로 날렸을 때 빠르게 되살리는 보험", 물리 백업+WAL은 "클러스터 전체 재해 복구와 PITR"용입니다. 그리고 pg_dump는 롤·테이블스페이스 같은 전역 객체를 포함하지 않으므로 pg_dumpall --globals-only를 반드시 함께 백업하세요.
# 논리 백업 — custom 포맷(-Fc)이 가장 권장: 선택적·병렬 복구 가능
pg_dump -h db.example -U postgres -Fc -f app.dump app
pg_dumpall --globals-only > globals.sql # 롤·전역 객체 (필수!)
# 복구 — 병렬 8개로
pg_restore -d app_restored --jobs=8 app.dump
# 복구 후 반드시 ANALYZE — 통계 없으면 플랜이 망가진다
11.2 PITR의 재료 — 베이스 백업 + WAL 아카이브
PITR(Point-In-Time Recovery)의 원리는 9장의 크래시 리커버리와 똑같습니다. 다른 점은 단 하나 — "어느 시점의 데이터 파일 + 어디까지의 WAL"을 우리가 고른다는 것. 그러려면 두 가지 재료를 상시 준비해야 합니다.
- 베이스 백업 — 특정 시점의 데이터 디렉터리 스냅샷 (
pg_basebackup, 주 1회 등) - WAL 아카이브 — 그 이후 생성된 모든 WAL 세그먼트의 영구 보관
주기적으로(예: 주 1회) pg_basebackup이 데이터 디렉터리 전체를 베이스 백업 저장소로 복사합니다. 서비스는 멈추지 않습니다.
그 사이에도 트랜잭션은 계속 WAL을 만듭니다. 16MB 세그먼트가 가득 찰 때마다 archive_command가 호출되어 아카이브 저장소로 복사합니다.
세그먼트가 하나도 빠짐없이 쌓입니다. 이것이 "연속 아카이브(continuous archiving)" — 중간에 구멍이 하나라도 나면 그 이후 시점으로는 복구할 수 없습니다.
이제 공식이 완성됐습니다: 베이스 백업 + 그 이후의 WAL 전부 = 백업 이후 임의 시점을 재현 가능. base만으로도, WAL만으로도 복구할 수 없습니다 — 반드시 조합입니다.
# postgresql.conf — 아카이브 활성화
wal_level = replica # minimal은 아카이브 불가
archive_mode = on # 변경 시 재시작 필요
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
# %p = WAL 파일 경로, %f = 파일명. 0 리턴 = 성공
# 베이스 백업 (주기 실행)
pg_basebackup -h primary.example -U replicator \
-D /backup/base_$(date +%Y%m%d) \
-Ft -z -X stream -P # tar+gzip, 백업 중 WAL 동시 수집
공식 문서의 test ! -f && cp 패턴은 내용 검증이 없어, 목적지에 손상된 동명 파일이 있으면 실패를 무한 반복합니다. 운영에서는 pgBackRest / wal-g / barman 같은 전용 도구를 쓰세요 — 원자적 업로드, 해시 검증, 병렬, 암호화, 보존 정책까지 기본 제공합니다. (v15+는 archive_library로 셸 오버헤드도 제거 가능)
11.3 PITR — 시간을 되감는 타임라인
시나리오: 4월 23일 14:00, 누군가 운영 DB에 실수로 DROP TABLE orders를 실행했습니다. 마지막 베이스 백업은 4월 20일. 목표는 사고 1분 전인 13:59로 되돌리는 것입니다.
4/20 00:00 베이스 백업 이후 WAL이 계속 아카이브되고 있었습니다. 그런데 4/23 14:00, 실수의 DROP TABLE이 실행됐습니다. 이 사고 자체도 WAL에 기록되어 있다는 점이 중요합니다.
목표 시점을 정합니다: 사고 직전인 13:59. 복구는 "사고 포함 전부"가 아니라 "사고 직전까지만" 재생하는 것이 핵심입니다.
별도 서버(또는 비운 데이터 디렉터리)에 베이스 백업을 복원합니다. 이 시점의 데이터는 4/20 00:00 상태입니다.
recovery.signal을 만들고 기동하면, restore_command가 아카이브에서 WAL을 가져와 순서대로 리플레이합니다. 4/21… 4/22… 시간이 빨리감기로 흐릅니다.
recovery_target_time = '13:59'에 도달하면 리플레이를 멈춥니다. DROP TABLE 레코드는 재생되지 않았으므로 orders는 살아 있습니다! 검증 후 pg_promote()로 쓰기를 열면 새 타임라인에서 서비스가 재개됩니다.
실제 절차는 설정 몇 줄입니다 (v12+ 기준 — recovery.conf는 v12에서 제거되었습니다).
# 1) 데이터 디렉터리 비우고 base backup 복원
pg_ctl stop -D $PGDATA && rm -rf $PGDATA/*
tar -xf /backup/base_20260420.tar -C $PGDATA
# 2) postgresql.conf에 복구 타겟 지정
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-04-23 13:59:00+09'
recovery_target_action = 'pause' # pause | promote | shutdown
# 3) recovery.signal 생성 후 기동
touch $PGDATA/recovery.signal
pg_ctl start -D $PGDATA
-- 4) 타겟 도달 후 데이터 확인 → 서비스 오픈
SELECT pg_wal_replay_resume(); -- 또는 pg_promote()
recovery_target_xid(트랜잭션 ID), recovery_target_lsn, recovery_target_name(pg_create_restore_point('before_migration')으로 만든 이름표)도 가능합니다. 큰 변경 작업 직전에 restore point를 만들어 두는 습관이 새벽의 당신을 구합니다.
11.4 아카이브가 조용히 실패하고 있다면
PITR 체계의 최대 적은 화려한 장애가 아니라 조용한 실패입니다. archive_command가 0이 아닌 값을 반환하면 PostgreSQL은 해당 WAL을 pg_wal/에 계속 보관하며 재시도합니다 — S3 인증 만료, 디스크 풀, 네트워크 단절이 흔한 원인입니다. 눈치채지 못하면 두 가지 재앙이 동시에 옵니다: 복구 가능 시점이 멈추고(RPO 파괴), pg_wal이 차올라 서버가 PANIC 정지합니다.
정상 상태: 세그먼트가 가득 차면 archive_command가 복사하고, 성공(0 리턴)하면 로컬 세그먼트는 재활용됩니다.
어느 날 S3 인증이 만료됩니다. archive_command가 실패를 반환하기 시작하지만 서비스는 멀쩡해 보입니다 — 조용한 실패의 시작.
PostgreSQL은 아카이브 안 된 WAL을 지울 수 없으므로 pg_wal/에 계속 쌓아두고 재시도합니다. archive_status/의 .ready 파일이 수천 개로 불어납니다.
결과는 이중 재앙: 복구 가능 시점은 마지막 성공 지점에 멈춰 있고(그 이후로는 PITR 불가), 디스크가 차면 PANIC 셧다운. pg_stat_archiver.failed_count를 일간 감시 항목에 반드시 넣으세요.
-- 아카이버 건강 검진 — failed_count는 꾸준히 0이어야 한다
SELECT archived_count, failed_count,
last_archived_wal, last_archived_time,
last_failed_wal, last_failed_time
FROM pg_stat_archiver;
11.5 RPO / RTO — 그리고 "복구해본 적 없는 백업은 백업이 아니다"
백업 설계의 언어 두 가지: RPO(Recovery Point Objective — 어디까지의 데이터를 잃어도 되는가)와 RTO(Recovery Time Objective — 얼마나 빨리 복구해야 하는가)입니다. 구성에 따른 대략의 수준은 이렇습니다.
| 구성 | 대략적 RPO | 대략적 RTO | 비용 |
|---|---|---|---|
| 주 1회 pg_dump만 | 최대 1주 | 수 시간 | 저 |
| 일 1회 base + 연속 WAL 아카이브 | 초 단위 | 1~수 시간 | 중 |
| + 비동기 스트리밍 replica | 초 단위 | 수 분 (페일오버) | 중~고 |
| + 동기 스트리밍 replica | 0 | 수 분 | 고 |
복구 절차가 문서로만 존재하면 새벽 2시에 반드시 무너집니다. 월 1회 별도 환경에서 최신 백업 → PITR → 쿼리 검증 → 소요 시간 기록까지 실제로 수행하세요. 자주 빠뜨리는 것: pg_dumpall --globals-only(롤 없으면 앱 접속 실패), 아카이브를 운영 서버와 같은 디스크에 저장(DR 요건 미달), 복구 후 ANALYZE 누락.
✍️ 이해도 체크
recovery_target_time은 어떻게 잡아야 할까요?recovery_target_action = 'pause'로 멈춘 뒤 데이터를 확인하고 promote하는 것이 안전한 순서입니다.이 장의 원문 문서: chapters/ch11_backup_recovery.md — pg_dump/pg_restore 전체 옵션, pgBackRest·wal-g 등 외부 도구 비교, DR 드릴 체크리스트, 자주 만나는 함정 7가지가 담겨 있습니다.