LLM WikiAccess-protected knowledge portal
← 스터디 홈
168편 · 약 18분

WAL(Write-Ahead Logging): 데이터베이스 내구성의 근본 원리와 운영 기준

PostgreSQL의 복제, MySQL Binlog, Debezium CDC, Kafka Connect, PITR 백업 — 이 모든 기능의 아래에는 WAL(Write-Ahead Log)이 있다. WAL을 이해하면 데이터베이스 내구성, 복제, 그리고 수많은 운영 장애의 원인을 한 번에 파악할 수 있다.

왜 WAL이 필요한가

데이터베이스가 변경 데이터를 디스크에 직접 쓰면 어떤 문제가 생길까. B-Tree 페이지를 업데이트하는 중에 전원이 나가면 페이지의 절반만 기록된 부분 쓰기(Partial Write) 상태가 된다. 이 페이지는 손상됐고, 원래 값이 무엇인지 알 방법이 없다.

WAL은 이 문제를 해결하는 가장 검증된 방법이다. 핵심 규칙은 단순하다.

실제 데이터 페이지를 바꾸기 전에, 반드시 변경 내역을 로그에 먼저 기록하라

이 로그가 WAL이다. 크래시가 발생하면 WAL을 순서대로 재생(Replay)해 데이터베이스를 일관된 상태로 복구한다. 이론적 기반은 1992년 IBM이 발표한 ARIES 알고리즘이다.

WAL의 기본 구조

로그 시퀀스 번호(LSN)

WAL의 각 레코드는 단조 증가하는 위치 식별자를 갖는다. PostgreSQL에서는 LSN(Log Sequence Number)이라 부른다. MySQL InnoDB에서도 같은 이름을 쓴다. WAL 스트림 안에서 해당 레코드가 몇 바이트 위치에 있는지를 나타낸다.

LSN은 세 곳에서 쓰인다:

  • 크래시 복구: "이 LSN까지는 디스크에 반영됐다"는 기준점
  • 스트리밍 복제: "팔로워가 어디까지 받았다"는 위치 추적
  • PITR: 복구 목표 시점 지정 (recovery_target_lsn)

물리적 WAL vs 논리적 WAL

WAL 레코드의 내용은 두 방식으로 기록된다.

물리적 WAL: 어느 파일, 어느 페이지, 어느 오프셋에 어떤 바이트가 기록됐는지 저장한다. PostgreSQL의 힙 수정 레코드가 이 방식이다. 재생이 결정론적이고 빠르다. 단점은 로그 크기가 크다.

논리적 WAL: 어떤 트랜잭션이 어떤 행을 어떻게 바꿨는지 기록한다. PostgreSQL의 논리적 복제(Logical Replication)와 Debezium CDC가 이 방식이다. 로그가 작고 해석 가능하다. 단점은 재생 시 수신 측의 스토리지 상태에 의존한다.

클라이언트 COMMIT
WAL 버퍼에 레코드 추가
WAL fsync
(디스크 flush)
클라이언트에 OK 응답
버퍼 캐시
(더티 페이지)
체크포인트 시
데이터 파일 flush
디스크(데이터 파일)
핵심 순서

WAL fsync 완료 → 커밋 확인 → (나중에) 데이터 페이지 flush
이 순서가 깨지면 내구성 보장이 무너진다.

WAL 쓰기 경로: 트랜잭션 커밋에서 디스크 반영까지

PostgreSQL의 WAL 구현

WAL 디렉터리와 세그먼트

PostgreSQL의 WAL은 $PGDATA/pg_wal/ 디렉터리에 고정 크기(기본 16MB) 세그먼트 파일로 저장된다. 파일명은 16진수 LSN 범위를 인코딩한다.

$PGDATA/pg_wal/
  000000010000000000000001   # 첫 번째 세그먼트 (16MB)
  000000010000000000000002
  000000010000000000000003
  archive_status/            # 아카이브 완료 파일 추적

파일명 구조: 타임라인(8자리) + 세그먼트 번호 상위(8자리) + 세그먼트 번호 하위(8자리).

쓰기 경로 상세

트랜잭션이 COMMIT하면 다음 순서로 진행한다.

  1. 변경 작업이 버퍼 캐시의 페이지를 수정한다
  2. 변경 내역이 WAL 버퍼에 기록된다 (아직 디스크가 아님)
  3. COMMIT 명령이 들어온다
  4. WAL 버퍼를 디스크로 fsync한다 ← 이 단계가 핵심
  5. 클라이언트에 성공 응답을 보낸다
  6. 이후 체크포인트 타이밍에 실제 데이터 페이지가 디스크에 기록된다

4단계(fsync)가 완료되기 전에는 COMMIT 성공을 알리지 않는다. 이것이 ACID의 내구성(Durability)을 보장하는 메커니즘이다.

체크포인트

체크포인트는 버퍼 캐시의 더티 페이지를 데이터 파일에 flush하는 이벤트다. 체크포인트가 완료되면 그 이전 LSN을 커버하는 WAL 세그먼트는 복구에 필요 없으므로 삭제하거나 아카이브할 수 있다.

