사전 WAL
패턴

WAL

gabury1

데이터 파일을 고치기 전에 무엇을 고칠지부터 로그에 적어 두는 방식입니다. 로그가 저장 장치에 닿은 뒤에야 데이터 파일을 고칩니다. 그래서 도중에 서버가 죽어도 로그를 다시 읽어 밀린 변경을 되살릴 수 있습니다.

쉽고 빠른 이해

데이터 파일을 고치기 전에 무엇을 고칠지부터 로그에 먼저 적어 두는 방식입니다. SQLite 는 데이터베이스 파일 옆에 -wal 이 붙은 파일을 하나 더 두고 변경분을 거기에 쌓습니다.

이걸 안 하면 커밋할 때마다 그 트랜잭션이 건드린 데이터 파일을 전부 디스크로 내려보내야 합니다. 여러 데이터 파일을 다 기다리는 대신 순차로 쌓이는 로그 파일 하나만 기다리면 됩니다. 도중에 서버가 죽어도 로그를 다시 읽어 밀린 변경을 되살릴 수 있습니다.

  1. 바꿀 내용을 로그에 적습니다.
  2. 커밋할 때 그 로그를 디스크에 확실히 내려보냅니다. 여기까지 되면 커밋이 끝난 것입니다.
  3. 데이터 파일은 나중에 몰아서 고칩니다. 이 따라잡기를 체크포인트라고 부릅니다.

대가는 셋입니다. 커밋이 로그가 디스크에 닿기를 기다립니다. 따라잡기라는 일이 새로 생겨 그때마다 입출력이 몰립니다. 로그 파일이 디스크를 따로 먹습니다.

상세

WAL(Write-Ahead Logging, 미리 쓰기 로그)은 쓰는 순서를 못 박기로 한 결정입니다. PostgreSQL 공식 문서는 그 중심 개념을 이렇게 적습니다. 테이블과 인덱스가 사는 데이터 파일에 대한 변경은 그 변경이 로그에 적힌 뒤에만 쓰여야 합니다. 정확히는 변경을 기술하는 WAL 레코드가 영구 저장소로 내려 쓰인 뒤입니다. 순서가 전부입니다. 로그가 먼저고 데이터 파일이 나중입니다.

이 절차를 따르면 트랜잭션이 커밋될 때마다 데이터 페이지를 디스크로 내려보낼 필요가 없어집니다. 같은 문서는 그 이유를 크래시가 나도 로그로 데이터베이스를 복구할 수 있다는 것을 알기 때문이라고 적습니다. 데이터 페이지에 아직 반영되지 않은 변경은 WAL 레코드에서 다시 실행됩니다. 이것을 롤포워드 복구, 다른 이름으로 REDO 라고 부릅니다.

쓰기가 향하는 자리도 달라집니다. 트랜잭션이 커밋됐음을 보장하려고 디스크로 내려보내야 하는 것은 그 트랜잭션이 건드린 모든 데이터 파일이 아니라 WAL 파일 하나입니다. WAL 파일은 순차로 쓰이므로, WAL 을 동기화하는 비용은 데이터 페이지를 내려보내는 비용보다 훨씬 적다고 같은 문서는 적습니다. 서버가 작고 동시적인 트랜잭션을 많이 처리할 때는 WAL 파일에 대한 fsync 한 번이 여러 트랜잭션의 커밋을 감당할 수도 있다고 덧붙입니다.

ARIES 논문은 같은 규칙을 WAL 프로토콜이라는 이름으로 적습니다. 어떤 데이터의 변경을 나타내는 로그 레코드가 이미 안정 저장소에 있어야, 그 변경된 데이터가 비휘발성 저장소에 있는 이전 버전을 대체하는 것이 허용된다는 규약입니다. 적어도 그 페이지의 갱신을 기술하는 로그 레코드의 언두 부분이 먼저 안정 저장소에 쓰여야 합니다. 이 규약을 강제하려고, WAL 방식을 쓰는 시스템은 페이지마다 그 페이지에 마지막으로 수행된 갱신을 기술하는 로그 레코드의 LSN(Log Sequence Number, 로그 일련번호)을 저장합니다.

이 결정은 다른 선택지를 밀어낸 자리이기도 합니다. 같은 논문은 WAL 기반 시스템에서 갱신된 페이지가 그것을 읽어 온 바로 그 비휘발성 저장소 위치로 되쓰인다고 적습니다. 제자리 갱신입니다. 대조되는 것이 System R 과 SQL/DS 같은 시스템이 쓴 섀도 페이지 기법입니다. 거기서는 갱신된 버전의 페이지가 비휘발성 저장소의 다른 위치에 쓰입니다. 다음 체크포인트 전에 시스템이 죽으면 이전 버전의 페이지가 데이터베이스 복구에 쓰입니다.

