DB Study

9장. WAL과 Checkpoint — 절대 잃어버리지 않는 기술

전원이 갑자기 나가도 커밋된 데이터는 살아남습니다. 그 비밀은 "데이터보다 일기를 먼저 쓴다"는 단순한 규칙 — WAL(Write-Ahead Log)에 있습니다.

중급 ⏱ 약 30분 🎬 애니메이션 3개 선수 지식: 2장 아키텍처
🧭
이 장에서 배우는 것

① WAL-first 원칙 — 커밋이 디스크에 살아남는 원리 ② 크래시 후 WAL 리플레이로 복구되는 과정 ③ Checkpoint가 복구 시작점을 전진시키는 방법 ④ full_page_writes와 pg_wal 폭증 3대 원인

9.1 WAL — 하나의 로그가 세 가지 일을 한다

WAL(Write-Ahead Log)은 "무슨 변경이 있었는지"를 순서대로 적는 한 줄짜리 일기장입니다. 이 단순한 로그 하나가 PostgreSQL의 세 기둥을 동시에 떠받칩니다.

🛡️ 내구성 (Durability)

COMMIT 응답 전에 변경 내역이 디스크의 WAL에 flush됩니다. 데이터 파일은 아직 안 써도 됩니다 — 일기만 있으면 재현 가능하니까요.

🔁 크래시 복구

비정상 종료 후 재시작하면 마지막 Checkpoint 지점부터 WAL을 다시 읽어 "잊힌 변경"을 재방송합니다.

📡 복제의 기반

Physical 복제는 WAL을 그대로 스트리밍하고, Logical 복제는 WAL을 디코드합니다. WAL이 없으면 복제도 없습니다. (10장)

핵심 불변식은 이름 그대로입니다: 데이터 페이지를 디스크에 쓰기 전에, 그 변경을 설명하는 WAL 레코드가 반드시 먼저 flush되어 있어야 한다. 아래 애니메이션에서 COMMIT 한 번의 여정을 따라가 봅시다.

클라이언트 Backend Shared Buffers (메모리) dirty page WAL Buffer (메모리) pg_wal/ 💾 (디스크, 영속) heap 데이터 파일 (디스크) COMMIT ① 메모리 페이지만 수정 WAL 레코드+LSN ② 변경 내역 append ③ write + fsync ⚡ ④ "커밋 성공!" (durable) heap은 아직 안 씀! ✋ ⑤ 나중에 Checkpoint/ BGWriter가 천천히 규칙: heap을 쓰기 전에 해당 WAL이 반드시 먼저 디스크에! (Write-Ahead)
💡
LSN — WAL 세계의 좌표

LSN(Log Sequence Number)은 WAL 내의 바이트 단위 위치를 가리키는 64비트 값입니다(표기: 16/B374D848). 복제 지연, 복구 위치, Checkpoint 기록 등 PostgreSQL의 거의 모든 "위치" 지표가 LSN으로 표현됩니다.

-- 현재 WAL 쓰기 위치(LSN) 확인
SELECT pg_current_wal_lsn();          -- 예: 16/B374D848

-- 두 LSN 사이 거리(바이트)
SELECT pg_wal_lsn_diff('16/B3800000', '16/B0000000');

-- 이 LSN이 속한 WAL 세그먼트 파일(기본 16MB)
SELECT pg_walfile_name('16/B374D848');  -- 0000000100000016000000B3

9.2 크래시 리커버리 — 일기장으로 되살리기

서버가 갑자기 죽으면 어떻게 될까요? 메모리의 dirty page들은 증발했지만, 커밋된 변경의 일기(WAL)는 디스크에 남아 있습니다. 재시작 시 PostgreSQL은 이 일기를 처음부터가 아니라, 마지막 Checkpoint의 redo point부터 다시 읽으며 재생합니다.

WAL (디스크에 살아남음) 이미 데이터 파일에 반영됨 redo point (마지막 Checkpoint) 크래시 전 커밋들 (heap엔 아직 없을 수 있음) 💥 크래시 ⚡ 전원 차단! 메모리의 dirty page 전부 증발 pg_control "redo point는 여기야" 레코드 하나씩 순차 리플레이 ▶▶ FPI면 → 페이지 통째로 복원 아니면 → 증분 패치 적용 ✅ 커밋된 것은 전부 복원 end-of-recovery Checkpoint 후 서비스 오픈

9.3 Checkpoint — 복구 시작점을 앞으로 당기기

WAL만 믿고 영원히 리플레이할 수는 없습니다. WAL이 1TB 쌓였다면 복구에 몇 시간이 걸릴 테니까요. 그래서 주기적으로 "지금까지의 dirty page를 전부 데이터 파일에 내려쓰고, 복구 시작점(redo point)을 현재로 전진"시키는 작업이 필요합니다. 이것이 Checkpoint입니다.