체크포인트는 두 조건 중 먼저 도달하는 쪽에서 트리거된다:

  • checkpoint_timeout (기본값: 5분)
  • max_wal_size (기본값: 1GB) 초과

체크포인트가 너무 잦으면: 더티 페이지를 자주 flush해 I/O 부하가 높아진다.

체크포인트가 너무 드물면: 크래시 복구 시간이 늘어난다. 마지막 체크포인트 이후의 WAL을 전부 재생해야 하기 때문이다.

checkpoint_completion_target = 0.9(기본값)를 설정하면 체크포인트 사이 간격의 90% 시간에 걸쳐 flush를 분산시켜 I/O 폭풍을 완화한다.

크래시 복구 절차

서버가 재시작하면 PostgreSQL은 다음을 수행한다.

  1. pg_control 파일에서 마지막 체크포인트 LSN을 읽는다
  2. 해당 LSN부터 WAL 끝까지 레코드를 순서대로 재생한다
  3. 각 레코드가 해당 페이지를 체크포인트 이후 상태로 복원한다
  4. WAL 끝에 도달하면 정상 운영으로 전환한다

이 과정에서 완료되지 않은 트랜잭션(WAL에 COMMIT 레코드가 없는 것)은 자동으로 롤백된다. 사람이 개입할 필요가 없다.

MySQL InnoDB의 WAL: Redo Log

InnoDB Redo Log와 Binary Log의 분리

MySQL은 두 가지 로그가 있다.

InnoDB Redo Log: 스토리지 엔진 수준의 물리적 WAL. 크래시 복구 담당. 파일: 전통적으로 ib_logfile0, ib_logfile1. MySQL 8.4에서 동적 크기 Redo Log로 전환됐다.

Binary Log (Binlog): MySQL 서버 수준의 논리적 로그. 복제와 PITR 담당. Redo Log와 별개로 존재한다.

과거에는 두 로그를 동기화하는 데 XA 2PC(2-Phase Commit)가 필요했다. Redo Log에 먼저 prepare하고, Binlog에 쓴 뒤, Redo Log에 commit하는 세 단계였다. MariaDB 12.3(챕터 47)은 InnoDB 기반 Binlog를 구현해 이 2PC를 없앴다.

InnoDB Redo Log 순환 구조

전통적인 InnoDB Redo Log는 고정 크기의 순환 버퍼다. 가장 오래된 레코드가 덮어쓰일 수 있는데, 그 레코드가 커버하는 데이터 페이지가 이미 체크포인트를 통해 flush됐을 때만 덮어쓴다. Redo Log가 꽉 차면 체크포인트를 강제 실행한다. 이 강제 체크포인트가 갑작스러운 I/O 스파이크의 원인이다.

WAL과 복제

WAL은 복제의 기초다.

PostgreSQL 물리적 스트리밍 복제

Primary가 생성하는 WAL 레코드 스트림을 Standby가 TCP로 수신해 재생한다. WAL 수신자와 WAL 발신자 프로세스가 각각 Standby와 Primary에서 동작한다.

동기 복제 옵션(synchronous_standby_names): Primary는 최소 하나의 Standby가 WAL 레코드를 flush할 때까지 COMMIT을 완료하지 않는다. 장애 시 데이터 손실을 방지하지만 지연시간이 늘어난다.

PostgreSQL 19 Beta의 WAIT FOR LSN(챕터 164)은 이 개념을 확장해 Read-Your-Writes 일관성을 비동기 복제본에서도 제공한다.

논리적 복제와 CDC

PostgreSQL 논리적 복제는 WAL을 해석해 행 수준 변경 이벤트를 스트리밍한다. 이 기능이 Debezium(챕터 54, 78)의 기반이다. Debezium은 pgoutput(PostgreSQL 기본 논리적 복제 플러그인)을 통해 WAL 스트림을 소비해 Kafka에 CDC 이벤트를 발행한다.

복제 유형WAL 방식용도
물리적 스트리밍 복제전체 WAL 레코드 전송HA, 읽기 분산
논리적 복제행 수준 변경 이벤트선택적 테이블 복제, 버전 업그레이드
Debezium CDC논리적 WAL 슬롯 소비이벤트 스트리밍, 데이터 파이프라인
PITR 아카이브WAL 세그먼트 파일특정 시점 복구

WAL 슬롯: 흔한 운영 장애 원인

PostgreSQL 논리적 복제 슬롯(Replication Slot)은 소비자(Debezium, 논리 복제 구독자 등)가 WAL을 다 읽을 때까지 해당 WAL 세그먼트를 보존한다. 이 설계가 문제를 일으킨다.

비활성 복제 슬롯: Debezium 인스턴스가 중단되거나 삭제됐는데 슬롯이 남아 있으면, PostgreSQL은 그 슬롯이 요구하는 LSN 이후의 WAL을 영구 보존한다. pg_wal 디렉터리가 무한정 커져 디스크를 가득 채울 수 있다.

-- 복제 슬롯 상태 확인 (PostgreSQL)
SELECT slot_name, 
       active, 
       restart_lsn,
       pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
       ) AS wal_lag
