RSS듀오랩스
데이터베이스

SQLite WAL 모드: .db만 복사하면 커밋이 사라지는 이유

작성자
듀오랩스 대표·9분 읽기

디렉터리에 파일이 세 개 있습니다.

vault.db
vault.db-wal
vault.db-shm

백업 스크립트를 짤 때 대부분 첫 줄만 집어갑니다. 뒤의 둘은 이름부터 임시 파일처럼 생겼고, 지워도 다시 생기는 것처럼 보이기 때문입니다.

SQLite 문서는 정반대를 말합니다.

The WAL file is part of the persistent state of the database and should be kept with the database if the database is copied or moved. If a database file is separated from its WAL file, then transactions that were previously committed to the database might be lost, or the database file might become corrupted.

SQLite, Write-Ahead Logging

previously committed 가 이 문장의 무게입니다. 실패한 쓰기가 날아간다는 이야기가 아니라, 성공했다고 응답까지 받은 쓰기가 날아간다는 이야기입니다.

최신 커밋은 아직 .db에 없습니다

흔히 세 파일을 이렇게 이해합니다. .db 는 데이터베이스 본체, -wal 은 사고에 대비한 로그, -shm 은 실행 중에만 쓰는 찌꺼기. 진짜는 하나고 나머지 둘은 딸린 것이라는 그림입니다.

절반만 맞습니다. -shm 은 정말 딸린 것입니다. -wal 은 아닙니다.

WAL 모드에서 쓰기는 .db 에 닿지 않습니다. 커밋은 -wal 끝에 커밋 레코드를 붙이는 것으로 끝나고, 본체는 그대로 있습니다. 그러니 방금 성공한 INSERT.db 안에 없습니다. 아직 -wal 에만 있습니다.

롤백 저널과 방향이 반대입니다

기본 저널 모드인 롤백 저널과 순서가 뒤집혀 있습니다. 롤백 저널은 본체를 직접 고치면서 고치기 전 원본을 옆에 적어둡니다. 실패하면 그걸로 되돌립니다. WAL은 본체를 놔두고 바뀐 내용을 덧붙입니다.

롤백 저널 WAL
보조 파일에 적히는 것 바뀌기 전 원본 바뀐 뒤 새 내용
최신 커밋이 있는 곳 .db .db-wal 을 겹친 결과
보조 파일을 잃으면 되돌리기가 안 됨 커밋이 사라짐
읽기와 쓰기 서로 막음 서로 막지 않음

읽는 쪽은 .db 를 보다가 -wal 에 더 새로운 페이지가 있으면 그쪽을 씁니다. 두 파일을 겹쳐야 비로소 현재 상태가 됩니다. 한쪽만 들고 가는 것은 데이터베이스를 들고 가는 게 아니라 과거의 어느 시점을 들고 가는 것입니다.

이 방식이 빠른 이유도 같은 데서 나옵니다. 문서는 WAL uses many fewer fsync() operations 라고 적고 있고, 쓰기가 파일 끝에 붙기만 하니 디스크 접근도 순차적입니다.

지워도 되는 파일과 안 되는 파일

-shm 은 WAL-index입니다. -wal 안에서 어떤 페이지의 최신 버전이 어디 있는지 찾아주는 색인이고, 여러 프로세스가 공유 메모리로 같이 들여다봅니다. 내용은 전부 -wal 에서 다시 만들 수 있습니다. 아무도 데이터베이스를 열고 있지 않을 때 지우면 다음에 열 때 재생성됩니다.

두 파일이 나란히 붙어 있고 이름도 비슷한데 성질이 정반대라는 점이, 저는 이 모드에서 가장 오해를 부르는 부분이라고 봅니다. 파일 이름이 아무 도움을 주지 않습니다.

파일이 하나만 보이는 경우

.db 하나만 있는 디렉터리를 보고 WAL을 안 쓴다고 판단하면 틀릴 수 있습니다.

Usually, the WAL file is deleted automatically when the last connection to the database closes. However, if the last process to have the database open exits without cleanly shutting down the database connection ... then the WAL file might be retained on disk.

마지막 연결이 정상적으로 닫히면서 정리된 것뿐입니다. 모드 자체는 파일에 남아 있습니다. PRAGMA journal_mode=WAL 은 연결 옵션이 아니라 데이터베이스에 기록되는 성질이라, 문서 표현으로는 persistent 합니다. 한 번 켜면 다음에 열어도 WAL입니다.