SQLite 는 같은 결정을 롤백 저널과 견줘 설명합니다. 롤백 저널은 원래의 바뀌지 않은 데이터베이스 내용을 별도 파일에 복사해 두고, 변경은 데이터베이스 파일에 곧바로 씁니다. WAL 은 이 방향을 뒤집습니다. 원래 내용은 데이터베이스 파일에 그대로 보존되고 변경분이 별도 WAL 파일에 덧붙습니다. 커밋을 나타내는 특별한 레코드가 WAL 에 덧붙는 순간이 COMMIT 입니다. 그래서 원본 데이터베이스에 한 번도 쓰지 않고 커밋이 끝날 수 있습니다. 하나의 WAL 파일 끝에 여러 트랜잭션이 이어 붙을 수 있습니다.

flowchart TD
    subgraph RJ["롤백 저널"]
        direction TD
        R0["변경"] --> R1["데이터베이스 파일"]
        R1 -->|"원래 내용을 복사해 둔다"| R2["저널 파일"]
    end
    subgraph WA["WAL"]
        direction TD
        W0["변경"] --> W1["WAL 파일"]
        W1 -.->|"나중에 옮긴다"| W2["데이터베이스 파일"]
    end

변경분이 어느 파일로 가고 원래 내용이 어느 파일에 남는지가 두 방식에서 서로 뒤바뀝니다. 롤백 저널은 데이터베이스 파일에 곧바로 씁니다. 옛 내용은 저널 파일에 따로 챙겨 둡니다. WAL 은 데이터베이스 파일을 그대로 둡니다. 새 내용을 WAL 파일에 따로 쌓습니다.

로그가 남는다는 성질은 복구 말고 다른 쓰임도 엽니다. PostgreSQL 문서는 WAL 이 온라인 백업과 시점 복구를 가능하게 한다고 적습니다. WAL 데이터를 아카이빙해 두면, 이전 물리 백업을 설치한 뒤 원하는 시각까지만 WAL 을 재생하는 식으로 그 시점으로 되돌릴 수 있습니다.

대가

WAL 을 고르면 원래 없던 일이 몇 가지 생깁니다. 값은 커밋을 기다리는 시간, 체크포인트라는 새 작업, 그리고 데이터베이스를 놓을 수 있는 자리의 제약으로 나뉘어 나옵니다.

커밋 대기와 최근 트랜잭션의 손실

PostgreSQL 문서는 트랜잭션 커밋이 보통 동기적이라고 적습니다. 서버는 그 트랜잭션의 WAL 레코드가 영구 저장소로 내려 쓰일 때까지 기다린 뒤에야 클라이언트에게 성공을 알립니다. 이 대기가 순서를 지키는 값입니다.

기다리지 않는 선택지는 비동기 커밋입니다. 같은 문서는 이것을 트랜잭션이 논리적으로 완료된 즉시, 그 트랜잭션이 만든 WAL 레코드가 실제로 디스크에 닿기 전에 성공을 돌려주는 모드라고 적습니다. 대가는 데이터베이스가 크래시하면 가장 최근 트랜잭션들을 잃을 수 있다는 것입니다. 문서는 이 위험이 데이터 손실이지 데이터 손상은 아니라고 못 박습니다. 크래시가 나면 데이터베이스는 WAL 을 재생해 복구합니다. 마지막으로 내려 쓰인 레코드까지입니다. 순 효과는 마지막 몇 개 트랜잭션의 손실입니다. 위험 창의 길이는 제한됩니다. WAL writer 라는 백그라운드 프로세스가 wal_writer_delay 밀리초마다 아직 쓰이지 않은 WAL 레코드를 내려보냅니다.

체크포인트라는 추가 작업

로그를 먼저 쓰기로 하면 로그와 데이터 파일 사이가 벌어집니다. 그 간격을 주기적으로 좁히는 작업이 체크포인트입니다. SQLite 문서는 이것을 단점 목록에 넣습니다. 체크포인팅이라는 추가 작업이 있고, 기본적으로 자동이긴 하지만 여전히 애플리케이션 개발자가 신경 써야 하는 것이라고 적습니다.

