- Published on
Write-Ahead Logging 완전 가이드 2025: ARIES 복구, PostgreSQL WAL, InnoDB Redo Log, 체크포인트, 내결함성 심층 분석
- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며: 데이터베이스가 절대 잊지 않는 비결
한 가지 질문으로 시작하자
당신의 은행 앱에서 10만 원을 송금한다. "송금 완료"가 화면에 뜬 순간, 데이터베이스 서버가 전원 차단된다. 서버가 재부팅된 후, 그 10만 원은 어떻게 될까?
답: 사라지지 않는다. 왜냐하면 데이터베이스는 "내가 커밋했다고 말한 순간" 을 절대 잊지 않기 때문이다.
그 마법의 정체가 바로 Write-Ahead Logging (WAL) 이다.
단순한 아이디어, 엄청난 결과
WAL의 핵심 원칙은 단 한 줄로 요약된다:
"데이터 파일에 변경을 적용하기 전에, 그 변경을 로그에 먼저 기록한다."
이 단순한 규칙에서 다음이 파생된다:
- Durability: 커밋된 트랜잭션은 장애 후에도 복구 가능.
- Atomicity: 트랜잭션은 전체가 적용되거나 전혀 적용되지 않음.
- 성능: 랜덤 I/O가 아닌 순차 I/O로 쓰기 집중.
- 복구: 장애 후 로그를 재생해서 상태 복원.
이 글에서는 WAL이 왜 필요한지, 어떻게 동작하는지, 그리고 PostgreSQL과 InnoDB에서 실제로 어떻게 구현되었는지 깊이 들여다본다.
1. WAL이 없던 시절: Shadow Paging
초기 접근법
WAL이 표준화되기 전, 데이터베이스는 Shadow Paging 같은 기법을 썼다:
- 페이지를 수정할 때 원본을 그대로 두고 새 페이지를 할당.
- 새 페이지에 변경 내용 쓰기.
- 트랜잭션 커밋 시 페이지 테이블을 원자적으로 교체.
- 장애 시 그냥 구 페이지 테이블로 되돌리면 됨.
왜 실패했는가?
Shadow Paging은 개념적으로 우아하지만 치명적 단점이 있었다:
- 페이지 단편화: 매번 새 페이지를 할당하니 공간 관리 복잡.
- 클러스터링 파괴: 원본과 복사본이 물리적으로 멀리 떨어져 랜덤 I/O 증가.
- 동시성 어려움: 페이지 테이블 교체가 원자적이어야 함.
- 작은 변경도 전체 페이지 복사: 1바이트 수정에 8KB 쓰기.
System R(IBM, 1970년대)과 초기 DB들이 Shadow Paging을 시도했다가 결국 WAL로 돌아섰다.
WAL의 등장
1970년대 후반 IBM의 연구진이 WAL을 공식화했고, 1992년 Mohan 등의 ARIES 논문이 현대 WAL의 표준이 되었다. ARIES는 지금도 거의 모든 RDBMS의 근간이다.
2. WAL의 기본 원리
두 가지 규칙
WAL은 두 가지 단순한 규칙으로 정의된다:
규칙 1 (WAL Rule):
데이터 페이지를 디스크에 쓰기 전에, 해당 변경의 로그 레코드가 먼저 디스크에 flush 되어야 한다.
규칙 2 (Force-at-Commit):
트랜잭션이 커밋된 것으로 간주되기 전에, 그 트랜잭션의 모든 로그 레코드가 디스크에 flush 되어야 한다.
이 두 규칙만 지키면 어떤 장애 시점에서도 일관된 상태로 복구할 수 있다.
로그 레코드 구조
전형적인 WAL 레코드는 다음을 포함한다:
LogRecord {
LSN : 로그 시퀀스 번호 (단조 증가)
PrevLSN : 같은 트랜잭션의 이전 레코드 LSN
TransactionID: 트랜잭션 식별자
Type : UPDATE, INSERT, DELETE, COMMIT, ABORT, CHECKPOINT...
PageID : 영향받은 페이지
Before : 변경 전 이미지 (undo 정보)
After : 변경 후 이미지 (redo 정보)
}
LSN (Log Sequence Number) 이 핵심이다. LSN은 로그 파일 내 바이트 오프셋에 가까운 개념으로, 한 번 부여되면 절대 재사용되지 않는다.
페이지 LSN
각 데이터 페이지도 pageLSN 을 저장한다: 이 페이지에 가장 최근에 적용된 로그 레코드의 LSN. 복구 중에 "이 변경이 이미 적용됐나?"를 판단하는 데 쓰인다.
if pageLSN >= logRecord.LSN:
이미 적용됨 → 건너뛰기
else:
다시 적용 (redo)
3. ARIES: 현대 WAL의 표준
ARIES의 세 가지 원칙
1992년 IBM 연구소의 C. Mohan 등이 발표한 ARIES (Algorithms for Recovery and Isolation Exploiting Semantics) 는 세 가지 원칙을 중심으로 한다:
- Write-Ahead Logging: 위에서 설명한 두 규칙.
- Repeating History During Redo: 복구 시 모든 변경(커밋 여부와 무관)을 재적용.
- Logging Changes During Undo: 롤백도 로그 레코드로 기록해서 재귀적 복구 지원.
3단계 복구 프로토콜
장애 후 데이터베이스가 시작될 때, ARIES는 세 단계로 복구한다:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Analysis │ → │ Redo │ → │ Undo │
│ (분석 단계) │ │ (재실행) │ │ (취소) │
└──────────────┘ └──────────────┘ └──────────────┘
Phase 1: Analysis
목적: 장애 시점의 상태를 파악.
- 마지막 체크포인트부터 로그를 순방향 스캔.
- Dirty Page Table (DPT) 재구성: 장애 시점에 메모리에만 있던 페이지들.
- Transaction Table 재구성: 장애 시 진행 중이던 트랜잭션들.
- redoLSN 결정: 이 LSN부터 redo 시작.
Phase 2: Redo
목적: 모든 로그 레코드를 재적용해서 장애 직전 상태로 복원.
- redoLSN부터 로그 끝까지 순방향 스캔.
- 각 로그 레코드에 대해:
- 해당 페이지를 읽어옴.
if pageLSN < logRecord.LSN→ 변경 적용, pageLSN 갱신.- 이미 적용됐으면 건너뛰기.
- 커밋된 트랜잭션과 커밋되지 않은 트랜잭션 모두의 변경을 재적용.
"왜 커밋 안 된 것까지?" 이는 Phase 3에서 일관되게 undo하기 위해서다.
Phase 3: Undo
목적: 커밋되지 않은 트랜잭션들을 롤백.
- Transaction Table에서 loser(미커밋) 트랜잭션 목록 확인.
- 각 loser에 대해 역방향으로 로그 따라가며 원상복구.
- 각 undo 동작도 CLR (Compensation Log Record) 로 로그에 기록.
- 롤백 완료 시 ABORT 레코드 기록.
CLR: 롤백도 로그에 남긴다
CLR(Compensation Log Record)은 ARIES의 영리한 아이디어다. 언뜻 보면 "롤백인데 왜 또 로그에 쓰지?"라는 생각이 들지만:
- 재귀적 복구: 롤백 중에 또 장애가 발생해도 CLR을 보고 어디까지 롤백했는지 알 수 있다.
- 멱등성: CLR은 다시 실행해도 결과가 같다.
CLR에는 undoNextLSN 필드가 있어서, 다음에 undo할 LSN을 가리킨다. 롤백을 중간부터 이어갈 수 있다.
체크포인트 (Checkpoint)
로그가 무한히 커질 수 없으므로 주기적으로 체크포인트를 찍는다:
- 모든 dirty page를 디스크에 flush.
- 현재 활성 트랜잭션 목록과 DPT를 로그에 기록.
- 체크포인트 레코드 작성.
- 체크포인트 이전의 로그는 (대부분) 버려도 됨.
ARIES의 Fuzzy Checkpoint는 더 영리하다: dirty page flush를 강제하지 않고, 현재 상태의 스냅샷만 기록. 백그라운드에서 flush가 이뤄진다.
4. PostgreSQL WAL 심층 분석
WAL 파일 구조
PostgreSQL의 WAL은 pg_wal 디렉토리(이전에는 pg_xlog)에 저장된다:
$PGDATA/pg_wal/
├── 000000010000000000000001 (16MB)
├── 000000010000000000000002
├── 000000010000000000000003
├── archive_status/
└── ...
각 파일은 기본 16MB 이고, 이름은 TIMELINEID + LOGID + SEGMENT 형식이다. 파일 내부는 8KB 페이지로 나뉘어 있으며, 각 페이지에 여러 WAL 레코드가 담긴다.
주요 WAL 레코드 타입
| 타입 | 설명 |
|---|---|
XLOG_HEAP_INSERT | 새 튜플 삽입 |
XLOG_HEAP_UPDATE | 튜플 업데이트 (기존 튜플 xmax + 새 튜플) |
XLOG_HEAP_DELETE | 튜플 삭제 (xmax 설정) |
XLOG_BTREE_INSERT_LEAF | B-Tree 리프에 키 삽입 |
XLOG_XACT_COMMIT | 트랜잭션 커밋 |
XLOG_XACT_ABORT | 트랜잭션 롤백 |
XLOG_CHECKPOINT_ONLINE | 온라인 체크포인트 |
XLOG_CHECKPOINT_SHUTDOWN | 셧다운 체크포인트 |
Full-Page Write
PostgreSQL의 독특한 기능: Full-Page Write.
문제: 8KB 페이지가 디스크에 쓰이는 도중 장애가 나면(torn page), 디스크에는 일부만 쓰인 페이지가 남는다. OS/디스크 수준에선 4KB 원자성만 보장되기 때문이다.
해결: 체크포인트 직후 첫 번째 수정 때 전체 페이지를 WAL에 기록한다. 이후 수정은 작은 증분만 기록.
-- 기본 활성화 (절대 끄지 마세요)
show full_page_writes; -- on
이로 인해 WAL 크기가 2~3배 늘어나지만, torn page로 인한 데이터 손상을 막는다. 체크포인트 직후 쓰기 폭주(write storm)의 주요 원인이기도 하다.
fsync와 커밋
트랜잭션 커밋 시 PostgreSQL은:
- COMMIT 레코드를 WAL 버퍼에 추가.
- WAL 버퍼를 디스크로 fsync.
- 클라이언트에 성공 응답.
fsync = off로 설정하면 빠르지만 장애 시 데이터 손실 가능. 절대 프로덕션에서 끄지 마라.
synchronous_commit 설정:
- on (기본): fsync 후 응답. 가장 안전.
- local: 로컬만 fsync, 복제는 비동기.
- remote_write: 복제본이 받기만 하면 OK.
- remote_apply: 복제본이 적용까지 해야 OK.
- off: fsync 없이 응답. 빠르지만 마지막 몇 ms 손실 가능 (단, 데이터 일관성은 유지).
체크포인트 튜닝
체크포인트는 성능에 큰 영향을 준다:
# postgresql.conf
checkpoint_timeout = 15min # 최대 15분마다
max_wal_size = 4GB # WAL이 이 크기에 도달하면 체크포인트
min_wal_size = 1GB
checkpoint_completion_target = 0.9 # 체크포인트를 이 비율 동안 분산 실행
Trade-off:
- 체크포인트 자주: 복구 빨라지나 I/O 부하 증가.
- 체크포인트 드물게: 복구 느려지나 평상시 성능 향상.
프로덕션에선 보통 timeout 기반 체크포인트가 자연스럽게 되도록 max_wal_size를 넉넉히 잡는다.
WAL과 복제
PostgreSQL의 스트리밍 복제는 WAL을 그대로 복제본에 전송한다:
Primary: WAL 생성 → 네트워크로 전송 → Standby가 재생(redo)
- Asynchronous: 기본. 빠르지만 약간의 lag 가능.
- Synchronous:
synchronous_commit = on+synchronous_standby_names설정. Standby의 ack를 기다림.
Logical Replication은 조금 다르다: 물리 WAL을 논리적 변경(INSERT/UPDATE/DELETE)으로 디코딩해서 전송. 버전 차이나 선택적 테이블 복제가 가능.
WAL 아카이빙과 PITR
Point-in-Time Recovery(PITR)를 위해선:
- 기본 백업 (
pg_basebackup) - WAL 아카이빙 (
archive_command로 WAL 파일을 S3 등으로 복사) - 장애 시 기본 백업 복원 + WAL 재생으로 원하는 시점까지 복구.
archive_mode = on
archive_command = 'aws s3 cp %p s3://my-bucket/wal/%f'
5. InnoDB의 Redo Log와 Undo Log
두 개의 로그 시스템
InnoDB는 PostgreSQL과 달리 두 종류의 로그를 유지한다:
- Redo Log: 변경을 재적용하기 위한 로그 (WAL 역할).
- Undo Log: 변경을 취소하기 위한 로그 (MVCC에도 사용).
이는 MVCC 구현이 PostgreSQL과 다르기 때문이다.
Redo Log 구조
InnoDB의 redo log는 고정 크기 원형 파일이다:
ib_logfile0 (기본 48MB, 여러 개 순환)
ib_logfile1
모든 수정이 redo log에 기록된다:
RedoLogRecord {
type: MLOG_REC_INSERT, MLOG_REC_UPDATE, MLOG_REC_DELETE, ...
space_id: 테이블스페이스 ID
page_no: 페이지 번호
offset: 페이지 내 오프셋
data: 실제 변경 내용
}
Mini-Transaction (mtr)
InnoDB의 핵심 개념: Mini-Transaction (mtr). 이는 원자적인 페이지 수정 그룹이다.
예를 들어 B-Tree에 키를 삽입하면:
- 리프 페이지 수정
- 부모 페이지 수정 (split 시)
- 헤더 업데이트
이 모두가 하나의 mtr이다. mtr의 로그 레코드들은 원자적으로 flush된다. 복구 시 mtr 단위로 재적용된다.
LSN과 Checkpoint LSN
InnoDB도 LSN을 사용하지만 개념이 조금 다르다:
- LSN: 로그 파일 시작부터의 바이트 오프셋.
- checkpoint_lsn: 이 LSN 이전의 변경은 모두 데이터 파일에 반영됨.
복구 시 checkpoint_lsn부터 로그를 재생한다.
Sharp vs Fuzzy Checkpoint
InnoDB는 두 종류의 체크포인트를 한다:
- Sharp Checkpoint: 모든 dirty page를 flush (드물게, 셧다운 시).
- Fuzzy Checkpoint: 일부만 flush하고 LSN만 기록 (자주, 백그라운드).
Fuzzy checkpoint 덕분에 평상시 성능 영향이 작다.
Undo Log와 Rollback Segment
Undo log는 별도의 세그먼트에 저장된다. 각 트랜잭션 시작 시 rollback segment의 undo slot을 할당받는다.
용도:
- 롤백: 트랜잭션 취소 시.
- MVCC: 오래된 버전을 조회할 때 undo log를 따라감.
- 복구 시 undo: 복구 중 미커밋 트랜잭션 취소.
InnoDB 복구 프로세스
- Redo 단계:
ib_logfile을checkpoint_lsn부터 읽어 모든 변경 재적용. - Rollback 단계: 커밋되지 않은 트랜잭션들을 undo log로 취소.
이는 ARIES의 Analysis/Redo/Undo 3단계와 동일한 개념이다.
Doublewrite Buffer: InnoDB의 Torn Page 해결법
PostgreSQL의 Full-Page Write에 대응하는 InnoDB의 기능이 Doublewrite Buffer다.
- Dirty page를 디스크로 쓸 때, 먼저 doublewrite buffer에 쓴다 (순차 I/O).
- doublewrite buffer에 성공적으로 쓰였으면, 실제 데이터 파일에 쓴다 (랜덤 I/O).
- 장애 시 실제 파일의 페이지가 손상되었으면, doublewrite buffer에서 복구.
이는 WAL을 두 배로 불리지 않는 대신 쓰기 I/O를 두 배로 만든다. 최신 NVMe에선 거의 무시할 수 있는 비용이다.
6. WAL의 성능 특성
순차 I/O의 축복
WAL의 가장 큰 성능 이점은 순차 쓰기다:
- 랜덤 HDD: ~100 IOPS, 1MB/s
- 순차 HDD: ~500 MB/s
- 랜덤 SSD: ~10,000 IOPS
- 순차 SSD: ~500 MB/s
WAL은 항상 파일 끝에 추가하므로 디스크 암(arm)이 움직이지 않는다. 초당 수십만 트랜잭션이 가능한 이유다.
fsync의 지연
그러나 fsync는 여전히 비싸다:
- 일반 SSD fsync:
0.11ms - NVMe fsync:
0.010.1ms - 클라우드 디스크 (EBS):
15ms
커밋마다 fsync를 하면 트랜잭션 처리량이 초당 수천에 갇힌다. 해결책:
Group Commit
여러 트랜잭션의 커밋을 한 번의 fsync로 처리한다:
트랜잭션 T1: COMMIT (WAL 버퍼에 기록)
트랜잭션 T2: COMMIT (WAL 버퍼에 기록)
트랜잭션 T3: COMMIT (WAL 버퍼에 기록)
↓
한 번의 fsync()
↓
T1, T2, T3 모두에게 성공 응답
fsync 1번으로 여러 커밋을 처리하니 처리량이 폭발적으로 늘어난다. PostgreSQL과 InnoDB 모두 기본 활성화.
PostgreSQL commit_delay
commit_delay = 10 # μs 단위. 그룹 커밋 대기 시간.
commit_siblings = 5 # 이만큼 트랜잭션이 있을 때만 대기.
이 설정은 고동시성 환경에서 처리량을 높일 수 있지만, 응답 시간을 증가시킨다. 벤치마크가 필수.
WAL 압축
PostgreSQL은 wal_compression = on으로 Full-Page Write를 압축할 수 있다. CPU 비용 대비 I/O 대역폭과 네트워크(복제) 절약.
PG 15부터는 wal_compression에 알고리즘 선택 가능 (pglz, lz4, zstd).
7. WAL과 장애 시나리오
시나리오 1: 전원 차단
- 커밋 전 장애: 해당 트랜잭션의 변경은 로그에도, 데이터에도 없거나 부분적. 복구 시 undo.
- 커밋 후 장애 (데이터 flush 전): 로그에는 있음. 복구 시 redo로 재적용.
- 결과: 커밋 성공 응답이 클라이언트에 도달한 모든 트랜잭션은 보존.
시나리오 2: OS 크래시
전원 차단과 동일. OS가 쓴 내용도 디스크에 반영되지 않을 수 있지만, fsync 이후의 데이터는 안전.
시나리오 3: 디스크 오류
여기서 WAL만으로는 부족하다. 디스크 자체가 손상되면 복구 불가. 해결책:
- 복제 (Replication): 다른 디스크/서버에 데이터 사본 유지.
- 백업: 주기적 백업 + WAL 아카이브 → PITR 가능.
- RAID: 디스크 장애를 하드웨어 수준에서 방어.
시나리오 4: 부분 쓰기 (Torn Page)
앞서 설명. PostgreSQL의 Full-Page Write 또는 InnoDB의 Doublewrite Buffer로 해결.
시나리오 5: "Lost Write"
디스크가 쓰기를 받았다고 거짓 응답하지만 실제로는 안 쓴 경우. fsync를 속이는 잘못된 저가 SSD에서 발생.
검증 방법:
- Diskchecker.pl 같은 도구로 fsync가 정말 동작하는지 테스트.
- 엔터프라이즈급 SSD 사용.
8. WAL 운영 모범 사례
1. WAL 디스크 분리
가능하면 WAL을 데이터 파일과 다른 디스크에 두자:
# PostgreSQL
# data_directory: /data/pgdata
# pg_wal을 심볼릭 링크로 /wal/pg_wal에 연결
이유:
- WAL은 순차 I/O, 데이터는 랜덤 I/O → 서로 경합 방지.
- WAL 디스크 장애와 데이터 디스크 장애 분리.
2. fsync는 무조건 켜라
fsync = off, synchronous_commit = off는 벤치마크용이지 프로덕션용이 아니다.
예외: 개발 환경, 일회성 데이터 로드, 복제본을 유지하는 경우 일시적으로 off 가능.
3. 체크포인트 빈도 조정
# 너무 자주 ← bad
checkpoint_timeout = 1min
# 균형 ← 권장
checkpoint_timeout = 15min
max_wal_size = 4GB
# 너무 드물게 ← bad (복구 느림)
checkpoint_timeout = 1h
4. WAL 크기 모니터링
-- PostgreSQL: 현재 WAL 크기
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0'));
-- 미아카이빙된 WAL
SELECT count(*) FROM pg_ls_waldir() WHERE name < (SELECT last_archived_wal FROM pg_stat_archiver);
WAL이 비정상적으로 커지면:
- 긴 트랜잭션?
- 복제 지연?
- 체크포인트 안 돌고 있나?
- archive_command 실패?
5. 복구 시간 테스트
"백업은 있지만 복구해 본 적 없다" 는 흔한 악몽이다. 주기적으로:
- 백업으로부터 새 환경 구축.
- WAL 재생으로 특정 시점까지 복구.
- 데이터 무결성 검증.
- 소요 시간 기록.
6. synchronous_commit의 이해
# 가장 안전 + 느림
synchronous_commit = remote_apply # 복제본 재생까지 대기
# 데이터 손실 없음 + 빠름
synchronous_commit = remote_write # 복제본 수신까지만 대기
# 일반적 + 빠름
synchronous_commit = on # 로컬 fsync까지
# 마지막 수 ms 손실 허용 + 매우 빠름
synchronous_commit = off
트랜잭션별로 바꿀 수도 있다:
SET LOCAL synchronous_commit = off;
INSERT INTO logs ...; -- 로그성 쓰기는 빠르게
COMMIT;
9. 최신 트렌드와 연구
NVMe와 Storage Class Memory
새로운 스토리지 기술은 WAL 설계에 영향을 준다:
- NVMe: 병렬 I/O 큐로 초당 수백만 IOPS. fsync 오버헤드 감소.
- Intel Optane DCPMM: DRAM 속도에 영구성까지. WAL 자체가 필요 없어질 수 있다는 기대.
- PMEM: mmap 기반 영구 메모리. 여러 DB가 실험 중.
그러나 Optane은 2022년 단종되었고, PMEM 채택은 여전히 제한적. 실전에선 NVMe 위 전통 WAL이 여전히 표준이다.
Async WAL
일부 NoSQL(예: Redis AOF)은 async flush 옵션을 제공한다. 성능은 빠르지만 마지막 수 ms ~ 수 초 손실 가능.
Log-Structured Everything
LSM Tree (RocksDB, Cassandra) 자체가 WAL 기반이다. 메모리 테이블 + WAL → SST 파일로 flush → 주기적 compaction. 이는 "쓰기 증폭" 문제가 있지만 대규모 데이터에 적합.
SMR 드라이브와 WAL
Shingled Magnetic Recording(SMR) HDD는 덮어쓰기가 느리지만 순차 쓰기는 빠르다. WAL의 순차 특성과 잘 맞아 연구가 활발하다.
10. 직접 구현해 보자: 간단한 WAL
최소 WAL 구현 (Python)
import json
import os
import struct
class SimpleWAL:
def __init__(self, log_path):
self.log_path = log_path
self.lsn = 0
if os.path.exists(log_path):
self._recover()
def log(self, record: dict):
"""WAL에 레코드 기록 (force=True로 fsync)"""
self.lsn += 1
record['lsn'] = self.lsn
data = json.dumps(record).encode()
with open(self.log_path, 'ab') as f:
# 길이(4B) + 데이터
f.write(struct.pack('>I', len(data)))
f.write(data)
f.flush()
os.fsync(f.fileno()) # 🔑 핵심: 디스크까지 확인
return self.lsn
def _recover(self):
"""시작 시 로그 재생"""
with open(self.log_path, 'rb') as f:
while True:
length_data = f.read(4)
if not length_data:
break
length = struct.unpack('>I', length_data)[0]
record = json.loads(f.read(length))
self.lsn = max(self.lsn, record['lsn'])
self._apply(record)
def _apply(self, record):
"""로그 레코드를 상태에 적용 (redo)"""
print(f"Applying: {record}")
# 사용 예시
wal = SimpleWAL('demo.wal')
wal.log({'type': 'insert', 'key': 'user:1', 'value': 'alice'})
wal.log({'type': 'update', 'key': 'user:1', 'value': 'bob'})
# 여기서 프로세스 죽어도, 재시작 시 로그 재생으로 복구
이건 극도로 단순화된 예시지만, "변경을 로그에 먼저 쓰고 fsync" 라는 핵심을 보여준다.
실전 구현에 필요한 것들
진짜 프로덕션 WAL은 이보다 훨씬 복잡하다:
- LSN 관리: 바이트 오프셋 기반.
- 페이지 단위 I/O: 4KB/8KB 블록.
- 체크포인트: 주기적 스냅샷.
- 로그 압축: 오래된 레코드 정리.
- 병렬 쓰기: WAL writer 스레드, group commit.
- 복제: 네트워크로 WAL 스트리밍.
- CRC: 로그 레코드 무결성 검증.
- torn page 방어: full-page write 또는 doublewrite.
그래서 PostgreSQL의 WAL 코드는 10,000줄 이상이다. 하지만 원리는 위 예시와 같다.
퀴즈로 복습하기
Q1. WAL의 두 가지 근본 규칙은?
A. (1) WAL Rule: 데이터 페이지를 디스크에 쓰기 전에 해당 변경의 로그 레코드가 먼저 디스크에 flush되어야 한다. (2) Force-at-Commit: 트랜잭션이 커밋된 것으로 간주되기 전에 그 트랜잭션의 모든 로그 레코드가 디스크에 flush되어야 한다. 이 두 규칙만 지키면 어떤 장애 시점에서도 일관된 상태로 복구할 수 있다.
Q2. ARIES의 복구 3단계와 각 단계의 목적은?
A. (1) Analysis: 마지막 체크포인트부터 로그를 읽어 장애 시점의 Dirty Page Table과 Transaction Table을 재구성. (2) Redo: 모든 로그 레코드(커밋/미커밋 무관)를 재적용해서 장애 직전 상태로 복원. "Repeating History". (3) Undo: 미커밋 트랜잭션들을 로그를 역방향으로 따라가며 롤백. 이 과정도 CLR(Compensation Log Record)로 로그에 남겨 재귀적 복구가 가능하게 한다.
Q3. PostgreSQL의 Full-Page Write와 InnoDB의 Doublewrite Buffer가 해결하는 공통 문제는?
A. Torn Page 문제다. 데이터베이스 페이지(보통 8KB/16KB)가 디스크에 쓰이는 중간에 장애가 발생하면, 일부만 쓰인 손상된 페이지가 남는다. OS/디스크는 보통 4KB 블록 단위 원자성만 보장하기 때문이다. PostgreSQL은 체크포인트 후 첫 수정 시 전체 페이지를 WAL에 기록해서 복구 시 사용한다. InnoDB는 doublewrite buffer에 먼저 쓴 후 실제 위치에 쓰는 방식으로 해결한다.
Q4. Group Commit이 성능을 얼마나 향상시키며, 왜 그런가?
A. Group Commit은 수십~수백 배의 처리량 향상을 가져올 수 있다. 단일 SSD의 fsync가 초당 1000~10000번 정도인데, 각 커밋이 fsync 1번을 요구하면 초당 수천 트랜잭션이 한계다. Group Commit은 여러 트랜잭션의 커밋 레코드를 WAL 버퍼에 모은 후 한 번의 fsync로 여러 커밋을 처리한다. fsync는 블로킹 I/O이므로 대기 중인 트랜잭션이 많을수록 효율이 높아진다. PostgreSQL과 InnoDB 모두 기본 활성화되어 있다.
Q5. 왜 복구 과정에서 미커밋 트랜잭션의 변경까지 먼저 redo하고 나중에 undo하는가?
A. ARIES의 "Repeating History" 원칙 때문이다. 이렇게 하면 복구 과정이 훨씬 단순하고 견고해진다:
-
상태 일관성: 일단 장애 직전 상태를 정확히 재현하면, 그 위에서 undo하는 것이 훨씬 쉽다. 어떤 변경이 디스크에 반영됐는지 일일이 구분할 필요가 없다.
-
구조 일관성: B-Tree 같은 복잡한 데이터 구조는 mtr(mini-transaction) 단위의 원자성이 필요하다. 일부 mtr만 적용된 상태에서 undo를 시도하면 구조가 깨질 수 있다.
-
CLR 기록: undo 과정 자체도 로그에 CLR로 남기므로, undo 중 또 장애가 나도 일관되게 이어갈 수 있다.
-
재귀성: 로그 적용과 취소가 같은 프레임워크를 공유하니 구현이 단순해진다.
마치며: WAL이 가르쳐준 것
핵심 정리
- 변경 전에 로그 먼저: 단순한 규칙이지만 내결함성의 기초.
- 순차 I/O의 위력: 왜 DB가 SSD보다 빠를 수 있는가의 답.
- fsync는 타협 불가: 속도와 안전성의 경계.
- Group Commit: 병렬성을 I/O 차원에서 활용.
- ARIES 3단계: Analysis → Redo → Undo가 현대 DB 복구의 표준.
- 체크포인트: 복구 시간과 평상시 성능의 균형.
모든 분산 시스템의 토대
WAL 개념은 단일 DB를 넘어 어디에나 있다:
- Kafka: 사실상 분산 WAL 그 자체.
- etcd/Raft: 로그 복제 = WAL 복제.
- Cassandra: Commit Log + SSTable.
- Redis AOF: Append-Only File.
- ZooKeeper: transaction log + snapshot.
- Blockchain: 불변의 공개 WAL.
"먼저 로그에 쓰고, 나중에 실제 상태에 적용한다" 는 아이디어는 컴퓨터 과학에서 가장 유용한 패턴 중 하나다.
마지막 조언
데이터베이스를 운영하면서 WAL을 이해하고 있으면:
- 복구 시간이 왜 이런지 설명할 수 있다.
- 왜 fsync를 끄면 안 되는지 자신 있게 말할 수 있다.
- 체크포인트 튜닝을 데이터 기반으로 할 수 있다.
- 복제 지연의 원인을 추적할 수 있다.
- 장애 후 무엇이 살아남았는지 판단할 수 있다.
데이터베이스 내부를 아는 엔지니어와 모르는 엔지니어의 차이는, 장애가 났을 때 가장 극명하게 드러난다. WAL을 이해하는 당신은 이미 한 걸음 앞서 있다.
참고 자료
- ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking (Mohan et al., 1992) - ARIES 원 논문
- PostgreSQL Reliability and WAL - 공식 문서
- MySQL InnoDB Redo Log
- The Internals of PostgreSQL - PostgreSQL WAL 상세 설명
- InnoDB Mini-Transaction
- Database Internals (Alex Petrov) - 훌륭한 참고서
- Designing Data-Intensive Applications, Ch.3 - 스토리지 엔진 개괄
- CMU 15-445 Database Systems: Recovery - 대학 강의