거꾸로 -wal 이 남아 있다면 둘 중 하나입니다. 지금 누군가 열고 있거나, 지난번에 깨끗하게 닫히지 않았거나.

체크포인트가 WAL을 본체로 접습니다

-wal 이 무한정 자라지 않는 이유는 체크포인트가 주기적으로 내용을 .db 로 옮기고 WAL을 처음부터 다시 쓰게 만들기 때문입니다. 기본 임계값은 페이지 수로 정해져 있습니다.

By default, SQLite does a checkpoint automatically when the WAL file reaches a threshold size of 1000 pages.

1000 페이지가 몇 바이트인지는 page_size 가 정합니다. 요즘 기본값인 4096바이트라면 약 4MB입니다.

다만 체크포인트가 끝까지 도는 조건이 있습니다. 그 WAL을 쓰는 다른 연결이 없어야 합니다. 그래서 읽기가 한순간도 끊기지 않는 서비스에서는 이런 일이 생깁니다.

if a database has many concurrent overlapping readers and there is always at least one active reader, then no checkpoints will be able to complete and hence the WAL file will grow without bound.

읽기가 많아서 WAL이 커지고, WAL이 커지면 읽기가 다시 느려집니다. 읽을 때마다 WAL을 확인해야 하니까요. 이 고리를 끊는 일반적인 답은 없다고 봅니다. 워크로드를 보고 수동 체크포인트를 넣을지 정하는 문제입니다.

읽기와 쓰기가 안 막는다는 말의 범위

WAL을 켜면 동시 쓰기가 된다고 이해하는 경우가 있습니다. 그건 아닙니다.

since there is only one WAL file, there can only be one writer at a time

바뀐 것은 읽기와 쓰기 사이의 관계 하나입니다. 읽는 쪽 여럿과 쓰는 쪽 하나가 동시에 돌 수 있게 됐을 뿐, 쓰는 쪽끼리는 여전히 줄을 섭니다.

읽기가 일관되게 유지되는 방식도 알아둘 만합니다. 읽기 트랜잭션은 시작할 때 WAL에서 마지막 유효한 커밋 위치를 기억하고, 트랜잭션이 끝날 때까지 그 지점까지만 봅니다. 문서는 이 지점을 end mark 라고 부릅니다. 읽는 도중에 새 커밋이 들어와도 화면이 흔들리지 않는 이유입니다.

WAL이 공짜가 아닌 지점

정의만 보면 안 쓸 이유가 없어 보이는데, 문서가 명시하는 제약이 몇 개 있습니다.

같은 호스트여야 합니다. -shm 을 공유 메모리로 다뤄야 하기 때문에 네트워크 파일시스템에서는 동작하지 않습니다. 데이터베이스 파일을 NFS나 유사한 공유 스토리지에 올려두고 여러 서버에서 붙이는 구성이라면 WAL은 선택지가 아닙니다.

WAL로 들어간 뒤에는 page_size 를 바꿀 수 없습니다. 페이지 크기를 조정할 생각이 있다면 순서가 먼저입니다.

synchronous 설정이 내구성을 정합니다. WAL 모드에서 synchronous=NORMAL 은 문서가 권하는 조합이지만, 커밋마다 WAL을 동기화하지 않는다는 뜻이기도 합니다. 애플리케이션이 죽는 것에는 안전하고, 전원이 끊기는 상황에서는 최근 커밋 몇 건을 잃을 수 있습니다. 데이터베이스가 깨지지는 않습니다. 성능 설정처럼 보이지만 실제로는 어디까지 잃어도 되는지를 정하는 설정입니다.

복사는 SQLite에게 시키는 쪽이 맞습니다

세 파일을 손으로 다 챙기는 방법도 있지만, 복사하는 사이에 쓰기가 들어오면 그것대로 어긋납니다. 열려 있는 데이터베이스를 안전하게 뜨는 경로가 이미 있습니다.

sqlite3 vault.db ".backup out.db"

SQL 안에서는 VACUUM INTO 'out.db' 가 같은 일을 합니다. 둘 다 일관된 시점 하나를 담은 단일 파일을 만들어 줍니다.

버전 관리에서 빼는 규칙도 같은 이유로 세 줄이어야 합니다.

*.db
*.db-wal
*.db-shm

*.db 한 줄만 적어두면, 커밋되지 않기를 바랐던 가장 최근 쓰기가 정확히 -wal 에 담긴 채로 올라갑니다. 가장 새로운 데이터가 가장 먼저 새어 나가는 셈입니다.

참고: Write-Ahead Logging, PRAGMA journal_mode

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.