FROM pg_replication_slots
ORDER BY restart_lsn;

-- 비활성 슬롯 삭제 (주의: 소비자가 다시 연결하면 처음부터 재스캔 필요)
SELECT pg_drop_replication_slot('slot_name');

max_slot_wal_keep_size (PostgreSQL 13+): 복제 슬롯이 보존할 WAL 최대 크기. 이 값을 초과하면 슬롯이 무효화된다. 디스크 보호 최후 수단이지만, 소비자가 재동기화해야 한다.

WAL 아카이빙과 PITR

archive_mode = onarchive_command 설정으로 완료된 WAL 세그먼트를 외부 스토리지로 복사한다. WAL-G나 pgBackRest가 이 아카이빙을 S3·GCS에 자동화한다.

PITR 복구 절차:

  1. 베이스 백업을 복구 환경에 복원한다
  2. recovery.conf(PG 11 이전) 또는 postgresql.conf의 복구 파라미터를 설정한다
  3. PostgreSQL을 시작하면 아카이브에서 WAL을 읽어 목표 시점까지 재생한다
# postgresql.conf (PostgreSQL 12+)
restore_command = 'wal-g wal-fetch %f %p'
recovery_target_time = '2026-09-03 14:30:00 KST'
recovery_target_action = 'promote'

동기 커밋 설정

synchronous_commit 파라미터가 내구성과 성능을 결정한다.

설정값WAL flush 시점복제본 대기적합한 상황
on (기본)로컬 flush없음일반 OLTP
remote_write로컬 flush복제본 OS 버퍼 쓰기HA, 약한 내구성 보장
remote_apply로컬 flush복제본이 변경 적용읽기 일관성 필요
local로컬 flush없음복제 지연 허용
offflush 생략없음대량 로드, 분석 배치

off는 크래시 시 최근 커밋(수 밀리초 분량)을 잃을 수 있다. 금융 트랜잭션에는 절대 쓰지 않는다.

흔한 운영 장애 패턴

pg_wal 디스크 풀: 비활성 복제 슬롯, 아카이빙 실패(archive_command 오류), 또는 max_wal_size 미설정이 원인이다. WAL 디렉터리 크기 경보를 max_wal_size의 150%에 설정한다.

체크포인트 I/O 폭풍: checkpoint_completion_target = 0.5(기본값이 낮은 환경)에서 체크포인트가 짧은 시간에 집중해 I/O가 몰린다. 0.9로 올린다.

복제 래그 증가: 쓰기 부하가 복제본 재생 속도를 초과하거나 네트워크 병목이 원인이다. WAL 송수신 프로세스의 상태를 pg_stat_replication으로 모니터링한다.

full_page_writes 오버헤드: 체크포인트 직후 첫 번째로 수정되는 페이지는 전체 페이지를 WAL에 기록한다. 부분 쓰기 오염 가능성에 대비하기 위해서다. SSD 환경에서는 이 위험이 줄어든다. 성능이 중요하면 full_page_writes = off를 검토하되, 반드시 상위 레이어에서 내구성을 보장해야 한다.

검증 체크리스트

  • [ ] pg_wal 디렉터리 크기 알림: max_wal_size × 1.5 초과 시 알림
  • [ ] 복제 슬롯 래그 모니터링 (pg_replication_slots.restart_lsn)
  • [ ] 비활성 복제 슬롯 자동 감지 정책 수립 (active = false 30분 초과 시 알림)
  • [ ] WAL 아카이빙 성공률 모니터링 (archive_command 실패 알림)
  • [ ] PITR 복구 정기 테스트 (분기 1회 이상, 실제 데이터로 검증)
  • [ ] synchronous_commit 설정이 내구성 요구사항과 일치하는지 확인
  • [ ] checkpoint_completion_target = 0.9 설정 확인
  • [ ] MySQL: Redo Log 크기(innodb_redo_log_capacity) 가 최대 트랜잭션 크기의 10배 이상인지 확인

Open Questions

  • PostgreSQL 19 WAIT FOR LSN이 Patroni(챕터 53) 같은 HA 레이어와 어떻게 통합되는지는 검증 사례가 많지 않다.
  • 분산 데이터베이스(CockroachDB, TiDB)에서 WAL에 해당하는 Raft 로그와 물리적 Redo Log의 경계가 구현마다 다르다.

References

  • Mohan et al., "ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging", ACM TODS 1992
  • PostgreSQL Documentation: Write-Ahead Logging — https://www.postgresql.org/docs/current/wal-intro.html
  • PostgreSQL Documentation: WAL Configuration — https://www.postgresql.org/docs/current/wal-configuration.html
  • MySQL Reference Manual: InnoDB Redo Log — https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html
  • Debezium Documentation: PostgreSQL Connector — https://debezium.io/documentation/reference/stable/connectors/postgresql.html
  • CMU 15-445/645 Database Systems, Lecture 20: Database Recovery (Andy Pavlo) — https://15445.courses.cs.cmu.edu/fall2023/
  • Brandur Leach, "Using WAL (Write-Ahead Logging) to Reduce Downtime" — https://brandur.org/postgres-default