DB Study

11장. 백업과 PITR — "어제 오후 1시 59분으로 되돌려 주세요"

실수로 DROP TABLE을 날린 순간에도 살아날 길은 있습니다. 베이스 백업 + WAL 아카이브의 조합으로 원하는 시점을 정확히 재현하는 Point-In-Time Recovery를 배웁니다.

고급 ⏱ 약 35분 🎬 애니메이션 3개 선수 지식: 9장 WAL
🧭
이 장에서 배우는 것

① 논리 백업(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"을 우리가 고른다는 것. 그러려면 두 가지 재료를 상시 준비해야 합니다.

  1. 베이스 백업 — 특정 시점의 데이터 디렉터리 스냅샷 (pg_basebackup, 주 1회 등)
  2. WAL 아카이브 — 그 이후 생성된 모든 WAL 세그먼트의 영구 보관
🐘 운영 서버 PostgreSQL 트랜잭션 처리 중… pg_wal/ 16MB 세그먼트들 📦 베이스 백업 저장소 base_0420 일요일 00:00 🗃️ WAL 아카이브 (S3/NFS) pg_basebackup (주 1회) 세그먼트가 찰 때마다 archive_command B3 B4 B5 끊김 없이 계속 쌓임 = "연속 아카이브" 복구 공식 📦 베이스 백업 + 🗃️ 그 이후의 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 동시 수집
⚠️
위 archive_command 예시는 "교과서용"입니다

공식 문서의 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 📦 base backup 4/21 4/22 🗃️ WAL 아카이브 계속 쌓임 4/23 14:00 💥 DROP TABLE orders! 🎯 target = 13:59 ① 새 서버에 base 복원 tar -xf base_0420.tar ② WAL 리플레이 ▶▶ restore_command로 아카이브에서 ③ 13:59에서 정지 ✋ recovery_target_time orders 테이블이 살아있는 세상 복원 완료 🎉 pause 상태에서 데이터 검증 → 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 정지합니다.

🐘 운영 서버 — pg_wal/ B3 B4 🗃️ 아카이브 (S3) 정상: 복사 후 pg_wal에서 재활용 … B1 B2 B3 ✓ S3 인증 만료! 실패 리턴 B5 B6 B7 B8 .ready 파일 적체… 실패한 WAL은 지울 수 없어 계속 쌓임 이중 재앙 ① 복구 가능 시점이 B4에서 멈춤 (RPO 파괴) ② 디스크 풀 → 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초 단위수 분 (페일오버)중~고
+ 동기 스트리밍 replica0수 분
🚨
DR 드릴 — 월 1회, 실제로 복구해 보라

복구 절차가 문서로만 존재하면 새벽 2시에 반드시 무너집니다. 월 1회 별도 환경에서 최신 백업 → PITR → 쿼리 검증 → 소요 시간 기록까지 실제로 수행하세요. 자주 빠뜨리는 것: pg_dumpall --globals-only(롤 없으면 앱 접속 실패), 아카이브를 운영 서버와 같은 디스크에 저장(DR 요건 미달), 복구 후 ANALYZE 누락.

✍️ 이해도 체크

PITR(Point-In-Time Recovery)을 수행하기 위해 반드시 필요한 재료의 조합은?
✅ WAL은 "변경의 증분 기록"이라 출발점(베이스 백업)이 없으면 재생할 대상이 없고, 베이스 백업만으로는 백업 시점 이후로 갈 수 없습니다. 둘의 조합이어야 "백업 이후 임의 시점" 복구가 가능합니다. pg_dump는 덤프 시점 하나로만 복구 가능하며 PITR은 불가능합니다. (참고로 recovery.conf는 v12에서 제거되었습니다.)
14:00에 실수로 DROP TABLE이 실행됐습니다. PITR로 복구할 때 recovery_target_time은 어떻게 잡아야 할까요?
✅ 사고(DROP TABLE)도 WAL에 기록되어 있으므로, WAL을 끝까지 재생하면 사고까지 그대로 재현됩니다. 타겟을 사고 직전으로 잡아 그 레코드가 리플레이되기 전에 멈추는 것이 PITR의 핵심입니다. recovery_target_action = 'pause'로 멈춘 뒤 데이터를 확인하고 promote하는 것이 안전한 순서입니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch11_backup_recovery.md — pg_dump/pg_restore 전체 옵션, pgBackRest·wal-g 등 외부 도구 비교, DR 드릴 체크리스트, 자주 만나는 함정 7가지가 담겨 있습니다.