Shared Buffers dirty dirty clean dirty clean dirty 데이터 파일 (heap/index) 💾 디스크 WAL 이전 redo ⏰ 트리거: checkpoint_timeout(5min) 또는 WAL 누적이 max_wal_size(1GB) 도달 dirty 전부 write+fsync (completion_target 0.9로 천천히 분산) 새 redo point! ♻️ 이전 WAL: 재활용/아카이브 가능 복구는 이제 새 redo point부터 시작 → 복구 시간 짧아짐 ✅
파라미터기본값의미
checkpoint_timeout5min시간 기반 Checkpoint 주기
max_wal_size1GBWAL 누적량 기반 트리거 (soft limit)
min_wal_size80MB재사용을 위해 유지할 최소 WAL
checkpoint_completion_target0.9 (v14+)I/O를 주기의 몇 %에 걸쳐 분산할지
⚠️
"checkpoints are occurring too frequently" 로그를 보셨다면

WAL이 max_wal_size에 너무 자주 도달한다는 뜻입니다. write-heavy 환경에서는 max_wal_size를 8GB~64GB까지 올리는 것이 일반적입니다. Checkpoint가 잦으면 다음 절의 FPI 때문에 WAL이 오히려 더 많이 생기는 역설도 발생합니다.

9.4 full_page_writes — 반쪽짜리 페이지 사고 방지

대부분의 디스크는 8KB 쓰기를 원자적으로 보장하지 않습니다. 쓰는 도중 전원이 나가면 "앞 4KB는 새 값, 뒤 4KB는 옛 값"인 torn page(찢어진 페이지)가 남을 수 있죠. WAL 레코드는 "이 페이지의 이 위치를 고쳐라"라는 증분 패치라서, 찢어진 페이지 위에 패치를 얹으면 복구가 오히려 망가집니다.

해결책: Checkpoint 후 페이지가 처음 더럽혀질 때, 페이지 전체 이미지(FPI)를 WAL에 통째로 기록합니다. 복구 시 FPI로 페이지를 먼저 복원한 뒤 그 위에 증분 패치를 적용하면 안전합니다.

# postgresql.conf — 내구성 관련 핵심 설정
full_page_writes = on          # 기본값, 절대 끄지 말 것
wal_compression = on           # FPI 압축 → WAL 양 감소 (v15+: pglz/lz4/zstd)
wal_level = replica            # v10+ 기본. 복제·PITR에 필요

# 쓰기 많은 OLTP 권장 예시
max_wal_size = 16GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9

9.5 synchronous_commit — 안전과 속도의 다이얼

커밋 응답을 언제 돌려줄지 선택할 수 있습니다. 안전할수록 느리고, 빠를수록 위험합니다.

응답 시점손실 위험
on (기본)로컬 WAL flush 완료 (동기 복제 시 스탠바이까지)없음
remote_apply스탠바이가 리플레이까지 완료없음, 가장 느림
local로컬 WAL flush만로컬은 안전, 스탠바이 지연 가능
off즉시 (WAL flush는 비동기)커밋 후 최대 수백 ms 손실 가능
-- 손실이 허용되는 로그성 INSERT만 세션 단위로 빠르게
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO event_log SELECT * FROM staging;
COMMIT;   -- fsync를 기다리지 않고 즉시 응답
🚨
pg_wal/ 폭증의 3대 원인 — 디스크 풀은 PANIC 셧다운

미회수 replication slot — 소비자가 사라진 슬롯이 WAL 삭제를 막음 (최빈 원인, max_slot_wal_keep_size로 방어) ② archive_command 실패 — 실패한 WAL은 계속 보관·재시도됨 (pg_stat_archiver.failed_count 감시) ③ wal_keep_size 과다 — 슬롯을 쓴다면 0 유지. 자세한 진단은 10~11장에서 이어집니다.

✍️ 이해도 체크

COMMIT이 클라이언트에게 "성공"으로 응답되는 시점(기본 설정)에 대한 설명으로 옳은 것은?
✅ WAL-first 원칙입니다. 커밋의 내구성은 WAL fsync가 보장하며, heap 반영은 나중에 Checkpointer/BGWriter가 천천히 합니다. 크래시가 나도 WAL 리플레이로 재현되므로 안전합니다. heap까지 기다리면 커밋마다 랜덤 I/O가 발생해 성능이 무너집니다.
Checkpoint가 하는 일이 아닌 것은?
✅ dead tuple 회수는 VACUUM(8장)의 일입니다. Checkpoint는 "메모리의 dirty page를 디스크에 내려 복구 시작점을 전진"시키는 작업으로, MVCC 청소와는 전혀 다른 역할입니다. 이름이 비슷한 배경 작업들의 역할 구분이 운영의 기본기입니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch09_wal_checkpoint.md — WAL 레코드 구조, wal_level 상세, BGWriter 역할 분담, 워크로드별 튜닝 가이드와 진단 쿼리 모음이 담겨 있습니다.