PostgreSQL 쪽은 그 부담을 수치가 붙는 자리로 적습니다. 모든 더티 데이터 페이지를 디스크로 내려보내야 한다는 체크포인트의 요구는 상당한 I/O(Input/Output, 입출력) 부하를 일으킬 수 있습니다. 같은 문서는 체크포인트가 꽤 비싸다고 적고 이유를 둘로 나눕니다. 첫째, 그 시점에 더티인 모든 버퍼를 써내야 합니다. 둘째, 그 결과 이후에 WAL 트래픽이 더 생깁니다.

로그는 공간도 먹습니다. pg_wal 디렉토리의 WAL 세그먼트 파일 개수는 min_wal_size, max_wal_size, 그리고 앞선 체크포인트 주기에 생성된 WAL 양에 달려 있다고 문서는 적습니다. 설정과 부하가 함께 디스크 소비를 정합니다.

배치 자리의 제약

SQLite 문서의 단점 목록은 배치 제약을 여럿 답니다. 데이터베이스를 쓰는 모든 프로세스가 같은 호스트 컴퓨터에 있어야 합니다. WAL 은 네트워크 파일시스템 위에서 동작하지 않습니다. WAL 이 모든 프로세스에게 작은 공유 메모리를 함께 쓰도록 요구하기 때문입니다. 서로 다른 호스트의 프로세스는 메모리를 공유할 수 없습니다.

여러 개의 붙인 데이터베이스에 걸친 변경을 담은 트랜잭션은 각 데이터베이스별로는 원자적이지만, 전체를 한 묶음으로 보면 원자적이지 않습니다.

WAL 모드에 들어간 뒤에는 page_size 를 바꿀 수 없습니다. 읽기 전용으로 WAL 데이터베이스를 여는 것도 안 됩니다. 여는 프로세스는 데이터베이스에 딸린 -shm wal-index 공유 메모리 파일에 쓰기 권한을 갖고 있어야 합니다. 그 파일이 없다면 데이터베이스 파일이 든 디렉토리에 쓰기 권한이 있어야 합니다.

파일도 늘어납니다. 데이터베이스마다 준영속적인 -wal 파일과 -shm 공유 메모리 파일이 따로 붙습니다. 같은 문서는 이 때문에 SQLite 를 애플리케이션 파일 포맷으로 쓰기에는 덜 끌리게 된다고 적습니다.

워크로드에 따라 손해가 되는 구간

읽기가 대부분이고 쓰기가 드문 애플리케이션에서는, 전통적인 롤백 저널 방식보다 아주 조금 느릴 수 있다고 SQLite 문서는 적습니다. 아마 1퍼센트나 2퍼센트 정도일 것이라고 덧붙입니다.

크기도 걸립니다. 같은 문서는 WAL 이 작은 트랜잭션에 가장 잘 맞고 아주 큰 트랜잭션에는 잘 맞지 않는다고 적습니다. 대략 100메가바이트를 넘는 트랜잭션에서는 전통적인 롤백 저널 모드가 더 빠를 가능성이 높다고 적습니다. 1기가바이트를 넘는 트랜잭션에서는 WAL 모드가 I/O 에러나 디스크 가득 참 에러로 실패할 수 있습니다. 다만 3.11.0 판, 그러니까 2016년 2월 15일 판부터는 WAL 모드가 큰 트랜잭션에서도 롤백 모드만큼 효율적으로 동작한다고 같은 문서가 덧붙입니다.

동작

한 변경이 지나는 자리는 셋입니다. 로그 버퍼, 로그 파일, 데이터 파일입니다.

ARIES 논문은 로그 레코드가 쓰일 때 먼저 로그 파일의 휘발성 저장소 버퍼에만 놓인다고 적습니다. 어떤 시점에, 예를 들어 커밋 시점에, 특정 LSN 까지의 로그 레코드가 로그 페이지 순서대로 안정 저장소에 쓰입니다. 이것을 그 LSN 까지 로그를 강제 기록한다고 부릅니다. 데이터 페이지가 비휘발성 저장소로 내려가는 것은 그 뒤입니다. 그 페이지의 갱신을 기술한 로그 레코드가 이미 안정 저장소에 있어야 한다는 조건이 걸립니다.

sequenceDiagram
    participant 버퍼 as 로그 버퍼
    participant 로그 as 로그 파일
    participant 데이터 as 데이터 파일
    Note over 버퍼: 변경 레코드가 먼저 놓이는 자리
    버퍼->>로그: 커밋 시점에 그 LSN 까지 강제 기록
    Note over 로그: 안정 저장소에 닿음
    로그->>데이터: 더티 페이지 내려보내기
    Note over 데이터: 체크포인트

