9장. WAL과 Checkpoint — 절대 잃어버리지 않는 기술
전원이 갑자기 나가도 커밋된 데이터는 살아남습니다. 그 비밀은 "데이터보다 일기를 먼저 쓴다"는 단순한 규칙 — WAL(Write-Ahead Log)에 있습니다.
① 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 한 번의 여정을 따라가 봅시다.
클라이언트가 COMMIT을 보냅니다. Backend는 Shared Buffers의 페이지를 수정합니다 — 아직 메모리에서만 바뀐 상태(dirty page)입니다.
동시에 "무엇을 바꿨는지" 설명하는 WAL 레코드를 WAL Buffer에 append하고, 위치 번호인 LSN(Log Sequence Number)을 할당받습니다.
커밋 시점에 WAL Buffer가 디스크의 pg_wal/로 write + fsync됩니다. 이 fsync가 커밋 지연의 대부분을 차지하는 진짜 디스크 쓰기입니다.
WAL이 디스크에 안착한 순간, 클라이언트에게 "커밋 성공"이 응답됩니다. heap 데이터 파일은 아직 안 썼지만 괜찮습니다 — 크래시가 나도 WAL로 재현할 수 있으니까요.
dirty page는 나중에 Checkpointer나 Background Writer가 천천히 데이터 파일로 내려보냅니다. "일기 먼저, 본문은 나중에" — 이것이 Write-Ahead Logging입니다.
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부터 다시 읽으며 재생합니다.
💥 전원이 갑자기 끊겼습니다. 메모리에 있던 dirty page는 모두 사라졌지만, 커밋 시점에 fsync된 WAL은 디스크에 온전히 살아 있습니다.
재시작하면 PostgreSQL은 먼저 pg_control 파일을 읽어 마지막 Checkpoint의 redo point를 확인합니다. "여기부터 다시 재생하면 된다"는 북마크입니다.
redo point부터 WAL 레코드를 순차적으로 리플레이합니다. Full Page Image(FPI)면 페이지를 통째로 복원하고, 아니면 그 위에 증분 패치를 적용합니다.
WAL 끝에 도달하면 커밋된 변경은 전부 복원된 상태! end-of-recovery Checkpoint를 찍고 정상 서비스를 시작합니다. 커밋된 데이터는 단 하나도 잃지 않았습니다.
9.3 Checkpoint — 복구 시작점을 앞으로 당기기
WAL만 믿고 영원히 리플레이할 수는 없습니다. WAL이 1TB 쌓였다면 복구에 몇 시간이 걸릴 테니까요. 그래서 주기적으로 "지금까지의 dirty page를 전부 데이터 파일에 내려쓰고, 복구 시작점(redo point)을 현재로 전진"시키는 작업이 필요합니다. 이것이 Checkpoint입니다.
Checkpoint는 checkpoint_timeout(기본 5분) 경과 또는 WAL 누적이 max_wal_size(기본 1GB)에 도달하면 시작됩니다.
그 시점의 dirty page 전부를 데이터 파일로 write + fsync합니다. I/O 폭탄이 되지 않도록 checkpoint_completion_target(0.9) 비율로 주기 전체에 걸쳐 천천히 분산합니다.
flush가 끝나면 pg_control의 redo point를 현재 LSN으로 전진시킵니다. "이 이전 것들은 이미 데이터 파일에 다 반영됐다"는 선언입니다.
이전 redo point 이전의 WAL은 크래시 복구에 더 이상 필요 없으므로 재활용·아카이브 대상이 됩니다. 복구 시작점이 가까워졌으니 크래시 복구도 빨라집니다.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
checkpoint_timeout | 5min | 시간 기반 Checkpoint 주기 |
max_wal_size | 1GB | WAL 누적량 기반 트리거 (soft limit) |
min_wal_size | 80MB | 재사용을 위해 유지할 최소 WAL |
checkpoint_completion_target | 0.9 (v14+) | I/O를 주기의 몇 %에 걸쳐 분산할지 |
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를 기다리지 않고 즉시 응답
① 미회수 replication slot — 소비자가 사라진 슬롯이 WAL 삭제를 막음 (최빈 원인, max_slot_wal_keep_size로 방어) ② archive_command 실패 — 실패한 WAL은 계속 보관·재시도됨 (pg_stat_archiver.failed_count 감시) ③ wal_keep_size 과다 — 슬롯을 쓴다면 0 유지. 자세한 진단은 10~11장에서 이어집니다.
✍️ 이해도 체크
이 장의 원문 문서: chapters/ch09_wal_checkpoint.md — WAL 레코드 구조, wal_level 상세, BGWriter 역할 분담, 워크로드별 튜닝 가이드와 진단 쿼리 모음이 담겨 있습니다.