WAL(Write-Ahead Logging) 모드

WAL(Write-Ahead Logging) 모드

SQLite가 원자적 커밋과 롤백을 구현하는 기본 방식은 롤백 저널(rollback journal)이에요. 그런데 3.7.0(2010-07-21) 버전부터는 롤백 저널 대신 쓸 수 있는 "WAL"(Write-Ahead Log) 옵션이 생겼어요. 이름 그대로 변경 사항을 데이터베이스 파일에 바로 쓰지 않고, 별도 로그 파일에 "먼저(ahead)" 기록한 뒤 나중에 데이터베이스로 옮기는 방식이에요.

출처: SQLite Write-Ahead Logging Documentation

WAL의 장점과 단점

장점은 이래요.

  1. 대부분의 시나리오에서 WAL이 훨씬 빠르고,
  2. 읽는 쪽이 쓰는 쪽을 막지 않고, 쓰는 쪽도 읽는 쪽을 막지 않아 동시성이 높아요. 읽기와 쓰기가 동시에 진행될 수 있죠.
  3. 디스크 I/O가 더 순차적으로 이뤄지고,
  4. fsync() 호출 횟수가 훨씬 적어서 fsync()가 깨진 시스템에서도 문제에 덜 노출돼요.

단점도 있어요.

  1. 같은 데이터베이스를 쓰는 모든 프로세스가 같은 호스트에 있어야 해요. WAL은 프로세스들이 작은 공유 메모리를 공유해야 하기 때문에 네트워크 파일시스템에서는 동작하지 않아요.
  2. 여러 ATTACHed 데이터베이스에 걸친 변경 트랜잭션은 데이터베이스별로는 원자적이지만, 전체를 하나로 묶어 원자적이진 않아요.
  3. WAL 모드에 들어간 뒤에는 page_size를 바꿀 수 없어요. 페이지 크기를 바꾸려면 롤백 저널 모드여야 해요.
  4. 읽기 전용 WAL 데이터베이스를 여는 게 기본적으로 불가능해요. -shm 파일에 대한 쓰기 권한이 필요하죠.
  5. 읽기 위주로 거의 쓰지 않는 애플리케이션에서는 WAL이 전통적인 롤백 저널보다 아주 약간(1~2%) 느릴 수 있어요.
  6. 각 데이터베이스에 -wal 파일과 -shm 공유 메모리 파일이 추가되어, 응용 프로그램 파일 형식으로 쓰기엔 덜 매력적일 수 있어요.
  7. 체크포인트(checkpoint)라는 추가 작업이 있고, 기본적으로 자동이지만 개발자가 인지하고 있어야 해요.

WAL이 동작하는 방식

롤백 저널은 원래의 변경 전 데이터를 별도 저널 파일에 복사하고, 변경은 데이터베이스 파일에 직접 써요. 크래시나 ROLLBACK이 발생하면 저널의 원본을 데이터베이스로 되돌려 재생하는 방식이죠. COMMIT은 저널 파일이 삭제될 때 일어나요.

WAL은 이 과정을 뒤집어요. 원본 내용은 데이터베이스 파일에 그대로 두고, 변경 사항은 별도의 WAL 파일에 **추가(append)**해요. 커밋을 나타내는 특별한 레코드가 WAL에 추가될 때 COMMIT이 일어나요. 그래서 원본 데이터베이스를 전혀 건드리지 않고 커밋이 가능하고, 변경이 WAL에 커밋되는 동안에도 읽는 쪽은 원본 데이터베이스로 계속 읽을 수 있어요. 하나의 WAL 파일 끝에 여러 트랜잭션을 계속 쌓을 수 있어요.

체크포인트

WAL에 쌓인 트랜잭션을 언젠가는 원본 데이터베이스로 옮겨야 해요. WAL 파일의 트랜잭션을 데이터베이스로 이동하는 것을 체크포인트라고 불러요. 기본적으로 SQLite는 WAL 파일이 1000페이지 크기의 임계값에 도달하면 자동으로 체크포인트를 실행해요. 애플리케이션이 특별히 할 일은 없지만, 원하면 자동 체크포인트의 임계값을 조정하거나 자동 체크포인트를 끄고 한가한 시간이나 별도 스레드에서 실행할 수도 있어요.

동시성

WAL 모드에서 읽기 작업이 시작되면 먼저 WAL에서 마지막 유효한 커밋 레코드의 위치를 기억해요. 이 지점을 "end mark"라고 불러요. 각 읽기 트랜잭션은 자신의 end mark를 지니고, 트랜잭션 동안에는 그 end mark가 바뀌지 않아서 특정 시점의 데이터베이스 내용만 보게 돼요.

쓰기는 단순히 WAL 파일 끝에 새 내용을 추가할 뿐이라 읽기를 방해하지 않아요. 그래서 읽기와 쓰기를 동시에 할 수 있지만, WAL 파일이 하나뿐이라 한 번에 쓰는 쪽은 하나만 존재해요. 읽는 쪽이 WAL에서 페이지를 빠르게 찾도록 wal-index라는 구조가 공유 메모리에 유지돼요. 이 공유 메모리 때문에 모든 읽는 쪽이 같은 머신에 있어야 하고, 네트워크 파일시스템에서 동작하지 않는 이유예요.

체크포인트는 읽기와 동시에 진행될 수 있지만, 현재 읽기의 end mark를 지나는 페이지에 도달하면 멈춰야 해요. 그래서 오래 실행되는 읽기 트랜잭션이 체크포인트 진행을 막을 수 있어요. 결국 그 읽기 트랜잭션은 언젠가 끝나고 체크포인트가 이어서 진행되겠죠.

성능 고려사항

쓰기 트랜잭션은 내용을 한 번만 쓰고(롤백 저널은 두 번) 순차적으로 써서 매우 빨라요. 반면 WAL 파일이 커질수록 읽기 성능은 떨어져요. 읽는 쪽이 WAL을 확인해야 하는데 그 시간이 WAL 크기에 비례하기 때문이에요. wal-index가 훨씬 빠르게 찾게 도와주지만 성능 저하를 완전히 막을 순 없어요. 그래서 정기적으로 체크포인트를 돌려 WAL 파일 크기를 작게 유지하는 게 좋은 읽기 성능 유지에 중요해요.

기본 전략은 WAL이 약 1000페이지가 될 때까지 여러 쓰기 트랜잭션이 쌓이게 두고, 그 후에는 WAL이 1000페이지 미만으로 줄 때까지 커밋마다 체크포인트를 실행하는 거예요. 기본적으로 커밋을 수행한 그 스레드가 자동으로 체크포인트를 돌리기 때문에, 대부분의 커밋은 매우 빠르지만 체크포인트를 유발하는 가끔의 커밋은 훨씬 느릴 수 있어요. 이게 싫다면 자동 체크포인트를 끄고 주기적 체크포인트를 별도 스레드나 프로세스에서 돌리면 돼요.

더 알아보기