체크포인트는 그 간격을 끊는 지점입니다. PostgreSQL 문서는 체크포인트를 그 시점 이전에 쓰인 모든 정보로 힙과 인덱스 데이터 파일이 갱신되어 있음이 보장되는 트랜잭션 순서상의 지점이라고 적습니다. 체크포인트 시각에 모든 더티 데이터 페이지가 디스크로 내려가고, 특별한 체크포인트 레코드가 WAL 파일에 쓰입니다. 체크포인터 프로세스가 이 일을 주기적으로 합니다. checkpoint_timeout 초마다, 또는 max_wal_size 를 넘기려는 참에, 둘 중 먼저 오는 쪽에서 체크포인트가 시작됩니다.

SQLite 에서 체크포인트는 옮기는 일입니다. WAL 파일에 덧붙어 있는 트랜잭션들을 결국 원래 데이터베이스로 되옮겨야 합니다. 그 옮김이 체크포인트입니다. 기본값으로 SQLite 는 WAL 파일이 1000 페이지라는 임계 크기에 닿으면 자동으로 체크포인트를 합니다.

죽고 나서 되살아나는 경로는 체크포인트 레코드에서 시작합니다. PostgreSQL 문서는 크래시 복구 절차가 가장 최근 체크포인트 레코드를 보고 REDO 를 시작할 WAL 안의 지점을 정한다고 적습니다. 그 지점 앞에서 데이터 파일에 가해진 변경은 이미 디스크에 있음이 보장됩니다. 그래서 체크포인트 이후에는, 그 지점을 담은 세그먼트보다 앞선 WAL 세그먼트들이 더 이상 필요 없어져 재활용하거나 지울 수 있습니다.

flowchart TD
    A[재시작] --> B[가장 최근 체크포인트 레코드 읽기]
    B --> C[REDO 를 시작할 지점 정하기]
    C --> D[그 지점부터 WAL 재생]
    D --> E[데이터 파일 최신화]

읽는 쪽과 옮기는 쪽이 겹치는 자리도 순서가 정해져 있습니다. SQLite 문서는 WAL 모드 데이터베이스에서 읽기가 시작될 때 WAL 안 마지막 유효 커밋 레코드의 위치를 먼저 기억한다고 적습니다. 이 지점을 끝 표시라고 부릅니다. 체크포인트는 읽는 쪽과 동시에 돌 수 있지만, 현재 읽는 쪽의 끝 표시를 지난 페이지에 닿으면 멈춰야 합니다. 쓰기가 일어날 때마다 쓰는 쪽은 체크포인터가 얼마나 진행했는지 확인합니다. WAL 전체가 데이터베이스로 옮겨져 동기화됐고 WAL 을 쓰고 있는 읽기가 하나도 없으면, 쓰는 쪽은 WAL 을 처음으로 되감아 새 트랜잭션을 앞에서부터 넣기 시작합니다.

block-beta
columns 3
  a["이미 옮겨진 부분"] b["끝 표시까지"] c["끝 표시를 지난 부분"]
  d["WAL 파일 · 앞에서 뒤로 이어 붙는다"]:3

체크포인트가 WAL 파일의 어디까지 옮길 수 있는지를 끝 표시가 가릅니다. 끝 표시 앞쪽은 옮길 수 있고, 그 지점을 지난 페이지에 닿으면 멈춥니다. 앞이 전부 옮겨지고 그 WAL 을 쓰는 읽기가 하나도 없을 때 쓰는 쪽이 파일을 처음으로 되감습니다.

옮기는 동안의 동기화 순서도 정해져 있습니다. 같은 문서는 전원 손실이나 하드 리부팅 뒤에 데이터베이스가 깨질 가능성을 피하려면 체크포인팅이 동기화를 요구한다고 적습니다. WAL 내용을 데이터베이스로 옮기기 전에 WAL 이 영속 저장소로 동기화돼야 합니다. WAL 을 되감기 전에는 데이터베이스 파일이 동기화돼야 합니다.

예시

SQLite

PRAGMA journal_mode=WAL;

SQLite 데이터베이스 연결은 기본이 journal_mode=DELETE 입니다. 위 한 줄이 WAL 모드로 바꿉니다. journal_mode PRAGMA 는 새 저널 모드를 문자열로 돌려주고, 성공하면 wal 을 돌려줍니다.

WAL 모드 데이터베이스에 연결이 열려 있는 동안 SQLite 는 미리 쓰기 로그라고 부르는 추가 저널 파일을 유지합니다. 디스크에서 이 파일 이름은 보통 데이터베이스 파일 이름에 -wal 접미사가 붙은 것입니다. WAL 파일은 어떤 연결이든 그 데이터베이스를 열고 있는 한 존재합니다. 보통은 마지막 연결이 닫힐 때 자동으로 지워집니다.

