WAL·Checkpoint 튜닝: write amplification, archive, replication slot
WAL 튜닝의 목표는 "WAL을 줄이는 것"이 아니다
PostgreSQL에서 WAL(Write-Ahead Log)은 성능을 잡아먹는 부가 기능이 아니라, 장애 복구와 복제와 PITR(Point-in-Time Recovery)을 가능하게 만드는 핵심 안전장치다. 데이터 파일을 매번 즉시 디스크에 완전히 반영하지 않아도 되는 이유는, 변경 내용이 먼저 WAL에 기록되고 flush되었기 때문이다. 장애가 나면 PostgreSQL은 마지막 checkpoint 이후의 WAL을 다시 적용해서 데이터 파일을 일관된 상태로 되돌린다.
그래서 운영자가 잡아야 하는 균형은 단순히 "WAL을 적게 쓰자"가 아니다. 실제 목표는 다음 네 가지를 동시에 맞추는 것이다.
- commit latency를 과도하게 늘리지 않는다.
- checkpoint가 만드는 디스크 write spike를 줄인다.
- crash recovery 시간이 감당 가능한 범위에 머무르게 한다.
- archive, standby, replication slot 때문에
pg_wal이 무한히 커지지 않게 한다.
이 장에서는 WAL과 checkpoint를 성능 튜닝 항목이 아니라 복구 가능성, 쓰기 증폭, 보관 정책, 복제 지연을 함께 다루는 운영 시스템으로 본다.
WAL과 checkpoint가 하는 일
WAL의 기본 규칙은 간단하다. 데이터 페이지를 디스크에 쓰기 전에, 그 변경을 재현할 수 있는 WAL record가 먼저 안전한 저장소에 기록되어야 한다. 이 규칙 덕분에 commit 시점마다 모든 변경 페이지를 데이터 파일에 flush하지 않아도 된다. WAL은 대체로 순차 쓰기라서, 랜덤한 데이터 페이지를 매번 flush하는 것보다 비용이 낮다.
Checkpoint는 WAL replay의 시작점을 줄이는 장치다. checkpoint가 수행되면 PostgreSQL은 그 시점 이전 변경이 반영된 dirty page를 데이터 파일에 flush하고, WAL에 checkpoint record를 남긴다. 이후 crash recovery는 가장 최근 checkpoint의 redo record부터 WAL을 재생하면 된다.
즉 WAL과 checkpoint의 관계는 다음처럼 볼 수 있다.
Checkpoint 간격을 짧게 하면 recovery 때 재생할 WAL은 줄어든다. 하지만 checkpoint가 너무 자주 발생하면 dirty page flush가 잦아지고, 특히 full_page_writes가 켜져 있을 때 checkpoint 이후 처음 변경되는 page 전체가 WAL에 기록되어 WAL volume이 늘어난다. 반대로 checkpoint 간격을 길게 하면 평상시 I/O spike와 WAL 증폭은 줄어들 수 있지만, crash recovery 시간이 길어지고 pg_wal에 필요한 여유 공간도 커진다.
쓰기 증폭은 어디서 생기는가
쓰기 증폭(write amplification)은 애플리케이션이 논리적으로 한 번 변경한 데이터보다 훨씬 많은 바이트가 디스크에 쓰이는 현상이다. PostgreSQL 쓰기 경로에서는 다음 지점에서 증폭이 생긴다.
| 지점 | 증폭이 생기는 이유 | 운영상 증상 |
|---|---|---|
| WAL record | 모든 변경을 redo 가능한 형태로 기록 | pg_stat_wal.wal_bytes 증가 |
full_page_writes | checkpoint 이후 page의 첫 변경 때 page image 기록 | checkpoint 직후 WAL 증가율 상승 |
| Checkpoint flush | dirty page를 데이터 파일에 반영 | 디스크 write spike, fsync 지연 |
| Background/backend write | bgwriter가 못 따라가면 backend가 직접 write | 쿼리 latency 흔들림 |
| Archive/replication retention | WAL segment 제거가 지연됨 | pg_wal 디스크 사용량 증가 |
특히 checkpoint가 잦으면 full_page_writes의 비용이 커진다. PostgreSQL은 torn page 위험을 줄이기 위해 checkpoint 이후 처음 변경되는 page의 전체 이미지를 WAL에 남길 수 있다. 이 기능은 기본적으로 켜져 있으며, 일반적인 운영에서는 끄지 않는 것이 맞다. 성능을 위해 안전장치를 끄는 대신, checkpoint가 불필요하게 자주 발생하지 않도록 max_wal_size, checkpoint_timeout, checkpoint_completion_target을 조정하는 편이 안전하다.
핵심 파라미터: timeout, size, completion target
Checkpoint는 주로 두 조건 중 하나로 시작된다.
checkpoint_timeout: 마지막 checkpoint 이후 시간이 충분히 지남max_wal_size: WAL 사용량이 설정한 크기에 가까워짐
기본값은 환경과 버전에 따라 확인해야 하지만, 공식 문서 기준으로 checkpoint_timeout은 5분, max_wal_size는 1GB가 기본 출발점이다. 쓰기가 많은 운영 DB에서는 이 값이 너무 작아서 size-triggered checkpoint가 자주 발생할 수 있다.
운영자가 먼저 봐야 할 질문은 이것이다.
checkpoint가 시간 때문에 발생하는가, WAL 압박 때문에 강제로 발생하는가?
PostgreSQL 17/18 계열에서는 pg_stat_checkpointer에서 num_timed, num_requested, write_time, sync_time, buffers_written 같은 checkpointer 통계를 볼 수 있다. 이전 버전에서는 많은 항목이 pg_stat_bgwriter에 있었다. 버전별 view 이름은 실제 운영 버전에서 \d pg_stat_checkpointer 또는 공식 문서로 확인해야 한다.
SELECT
num_timed,
num_requested,
buffers_written,
write_time,
sync_time,
stats_reset
FROM pg_stat_checkpointer;해석은 간단하다.
num_requested가 계속 빠르게 증가한다면max_wal_size가 workload 대비 작을 가능성이 높다.sync_time이 길다면 storage의 fsync 지연 또는 checkpoint 순간 I/O 포화가 의심된다.write_time이 checkpoint interval 대부분에 고르게 분산되어 있으면 spike가 상대적으로 완화된 상태다.- checkpoint 로그가 너무 자주 나온다면
checkpoint_warning메시지도 함께 확인한다.
checkpoint_completion_target은 checkpoint write를 checkpoint interval의 어느 정도까지 분산할지 정한다. 예를 들어 0.9는 다음 checkpoint 예상 시점의 90% 구간에 걸쳐 dirty page write를 퍼뜨리려는 설정이다. 너무 낮으면 짧은 시간에 많이 쓰고, 너무 높으면 다음 checkpoint와 겹칠 여지가 생긴다. 실무에서는 write spike를 줄이기 위해 0.9 부근에서 시작하는 경우가 많지만, 정답은 storage latency와 WAL 생성률에 따라 달라진다.
튜닝의 기본 순서
WAL/checkpoint 튜닝은 파라미터를 바로 바꾸는 작업이 아니라 관측 순서가 중요하다.
- 현재 WAL 생성률을 본다.
pg_stat_wal.wal_bytes증가량을 시간 단위로 측정한다. - checkpoint 원인을 구분한다. timed checkpoint가 많은지, requested checkpoint가 많은지 본다.
- 디스크 쓰기 지연을 본다. checkpointer의 write/sync time, OS disk latency, cloud volume burst credit을 같이 본다.
- archive와 replication slot을 확인한다. WAL이 제거되지 않는 이유가 checkpoint가 아니라 archive 실패나 slot lag일 수 있다.
- 한 번에 하나씩 바꾼다.
max_wal_size를 키웠다면 recovery time과 disk headroom도 같이 재산정한다.
예를 들어 num_requested가 급증하고, checkpoint 로그가 몇 분 간격으로 반복되며, checkpoint 직후 애플리케이션 write latency가 튄다면 우선 max_wal_size를 늘리는 쪽을 검토한다. 하지만 pg_wal 파일이 이미 디스크를 압박하고 있는데 무작정 max_wal_size만 늘리면 더 큰 장애로 이어질 수 있다. 먼저 archive 실패, replication slot 지연, standby 중단 여부를 확인해야 한다.
WAL archive는 checkpoint 튜닝과 다른 문제다
WAL archiving은 PITR과 standby bootstrap의 기반이다. archive_mode = on이고 archive_command 또는 archive_library가 설정되면, PostgreSQL은 완료된 WAL segment를 외부 저장소로 복사한 뒤에야 해당 segment를 안전하게 재활용할 수 있다.
여기서 중요한 점은 archive는 성공/실패가 명확해야 한다는 것이다. archive_command는 복사가 성공했을 때만 exit code 0을 반환해야 한다. 실패했는데도 0을 반환하면 PostgreSQL은 WAL이 안전하게 보관됐다고 믿고 제거할 수 있고, PITR 사슬이 끊어진다. 반대로 계속 실패하면 WAL segment가 pg_wal에 쌓여 디스크를 채운다.
운영 점검 쿼리는 다음처럼 시작할 수 있다.
SELECT
archived_count,
last_archived_wal,
last_archived_time,
failed_count,
last_failed_wal,
last_failed_time,
stats_reset
FROM pg_stat_archiver;그리고 archive 경로에서 다음을 확인한다.
- archive 저장소가 primary 로컬 디스크 하나에만 있지 않은가?
- 같은 archive prefix에 여러 cluster가 쓰고 있지 않은가?
archive_command가 기존 파일을 덮어쓰지 않도록 되어 있는가?- archive 저장소 권한이 WAL에 담긴 민감 데이터를 보호할 만큼 제한적인가?
- base backup 시작 시점부터 필요한 WAL segment가 연속적으로 보존되는가?
Checkpoint를 조정한다고 archive 지연이 해결되지는 않는다. archive_timeout도 checkpoint 간격을 조정하는 파라미터가 아니다. 파일 기반 log shipping에서 WAL segment 전환을 더 자주 일으켜 데이터 손실 창을 줄이고 싶을 때 쓰는 별도 장치다. 너무 낮게 잡으면 거의 비어 있는 segment를 자주 archive해서 저장소와 네트워크 비용이 늘어난다.
Replication slot은 편리하지만 디스크를 인질로 잡을 수 있다
Replication slot은 standby나 logical decoding consumer가 아직 받지 못한 WAL을 primary가 제거하지 않도록 보장한다. 이 보장은 운영상 매우 유용하다. standby가 잠시 끊겨도 필요한 WAL이 남아 있으면 다시 이어받을 수 있고, CDC consumer도 자신의 위치부터 재개할 수 있다.
하지만 slot consumer가 멈춘 채 오래 방치되면 문제가 된다. PostgreSQL은 slot의 restart_lsn 이후 WAL을 보존하려고 하므로, pg_wal이 계속 커질 수 있다. 기본적으로 max_slot_wal_keep_size = -1이면 slot이 보존할 수 있는 WAL 크기에 제한이 없다. CDC 장애 하나가 primary 디스크 full로 이어질 수 있는 이유다.
Slot 상태는 pg_replication_slots에서 봐야 한다.
SELECT
slot_name,
slot_type,
active,
inactive_since,
restart_lsn,
confirmed_flush_lsn,
wal_status,
safe_wal_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
WHERE restart_lsn IS NOT NULL
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;운영 판단은 다음 기준으로 나눈다.
| 상태 | 의미 | 대응 |
|---|---|---|
active = true지만 retained WAL 증가 | consumer가 따라가지 못함 | consumer 처리량, network, downstream sink 확인 |
active = false와 오래된 inactive_since | consumer 중단 또는 폐기 가능성 | 소유자 확인 후 재기동/삭제 판단 |
wal_status = extended | slot 때문에 max_wal_size 이상 보존 | 디스크 headroom과 lag 원인 확인 |
safe_wal_size가 작아짐 | 제한 도달 시 slot 손실 위험 | consumer 복구 또는 max_slot_wal_keep_size 재검토 |
wal_status = lost | 필요한 WAL이 사라져 slot 사용 불가 | 재동기화 또는 재구축 필요 |
Slot을 삭제하는 명령은 간단하지만, 영향은 크다.
SELECT pg_drop_replication_slot('slot_name');이 명령은 해당 consumer가 이어받을 위치를 잃게 만들 수 있다. 특히 logical replication이나 CDC에서는 downstream 재동기화가 필요할 수 있다. 따라서 slot 삭제는 "디스크를 살리기 위한 긴급 조치"일 수 있지만, 항상 소유자, 재동기화 절차, 데이터 중복/누락 처리 방식을 확인한 뒤 수행해야 한다.
관측해야 할 지표 묶음
WAL/checkpoint 문제는 하나의 지표로 판단하기 어렵다. 다음 묶음을 같이 봐야 원인을 잘못 짚지 않는다.
| 영역 | 지표/뷰 | 봐야 할 질문 |
|---|---|---|
| WAL 생성량 | pg_stat_wal.wal_bytes, wal_records, wal_fpi | 쓰기 workload가 늘었는가, full page image가 많은가 |
| Checkpoint | pg_stat_checkpointer, checkpoint 로그 | 시간 기반인가, WAL size 기반인가, sync가 느린가 |
| Background writer | pg_stat_bgwriter | backend가 직접 write하는 비율이 높은가 |
| Archive | pg_stat_archiver, archive 저장소 | archive 실패로 WAL 제거가 막혔는가 |
| Replication | pg_stat_replication, pg_replication_slots | standby/slot lag가 WAL 보존을 강제하는가 |
| Disk | filesystem usage, IOPS, latency | pg_wal과 data volume 여유가 충분한가 |
| Recovery | 복구 리허설 시간 | 늘린 WAL 여유가 RTO를 깨지 않는가 |
PostgreSQL 16 이후에는 pg_stat_io도 함께 보면 backend, checkpointer, background writer별 I/O 패턴을 더 세밀하게 볼 수 있다. track_wal_io_timing을 켜면 WAL write/fsync timing을 관측할 수 있지만, 타이밍 수집에는 약간의 overhead가 있을 수 있으므로 운영 환경에서는 비용을 확인하고 적용한다.
실전 시나리오: 쓰기 지연이 10분마다 튄다
상황을 하나 가정해보자. 애플리케이션 write latency가 약 10분마다 튄다. DB CPU는 여유가 있는데 disk write latency가 순간적으로 올라간다. 이때 바로 shared_buffers나 query tuning으로 뛰어가면 원인을 놓칠 수 있다.
조사 순서는 다음이 안전하다.
- PostgreSQL 로그에서 checkpoint 완료 메시지와 지연 시점이 겹치는지 본다.
pg_stat_checkpointer에서num_requested증가가 많은지 본다.pg_stat_wal에서 WAL 생성률과wal_fpi비율을 본다.pg_stat_archiver와pg_replication_slots로 WAL 보존 지연이 있는지 배제한다.- OS/cloud volume 지표에서 fsync latency, burst credit, queue depth를 확인한다.
만약 num_requested가 많고 archive/slot 문제는 없다면 max_wal_size가 너무 작아 checkpoint가 WAL 압박으로 강제되는 상황일 수 있다. 이 경우 max_wal_size를 늘리고 checkpoint_completion_target을 0.9 근처로 조정해 write를 분산하는 방안을 검토한다. 단, 변경 전에 다음 질문에 답해야 한다.
- crash recovery 때 더 많은 WAL을 replay해도 RTO를 만족하는가?
pg_walfilesystem에 충분한 headroom이 있는가?- archive 저장소와 네트워크가 늘어난 WAL 보관을 감당하는가?
- standby나 CDC consumer가 지연될 때
max_slot_wal_keep_size로 피해 범위를 제한할 것인가?
운영 체크리스트
WAL/checkpoint 운영에서 주기적으로 확인할 항목은 다음과 같다.
pg_wal이 별도 volume이라면 사용률과 inode/공간 알림이 있는가?max_wal_size는 정상 쓰기 피크를 버틸 만큼 크고, recovery time을 과도하게 늘리지 않는가?- checkpoint가 대부분 timed인지, requested가 비정상적으로 많은지 추적하는가?
- checkpoint 로그(
log_checkpoints)를 운영 분석에 사용할 수 있는가? archive_command는 실패 시 반드시 non-zero를 반환하는가?- archive 저장소에 base backup 이후 WAL이 연속적으로 보존되는지 복구 리허설로 검증했는가?
- replication slot별 소유자, 용도, 삭제 기준, 재동기화 방법이 문서화되어 있는가?
max_slot_wal_keep_size와 alert가 설정되어 slot 하나가 primary disk full을 만들지 않게 되어 있는가?- standby lag와 CDC lag를 단순 지연이 아니라 WAL retention risk로도 보고 있는가?
핵심 정리
WAL과 checkpoint 튜닝은 성능 파라미터 조정이면서 동시에 복구 전략 조정이다. max_wal_size를 늘리면 checkpoint spike는 줄어들 수 있지만 crash recovery와 디스크 headroom 문제가 커진다. checkpoint를 자주 하면 recovery는 빨라질 수 있지만 full_page_writes 때문에 WAL volume과 write amplification이 늘 수 있다. Archive와 replication slot은 WAL 보존을 강제하므로, checkpoint 설정과 별도로 감시해야 한다.
좋은 운영 기준은 "WAL이 얼마나 큰가"가 아니라 다음 문장에 답할 수 있는 상태다.
정상 피크, archive 지연, standby 지연, CDC 중단, crash recovery 상황에서 WAL이 어디까지 쌓이고, 언제 알림이 울리며, 어떤 순서로 복구하거나 잘라낼지 알고 있는가?
이 질문에 답할 수 있으면 WAL/checkpoint는 더 이상 불투명한 내부 동작이 아니라, 장애를 견디는 데이터베이스 운영의 통제 가능한 축이 된다.
References
- PostgreSQL Documentation: Write-Ahead Logging (WAL)
- PostgreSQL Documentation: WAL Configuration
- PostgreSQL Documentation: Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL Documentation: Log-Shipping Standby Servers
- PostgreSQL Documentation: The Cumulative Statistics System
- PostgreSQL Documentation: pg_replication_slots
- PostgreSQL Documentation: Replication Configuration