자동 체크포인트는 기본으로 켜져 있습니다. COMMIT 이 일어나 WAL 파일이 1000 페이지 이상이 되거나, 그 데이터베이스 파일에 대한 마지막 연결이 닫히면 돕니다.

PostgreSQL

wal_level = replica
checkpoint_timeout = 5min

wal_level 은 WAL 에 얼마나 많은 정보를 쓸지 정합니다. 기본값은 replica 입니다. WAL 아카이빙과 복제를 지원하기에 충분한 데이터를 씁니다. 스탠바이 서버에서 읽기 전용 질의를 돌리는 것까지 포함합니다. minimal 은 크래시나 즉시 종료에서 복구하는 데 필요한 정보만 남기고 나머지 로깅을 없앱니다. logical 은 논리 디코딩을 지원하는 데 필요한 정보를 더 씁니다. 다만 minimal WAL 은 시점 복구에 충분한 정보를 담지 않으므로, 연속 아카이빙과 스트리밍 바이너리 복제를 켜려면 replica 이상이어야 합니다.

checkpoint_timeout 은 자동 WAL 체크포인트 사이의 최대 시간입니다. 단위 없이 적으면 초로 봅니다. 유효 범위는 30초에서 하루까지고 기본값은 5분입니다. 체크포인트는 이 값과 max_wal_size 중 먼저 걸리는 쪽에서 시작합니다. 기본 설정은 각각 5분과 1기가바이트입니다.

WAL 세그먼트 파일은 pg_wal 디렉토리에 놓입니다. 그 개수는 min_wal_size, max_wal_size, 그리고 앞선 체크포인트 주기에 생성된 WAL 양에 달려 있습니다.

MySQL InnoDB

SET GLOBAL innodb_redo_log_capacity = 8589934592;

MySQL 은 같은 결정을 리두 로그라는 다른 이름으로 부릅니다. 공식 문서는 리두 로그를 크래시 복구 중에 완료되지 못한 트랜잭션이 쓴 데이터를 바로잡는 데 쓰는 디스크 기반 자료구조라고 적습니다. 정상 동작 중에는 SQL(Structured Query Language, 구조화 질의 언어) 문이나 저수준 API(Application Programming Interface) 호출에서 나온 테이블 데이터 변경 요청을 인코딩합니다. 예기치 못한 종료 전에 데이터 파일 갱신을 끝내지 못한 수정은 초기화 중에, 연결을 받아들이기 전에 자동으로 재생됩니다.

리두 로그는 디스크에서 리두 로그 파일로 표현됩니다. 데이터가 리두 로그 파일을 지나간 자취는 계속 커지는 LSN 값으로 나타납니다. 리두 로그 데이터는 데이터 변경이 일어날 때마다 덧붙습니다. 체크포인트가 진행되면 가장 오래된 데이터가 잘려 나갑니다. 위 한 줄은 용량을 8기가바이트로 잡는 예입니다. InnoDB 는 리두 로그 파일을 총 32개 유지하려 합니다. 각 파일의 크기는 innodb_redo_log_capacity 의 32분의 1과 같습니다.

관련 항목

WAL 을 이루는 구성 요소와 참여자

LSN · 체크포인트 · 리두 로그 · 언두 · 더티 페이지 · 레코드 · 세그먼트 · 로그 · 재활용 · fsync · WAL writer · 프로세스

이것이 고른 갱신 방식과 그 대안

제자리 갱신 · 롤백 저널 · 섀도 페이지 · System R · SQL/DS · 저널링 파일시스템

트랜잭션이 커밋되는 방식

트랜잭션 · 커밋 · 롤백 · 동기 커밋 · 비동기 커밋

크래시에서 되살아나는 경로

크래시 복구 · 시점 복구 · 롤포워드 복구 · REDO · 온라인 백업

로그가 배포·전달되는 경로

WAL 아카이빙 · 스트리밍 복제 · 논리 디코딩

WAL 을 실제로 구현·채택한 제품

PostgreSQL · SQLite · MySQL · InnoDB · 데이터베이스

변경 요청이 비롯되는 지점

SQL · API · 테이블 · 인덱스

WAL 규약을 정의한 논문과 그 소속

ARIES · IBM · ACM

WAL 이 요구하는 운영 제약

네트워크 · 공유 메모리 · 권한

다른 이름: Write-Ahead Logging · write-ahead log · 미리 쓰기 로그 · WAL 모드