WAL 모드 파일 형식
WAL 모드 파일 형식 (WAL-mode File Format)
WAL(Write-Ahead Log) 모드가 unix와 windows에서 어떻게 구현되는지에 대한 저수준 세부 사항을 다루는 문서예요. 데이터베이스 파일, "-wal" 파일, "-shm"(wal-index) 파일의 구조와 잠금 프로토콜, 복구 절차를 설명해요.
본문
이 문서는 WAL 모드가 unix와 windows에서 어떻게 구현되는지에 대한 저수준 세부 사항을 설명해요.
별도의 파일 형식 문서는 데이터베이스 파일과 WAL 모드에서 사용되는 write-head log 파일의 구조에 대한 세부 사항을 제공해요. 하지만 잠금 프로토콜과 WAL-index 형식의 세부 사항은 개별 VFS 구현의 재량에 맡겨져 있기 때문에 의도적으로 생략되어 있어요. 이 문서는 unix와 windows VFS에 대해 그 빠진 세부 사항을 채워넣어요.
완전성을 위해, 파일 형식 문서와 그 외 곳에 있는 고수준 형식 정보 중 WAL 모드 처리와 관련된 일부를 여기에도 옮겨 적었어요.
1. 디스크의 파일들 (Files On Disk)
활발히 사용 중일 때 WAL 모드 데이터베이스의 상태는 세 개의 별도 파일로 기술돼요:
- 임의의 이름 "X"를 가진 메인 데이터베이스 파일.
- 보통 "X-wal"이라고 불리는 write-ahead log 파일.
- 보통 "X-shm"이라고 불리는 wal-index 파일.
1.1. 메인 데이터베이스 파일 (The Main Database File)
메인 데이터베이스 파일의 형식은 파일 형식 문서에 설명된 대로예요. 메인 데이터베이스의 오프셋 18과 19에 있는 파일 형식 버전 번호는 데이터베이스가 WAL 모드임을 나타내기 위해 둘 다 2여야 해요. 메인 데이터베이스는 기본 파일 시스템이 허용하는 임의의 이름을 가질 수 있어요. 특별한 파일 접미사는 필요 없지만, ".db", ".sqlite", ".sqlite3"이 인기 있는 선택인 것 같아요.
1.2. Write-Ahead-Log 또는 "-wal" 파일 (The Write-Ahead-Log or "-wal" File)
write-ahead log 또는 "wal" 파일은 커밋되었지만 아직 메인 데이터베이스에 적용되지 않은 트랜잭션을 기록하는 롤포워드(roll-forward) 저널이에요. wal 파일 형식에 대한 세부 사항은 메인 파일 형식 문서의 WAL 형식 하위 절에 설명되어 있어요. wal 파일의 이름은 메인 데이터베이스 파일 이름 끝에 "wal" 4자를 덧붙여 만들어져요. 단, 8+3 파일 시스템에서는 그러한 이름이 허용되지 않아, 그 경우 파일 접미사는 ".WAL"로 바뀌어요. 하지만 8+3 파일 시스템이 점점 드물어져서 그 예외적인 경우는 보통 무시할 수 있어요.
1.3. Wal-Index 또는 "-shm" 파일 (The Wal-Index or "-shm" file)
wal-index 파일 또는 "shm" 파일은 실제로 파일로 사용되지 않아요. 대신 각 데이터베이스 클라이언트가 shm 파일을 mmap하고, 이것을 데이터베이스 접근을 조정하는 공유 메모리와 wal 파일 안의 프레임을 빠르게 찾는 캐시로 사용해요. shm 파일의 이름은 메인 데이터베이스 파일 이름에 "shm" 4자를 덧붙인 것이에요. 또는 8+3 파일 시스템에서는 메인 데이터베이스 파일의 접미사를 ".SHM"으로 바꾼 것이에요.
shm 파일은 데이터베이스 내용을 담지 않고, 충돌 후 데이터베이스를 복구하는 데 필요하지 않아요. 그 때문에 고요한(quiescent) 데이터베이스에 처음 연결하는 클라이언트는 shm 파일이 존재하면 보통 그것을 잘라내요(truncate). shm 파일의 내용은 충돌을 넘어 보존될 필요가 없으므로, shm 파일은 절대 fsync()로 디스크에 기록되지 않아요. 실제로, SQLite가 shm 파일을 디스크에 영속화하지 말고 항상 캐시 메모리에 보관하라고 운영체제에 말할 수 있는 메커니즘이 있다면, SQLite는 그 메커니즘을 사용해 shm 파일과 관련된 불필요한 디스크 I/O를 피할 거예요. 하지만 표준 posix에는 그런 메커니즘이 없어요.
shm은 동시 클라이언트 간의 접근을 조정하는 데만 사용되기 때문에, 최적화로 배타적 잠금 모드(exclusive locking mode)가 설정되면 shm 파일은 생략돼요. 배타적 잠금 모드가 설정되면 SQLite는 메모리 매핑된 shm 파일 대신 힙(heap) 메모리를 사용해요.
1.4. 파일 라이프사이클 (File Lifecycles)
WAL 모드 데이터베이스가 활발히 사용 중일 때 위 세 파일이 보통 모두 존재해요. 단, 배타적 잠금 모드가 설정되면 Wal-Index 파일은 생략돼요.
데이터베이스를 사용하는 마지막 클라이언트가 sqlite3_close()를 호출해 깨끗하게 종료하면, wal 파일의 모든 정보를 메인 데이터베이스로 옮기기 위해 체크포인트가 자동으로 실행되고, shm 파일과 wal 파일이 둘 다 unlink돼요. 따라서 데이터베이스가 어떤 클라이언트도 사용하지 않을 때는 보통 메인 데이터베이스 파일만 디스크에 존재해요. 하지만 마지막 클라이언트가 종료 전에 sqlite3_close()를 호출하지 않았거나, 마지막으로 연결을 끊은 클라이언트가 읽기 전용 클라이언트였다면, 최종 정리 작업이 수행되지 않아 데이터베이스가 사용되지 않을 때도 shm과 wal 파일이 디스크에 여전히 존재할 수 있어요.
1.5. 변형 (Variations)
PRAGMA locking_mode=EXCLUSIVE(배타적 잠금 모드)가 설정되면 한 번에 한 클라이언트만 데이터베이스를 열 수 있어요. 한 클라이언트만 데이터베이스를 사용할 수 있으므로 shm 파일은 생략돼요. 그 단일 클라이언트는 메모리 매핑된 shm 파일 대신 힙 메모리의 버퍼를 사용해요.
읽기/쓰기 클라이언트가 종료 전에 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL)을 호출하면, 종료 시 체크포인트는 여전히 실행되지만 shm 파일과 wal 파일은 삭제되지 않아요. 이렇게 하면 이후의 읽기 전용 클라이언트가 데이터베이스에 연결해 읽을 수 있어요.
2. WAL-Index 파일 형식 (The WAL-Index File Format)
WAL-index 또는 "shm" 파일은 여러 클라이언트가 데이터베이스에 접근하는 것을 조정하고, 클라이언트가 wal 파일 안의 프레임을 빠르게 찾는 데 도움이 되는 캐시로 사용돼요.
shm 파일은 복구에 관여하지 않으므로 머신 바이트 순서에 독립적일 필요가 없어요. 따라서 shm 파일의 숫자 값은 메인 데이터베이스 파일과 wal 파일에서처럼 특정 크로스 플랫폼 바이트 순서로 변환되는 대신, 호스트 컴퓨터의 네이티브 바이트 순서로 기록돼요.
shm 파일은 하나 이상의 해시 테이블로 구성되며, 각 해시 테이블은 32768바이트 크기예요. 단, 136바이트 헤더가 첫 번째 해시 테이블의 앞부분에서 잘려 나가서, 첫 번째 해시 테이블은 32632바이트 크기뿐이에요. shm 파일의 총 크기는 항상 32768의 배수예요. 대부분의 경우 shm 파일의 총 크기는 정확히 32768바이트예요. shm 파일이 단일 해시 테이블을 넘어 커질 필요가 있는 것은 wal 파일이 아주 커졌을 때(4079 프레임 초과)뿐이에요. 기본 자동 체크포인트 임계값이 1000이므로, WAL 파일은 shm 파일이 커지게 하는 4079 임계값에 거의 도달하지 않아요.
2.1. WAL-Index 헤더 (The WAL-Index Header)
shm 파일의 처음 136바이트는 헤더예요. shm 헤더는 다음과 같은 세 주요 부분으로 나뉘어요:
WAL-Index 헤더 구분 | 바이트 | 설명 | |---|---| | 0..47 | WAL Index 정보의 첫 번째 사본 | | 48..95 | WAL Index 정보의 두 번째 사본 | | 96..135 | 체크포인트 정보와 잠금 |
WAL 헤더에서 복사된 salt 값을 제외한 shm 헤더의 개별 필드는 호스트 머신의 네이티브 바이트 순서의 부호 없는 정수예요. salt 값은 WAL 헤더에서 정확히 복사된 것으로, WAL 파일이 사용하는 어떤 바이트 순서든 그대로예요. 정수 크기는 8, 16, 32 또는 64비트일 수 있어요. shm 헤더의 개별 필드를 자세히 분해하면 다음과 같아요:
WAL-Index 헤더 세부 사항
| 바이트 | 이름 | 의미 |
|---|---|---|
| 0..3 | iVersion | WAL-index 형식 버전 번호. 항상 3007000. |
| 4..7 | | 사용하지 않는 패딩 공간. 0이어야 함. |
| 8..11 | iChange | 각 트랜잭션과 함께 증가하는 부호 없는 정수 카운터. |
| 12 | isInit | "isInit" 플래그. shm 파일이 초기화되면 1. |
| 13 | bigEndCksum | WAL 파일이 빅엔디언 체크섬을 사용하면 참. WAL이 리틀엔디언 체크섬을 사용하면 0. |
| 14..15 | szPage | 바이트 단위의 데이터베이스 페이지 크기. 페이지 크기가 65536이면 1. |
| 16..19 | mxFrame | WAL 파일의 유효하고 커밋된 프레임 수. |
| 20..23 | nPage | 페이지 단위의 데이터베이스 파일 크기. |
| 24..31 | aFrameCksum | WAL 파일의 마지막 프레임의 체크섬. |
| 32..39 | aSalt | WAL 파일 헤더에서 복사된 두 salt 값. 이 값은 머신의 네이티브 바이트 순서와 다를 수 있는 WAL 파일의 바이트 순서로 되어 있음. |
| 40..47 | aCksum | 이 헤더의 바이트 039에 대한 체크섬. |
| 48..95 | | 이 헤더의 바이트 047의 사본. |
| 96..99 | nBackfill | 이전 체크포인트에 의해 이미 데이터베이스로 백필(backfill)된 WAL 프레임 수. |
| 100..119 | read-mark[0..4] | 다섯 개의 "읽기 표시(read mark)". 각 읽기 표시는 32비트 부호 없는 정수(4바이트). |
| 120..127 | | 8개의 파일 잠금을 위해 마련된 사용하지 않는 공간. |
| 128..132 | nBackfillAttempted | 백필을 시도했지만 성공적으로 백필되지 않았을 수 있는 WAL 프레임 수. |
| 132..136 | | 추가 확장을 위해 예약된 사용하지 않는 공간. |
2.1.1. mxFrame 필드
오프셋 16(그리고 오프셋 64에 반복)의 32비트 부호 없는 정수는 WAL의 유효한 프레임 수예요. WAL 프레임은 1부터 번호가 매겨지므로 mxFrame은 WAL에서 마지막 유효한 커밋 프레임의 인덱스이기도 해요. 커밋 프레임은 프레임 헤더의 바이트 4~7에 "데이터베이스 크기" 값이 0이 아닌 프레임으로, 트랜잭션의 끝을 나타내요.
mxFrame 필드가 0이면 WAL이 비어 있고 모든 내용을 데이터베이스 파일에서 직접 얻어야 함을 나타내요.
mxFrame이 nBackfill과 같으면 WAL의 모든 내용이 데이터베이스로 다시 쓰여졌음을 나타내요. 그 경우 모든 내용을 데이터베이스에서 직접 읽을 수 있어요. 게다가 N>0에 대해 다른 연결이 WAL_READ_LOCK(N)에 잠금을 보유하지 않으면 다음 쓰기가 자유롭게 WAL을 리셋할 수 있어요.
mxFrame 값은 항상 nBackfill과 nBackfillAttempted 둘 다보다 크거나 같아요.
2.1.2. nBackfill 필드
WAL-index 헤더의 오프셋 128에 있는 32비트 부호 없는 정수를 "nBackfill"이라고 불러요. 이 필드는 메인 데이터베이스로 복사된 WAL 파일의 프레임 수를 담아요.
nBackfill 수는 mxFrame보다 결코 크지 않아요. nBackfill이 mxFrame과 같으면 WAL 내용이 데이터베이스로 완전히 다시 쓰여졌고, N>0에 대한 어떤 WAL_READ_LOCK(N)에도 잠금이 없으면 WAL을 리셋해도 좋다는 뜻이에요.
nBackfill은 WAL_CKPT_LOCK을 보유하는 동안에만 증가할 수 있어요. 하지만 WAL 리셋 중에는 nBackfill이 0으로 바뀌고, 이는 WAL_WRITE_LOCK을 보유하는 동안 발생해요.
2.1.3. WAL 잠금 (WAL Locks)
sqlite3_io_methods 객체의 xShmLock() 메서드를 사용한 파일 잠금을 지원하기 위해 헤더에 8바이트의 공간이 마련되어 있어요. SQLite는 이 8바이트를 절대 읽지도 쓰지도 않아요. 일부 VFS(예: Windows)가 강제 파일 잠금(mandatory file lock)을 사용해 잠금을 구현할 수 있기 때문이에요.
지원되는 8개의 잠금은 다음과 같아요:
xShmLock()이 제어하는 WAL-Index 잠금 | 이름 | 인덱스 | 파일 오프셋 | |---|---|---| | WAL_WRITE_LOCK | 0 | 120 | | WAL_CKPT_LOCK | 1 | 121 | | WAL_RECOVER_LOCK | 2 | 122 | | WAL_READ_LOCK(0) | 3 | 123 | | WAL_READ_LOCK(1) | 4 | 124 | | WAL_READ_LOCK(2) | 5 | 125 | | WAL_READ_LOCK(3) | 6 | 126 | | WAL_READ_LOCK(4) | 7 | 127 |
2.2. WAL-Index 해시 테이블 (WAL-Index Hash Tables)
shm 파일의 해시 테이블은 다음 질문에 빠르게 답하도록 설계되었어요:
FindFrame(P,M): 페이지 번호 P와 최대 WAL 프레임 인덱스 M이 주어졌을 때, M을 초과하지 않는 페이지 P에 대한 가장 큰 WAL 프레임 인덱스를 반환하거나, M을 초과하지 않는 페이지 P에 대한 프레임이 없으면 NULL을 반환한다.
데이터 타입 "u8", "u16", "u32"가 각각 길이 8, 16, 32비트의 부호 없는 정수를 의미한다고 하자. 그러면 shm 파일의 첫 번째 32768바이트 단위는 다음과 같이 구성돼요:
u8 aWalIndexHeader[136];
u32 aPgno[4062];
u16 aHash[8192];
shm 파일의 두 번째와 그 이후의 모든 32768바이트 단위는 다음과 같아요:
u32 aPgno[4096];
u16 aHash[8192];
집합적으로 aPgno 항목들은 WAL 파일의 모든 프레임에 저장된 데이터베이스 페이지 번호를 기록해요. 첫 번째 해시 테이블의 aPgno[0] 항목은 WAL 파일의 맨 첫 프레임에 저장된 데이터베이스 페이지 번호를 기록해요. 첫 번째 해시 테이블의 aPgno[i] 항목은 WAL 파일의 i번째 프레임의 데이터베이스 페이지 번호예요. 두 번째 해시 테이블의 aPgno[k] 항목은 WAL 파일의 (k+4062)번째 프레임의 데이터베이스 페이지 번호예요. shm 파일의 n번째(n>1) 32768바이트 해시 테이블의 aPgno[k] 항목은 WAL 파일의 (k+4062+4096*(n-2))번째 프레임에 저장된 데이터베이스 페이지 번호를 담아요.
aPgno 값을 살짝 다른 방식으로 설명하면: 모든 aPgno 값을 연속 배열로 생각하면, WAL 파일의 i번째 프레임에 저장된 데이터베이스 페이지 번호는 aPgno[i]에 저장돼요. 물론 aPgno는 연속 배열이 아니에요. 첫 4062개 항목은 shm 파일의 첫 32768바이트 단위에 있고, 이후 값들은 shm 파일의 이후 단위에 4096개 항목 덩어리로 있어요.
FindFrame(P,M)을 계산하는 한 가지 방법은 M번째 항목에서 시작해 앞쪽으로 거슬러 aPgno 배열을 스캔하고 aPgno[J]==P인 J를 반환하는 것이에요. 그런 알고리즘도 동작하고, 페이지 번호 P를 가진 최신 프레임을 위해 WAL 파일 전체를 검색하는 것보다 빠를 거예요. 하지만 aHash 구조를 사용하면 검색을 훨씬 더 빠르게 할 수 있어요.
데이터베이스 페이지 번호 P는 다음 해시 함수를 사용해 해시 값으로 매핑돼요:
h = (P * 383)%8192
이 함수는 모든 페이지 번호를 0부터 8191까지(포함)의 정수로 매핑해요. 각 32768바이트 shm 파일 단위의 aHash 필드는 P 값을 같은 단위의 aPgno 필드의 인덱스로 다음과 같이 매핑해요:
- 해시 값을 계산한다: h = P * 383
- X를 모든 j에 대해 aPgno[j%8192]!=0인 연속 정수 집합 {h, h+1, h+2, ..., h+N} 중 가장 큰 집합이라고 하자. aPgno[h%8192]==0이면 X 집합은 비어 있어요. X 집합은 h%8192 값으로 시작해 aPgno[h%8192] 항목을 X에 추가하고 첫 번째 aPgno[h%8192] 항목이 0이 나올 때까지 h를 증가시키면 쉽게 계산돼요.
- X 집합은 FindFrame(P,M) 함수의 해답이 될 수 있는 현재 32768바이트 shm 파일 단위의 aPgno 안 모든 항목의 인덱스를 담아요. 이 항목 각각은 aPgno 값이 P이고 프레임 번호가 M을 초과하지 않는지 확인하기 위해 개별적으로 검사해야 해요. 그 두 검사를 통과하는 가장 큰 프레임 번호가 답이에요.
aPgno 배열의 각 항목은 aHash 배열에 정확히 하나씩 대응하는 항목을 가져요. aHash에는 aPgno보다 더 많은 사용 가능한 슬롯이 있어요. aHash의 사용하지 않는 슬롯은 0으로 채워져요. 그리고 aHash에 사용하지 않는 슬롯이 반드시 있으므로 X를 계산하는 루프가 반드시 종료됨이 보장돼요. X의 예상 크기는 2보다 작아요. 최악의 경우 X가 aPgno의 항목 수와 같아져서 알고리즘이 aPgno의 선형 스캔과 거의 같은 속도로 실행돼요. 하지만 그 최악의 성능은 극히 드물어요. 보통 X의 크기는 작고, aHash 배열을 사용하면 FindFrame(P,M)을 훨씬 빠르게 계산할 수 있어요.
해시 조회 알고리즘을 설명하는 대안적인 방법은 다음과 같아요: h = (P * 383)%8192로 시작해 aHash[h]와 이후 항목을 보고, h가 8192에 도달하면 0으로 되돌아가며(aHash[h]==0인 항목을 찾을 때까지) 진행해요. 페이지 번호 P를 가진 모든 aPgno 항목은 이렇게 계산된 aHash[h] 값 중 하나인 인덱스를 가질 거예요. 하지만 계산된 모든 aHash[h] 값이 일치 기준을 충족하는 것은 아니므로, 그것들을 독립적으로 검사해야 해요. 속도 이점은 보통 이 h 값 집합이 매우 작기 때문에 발생해요.
shm 파일의 각 32768바이트 단위는 자신만의 aHash와 aPgno 배열을 가진다는 점에 주의하세요. 단일 단위의 aHash 배열은 같은 단위의 aPgno 항목을 찾는 데만 도움이 돼요. 전체 FindFrame(P,M) 함수는 최신 단위부터 시작해 답을 찾을 때까지 이전 단위로 거슬러 해시 조회를 해야 해요.
2.3. 잠금 매트릭스 (Locking Matrix)
WAL 모드에서의 접근은 sqlite3_io_methods 객체의 xLock과 xUnlock 메서드가 제어하는 레거시 DELETE-모드 잠금과 sqlite3_io_methods 객체의 xShmLock 메서드가 제어하는 WAL 잠금을 둘 다 사용해 조정돼요.
개념적으로 단일 DELETE-모드 잠금만 있어요. 단일 데이터베이스 연결에 대한 DELETE-모드 잠금은 다음 상태 중 정확히 하나일 수 있어요:
- SQLITE_LOCK_NONE (잠금 해제)
- SQLITE_LOCK_SHARED (읽기 중)
- SQLITE_LOCK_RESERVED (읽기 중, 쓰기 대기 중)
- SQLITE_LOCK_PENDING (새 읽기 차단, 쓰기 대기 중)
- SQLITE_LOCK_EXCLUSIVE (쓰기 중)
DELETE-모드 잠금은 메인 데이터베이스 파일의 lock-byte 페이지에 저장돼요. WAL 모드 데이터베이스에서는 SQLITE_LOCK_SHARED와 SQLITE_LOCK_EXCLUSIVE만 중요한 요소예요. 다른 잠금 상태는 롤백 모드에서 사용되지만 WAL 모드에서는 사용되지 않아요.
WAL 모드 잠금은 위에서 설명했어요.
2.3.1. 다양한 잠금의 사용 방법 (How the various locks are used)
다음 규칙들은 각 잠금이 어떻게 사용되는지 보여줘요.
- SQLITE_LOCK_SHARED
모든 연결은 WAL 모드 데이터베이스에 연결되어 있는 동안 연속적으로 SQLITE_LOCK_SHARED를 보유해요. 이는 읽기/쓰기 연결과 읽기 전용 연결 모두에 해당해요. SQLITE_LOCK_SHARED 잠금은 트랜잭션 안에 있지 않은 연결도 보유해요. 이것은 각 트랜잭션 끝에 SQLITE_LOCK_SHARED가 해제되는 롤백 모드와 다르다는 점이에요.
- SQLITE_LOCK_EXCLUSIVE
연결은 WAL 모드와 다양한 롤백 모드 사이를 전환할 때 배타적 잠금을 보유해요. 연결은 WAL 모드에서 연결을 끊을 때도 EXCLUSIVE 잠금을 얻으려 시도할 수 있어요. 연결이 EXCLUSIVE 잠금을 얻을 수 있다면, 그것은 데이터베이스에 대한 유일한 연결이라는 뜻이므로 체크포인트를 시도한 다음 WAL-index와 WAL 파일을 삭제할 수 있어요.
연결이 메인 데이터베이스에 SHARED 잠금을 보유하고 있으면, 다른 연결이 EXCLUSIVE 잠금을 얻는 것을 막고, 이는 차례로 다른 사용자들이 사용하는 도중 WAL-index와 WAL 파일이 삭제되는 것을 막고, 다른 사용자가 WAL 모드로 데이터베이스에 접근하는 동안 WAL 모드 밖으로의 전환을 막아요.
- WAL_WRITE_LOCK
WAL_WRITE_LOCK은 배타적으로만 잠겨요. WAL_WRITE_LOCK에는 공유 잠금이 절대 걸리지 않아요.
WAL 끝에 내용을 덧붙이는 어떤 연결이라도 EXCLUSIVE WAL_WRITE_LOCK을 보유해요. 따라서 한 번에 한 프로세스만 WAL에 내용을 덧붙일 수 있어요. 쓰기의 결과로 WAL 리셋이 발생하면, 이 잠금을 보유하는 동안 WAL-index 헤더의 nBackfill 필드가 0으로 리셋돼요.
연결이 공유 WAL-index에 대해 복구를 실행할 때도 여러 다른 잠금 바이트와 함께 WAL_WRITE_LOCK에 EXCLUSIVE가 걸려요.
- WAL_CKPT_LOCK
WAL_CKPT_LOCK은 배타적으로만 잠겨요. WAL_CKPT_LOCK에는 공유 잠금이 절대 걸리지 않아요.
체크포인트를 실행하는 어떤 연결이라도 EXCLUSIVE WAL_CKPT_LOCK을 보유해요. 이 배타적 잠금을 보유하는 동안 WAL-index 헤더의 nBackfill 필드는 증가할 수 있지만 감소할 수는 없어요.
연결이 공유 WAL-index에 대해 복구를 실행할 때도 여러 다른 잠금 바이트와 함께 WAL_CKPT_LOCK에 EXCLUSIVE가 걸려요.
- WAL_RECOVER_LOCK
WAL_RECOVER_LOCK은 배타적으로만 잠겨요. WAL_RECOVER_LOCK에는 공유 잠금이 절대 걸리지 않아요.
공유 WAL-index를 재구성하기 위해 복구를 실행하는 어떤 연결이라도 EXCLUSIVE WAL_RECOVER_LOCK을 보유해요.
자신의 개인 힙 메모리 WAL-index를 재구성하는 읽기 전용 연결은 이 잠금을 보유하지 않아요. (보유할 수 없어요. 읽기 전용 연결은 어떤 배타적 잠금도 보유할 수 없기 때문이에요.) 이 잠금은 메모리 매핑된 SHM 파일에 포함된 전역 공유 WAL-index를 재구성할 때만 보유돼요.
이 바이트를 잠그는 것에 더해, 복구를 실행하는 연결은 WAL_READ_LOCK(0)을 제외한 모든 다른 WAL 잠금에도 배타적 잠금을 얻어요.
- WAL_READ_LOCK(N)
0부터 4까지 다섯 개의 별도 읽기 잠금이 있어요. 읽기 잠금은 SHARED 또는 EXCLUSIVE일 수 있어요. 연결은 트랜잭션 안에 있는 동안 읽기 잠금 바이트 중 하나에 공유 잠금을 얻어요. 연결은 또한 해당 읽기 표시(read-mark) 값들을 갱신하는 짧은 순간에 읽기 잠금 하나씩에 차례로 배타적 잠금을 얻어요. 복구를 실행할 때 읽기 잠금 1~4는 배타적으로 보유돼요.
각 읽기 잠금 바이트는 WAL-index 헤더의 바이트 100~119에 있는 다섯 개의 32비트 읽기 표시 정수 중 하나에 다음과 같이 대응해요:
| 잠금 이름 | 잠금 오프셋 | 읽기 표시 이름 | 읽기 표시 오프셋 |
|---|---|---|---|
| WAL_READ_LOCK(0) | 123 | read-mark[0] | 100..103 |
| WAL_READ_LOCK(1) | 124 | read-mark[1] | 104..107 |
| WAL_READ_LOCK(2) | 125 | read-mark[2] | 108..111 |
| WAL_READ_LOCK(3) | 126 | read-mark[3] | 112..115 |
| WAL_READ_LOCK(4) | 127 | read-mark[4] | 116..119 |
연결이 WAL_READ_LOCK(N)에 공유 잠금을 보유할 때, 그것은 WAL의 첫 read-mark[N] 항목에 의해 수정된 데이터베이스 페이지에 대해 WAL 파일이 아닌 WAL을 사용하겠다는 연결의 약속이에요. read-mark[0]은 항상 0이에요. 연결이 WAL_READ_LOCK(0)에 공유 잠금을 보유하면, 그 연결은 원하면 첫 mxFrame 프레임까지 WAL 파일의 더 많은 부분을 사용할 수 있어요. 또한 공유 잠금을 보유하는 연결은 첫 read-mark[N] 프레임을 넘어 WAL 내용은 메인 데이터베이스로 옮기지 않도록 체크포인트를 강제해요. N>0이면 연결은 원한다면 read-mark[N]을 넘어 WAL 파일의 더 많은 부분을 (첫 mxFrame 프레임까지) 자유롭게 사용할 수 있어요. 하지만 연결이 WAL_READ_LOCK(0)에 공유 잠금을 보유할 때, 그것은 WAL에서 내용을 절대 읽지 않고 모든 내용을 메인 데이터베이스에서 직접 얻겠다는 약속이에요.
체크포인트가 실행될 때 WAL_READ_LOCK(N)에 잠금이 보이면, 첫 read-mark[N] 프레임보다 많이 WAL 내용을 메인 데이터베이스로 옮겨서는 안 돼요. 그렇게 하면 잠금을 보유한 프로세스가 메인 데이터베이스 파일에서 읽을 수 있기를 기대하는 내용을 덮어쓰게 되기 때문이에요. 그 결과, WAL 파일에 read-mark[N]보다 많은 프레임이 있으면(다른 프로세스가 WAL_READ_LOCK(N)을 보유한 어떤 read-mark에 대해 mxFrame>read-mark[N]이면) 체크포인트는 완료까지 실행될 수 없어요.
쓰기가 WAL을 리셋하려면 N>0에 대한 WAL_READ_LOCK(N)에 잠금이 없는지 확인해야 해요. 그런 잠금은 다른 연결이 여전히 현재 WAL 파일을 사용 중임을 나타내고, WAL 리셋이 그 다른 연결들로부터 내용을 삭제할 것이기 때문이에요. 다른 연결이 WAL_READ_LOCK(0)을 보유하고 있으면 WAL 리셋이 발생해도 괜찮아요. WAL_READ_LOCK(0)을 보유함으로써 그 다른 연결들이 WAL의 어떤 내용도 사용하지 않겠다고 약속하기 때문이에요.
2.3.2. 잠금이 필요한 작업과 그 작업들이 사용하는 잠금 (Operations that require locks and which locks those operations use)
- WAL 모드로/에서의 전환 (Transition into and out of WAL-mode)
WAL 모드로 또는 WAL 모드에서 전환하려는 연결은 SQLITE_LOCK_EXCLUSIVE 잠금을 보유해야 해요. 따라서 WAL 모드로 전환하는 것은 다른 쓰기 트랜잭션과 똑같아요. 롤백 모드의 모든 쓰기 트랜잭션은 SQLITE_LOCK_EXCLUSIVE 잠금을 요구하기 때문이에요. 데이터베이스 파일이 이미 WAL 모드라면(따라서 다시 롤백 모드로 바꾸려는 것이라면) 데이터베이스에 두 개 이상의 연결이 있다면, 그 연결 각각이 SQLITE_LOCK_SHARED 잠금을 보유할 거예요. 즉 SQLITE_LOCK_EXCLUSIVE를 얻을 수 없고 WAL 모드 밖으로의 전환이 허용되지 않아요. 이것은 한 연결이 다른 연결이 WAL 모드를 사용하는 동안 WAL 모드를 삭제하는 것을 막아요. 또한 데이터베이스를 WAL 모드에서 롤백 모드로 옮기는 유일한 방법은 데이터베이스에 대한 연결을 하나만 남기고 모두 닫는 것임을 의미해요.
- WAL 모드 데이터베이스에 대한 연결 닫기 (Close a connection to a WAL mode database)
데이터베이스 연결이 (sqlite3_close() 또는 sqlite3_close_v2()를 통해) 닫힐 때 SQLITE_LOCK_EXCLUSIVE를 획득하려 시도해요. 이 시도가 성공하면 닫고 있는 연결이 데이터베이스에 대한 마지막 연결이라는 뜻이에요. 그 경우 WAL과 WAL-index 파일을 정리하는 것이 바람직하므로, 닫는 연결은 (SQLITE_LOCK_EXCLUSIVE를 보유한 채) 체크포인트를 실행하고 이것이 WAL과 WAL-index 파일을 둘 다 삭제해요. SQLITE_LOCK_EXCLUSIVE는 WAL과 WAL-index 파일이 둘 다 삭제된 후에야 해제돼요.
응용 프로그램이 닫기 전에 데이터베이스 연결에 sqlite3_file_control(SQLITE_FCNTL_PERSIST_WAL)을 호출했다면, 최종 체크포인트는 여전히 실행되지만 WAL과 WAL-index 파일은 평소처럼 삭제되지 않아요. 이는 데이터베이스, WAL 또는 WAL-index 파일에 쓰기 권한이 없는 다른 프로세스가 데이터베이스를 읽기 전용으로 열 수 있는 상태로 데이터베이스를 남겨요. WAL과 WAL-index 파일이 없으면, 그 파일들을 만들고 초기화할 권한이 없는 프로세스는 데이터베이스가 immutable 쿼리 매개변수로 immutable로 지정되지 않는 한 데이터베이스를 열 수 없어요.
- 복구 중 전역 공유 WAL-index 재구성 (Reconstruct the global shared WAL-index during recovery)
WAL_READ_LOCK(0)을 제외한 모든 WAL-index 잠금은 복구 동안 전역 공유 WAL-index를 재구성하는 동안 배타적으로 보유돼요.
- WAL 끝에 새 트랜잭션 덧붙이기 (Append a new transaction to the end of the WAL)
WAL 파일 끝에 새 프레임을 추가하는 동안 WAL_WRITE_LOCK에 배타적 잠금이 보유돼요.
- 트랜잭션의 일부로 데이터베이스와 WAL에서 내용 읽기 (Read content from the database and WAL as part of a transaction)
- 체크포인트 실행 (Run a checkpoint)
- WAL 파일 리셋 (Reset the WAL file)
WAL 리셋은 WAL을 되감고 새 프레임을 처음에 추가하기 시작하는 것을 의미해요. 이는 mxFrame이 nBackfill과 같고 WAL_READ_LOCK(1)부터 WAL_READ_LOCK(4)에 잠금이 없는 WAL에 새 프레임을 추가하는 동안 발생해요. WAL_WRITE_LOCK이 보유돼요.
3. 복구 (Recovery)
복구는 WAL-index를 WAL과 동기화되도록 재구성하는 과정이에요.
복구는 WAL 모드 데이터베이스에 연결하는 첫 번째 스레드가 실행해요. 복구는 WAL-index가 WAL 파일을 정확히 기술하도록 복원해요. 첫 번째 스레드가 데이터베이스에 연결할 때 WAL 파일이 없으면 복구할 것이 없지만, WAL-index를 초기화하기 위해 복구 프로세스는 여전히 실행돼요.
WAL-index가 메모리 매핑된 파일로 구현되고 그 파일이 연결하는 첫 번째 스레드에 읽기 전용이라면, 그 스레드는 개인 힙 메모리 ersatz(대체) WAL-index를 만들고 그 개인 WAL-index를 채우기 위해 복구 루틴을 실행해요. 같은 데이터가 결과로 나오지만, 공개 공유 메모리 영역에 쓰이는 대신 개인적으로 보유돼요.
복구는 WAL을 처음부터 끝까지 한 번 통과하는 것으로 동작해요. 읽히는 WAL의 각 프레임에서 체크섬이 검증돼요. 스캔은 파일 끝 또는 첫 번째 유효하지 않은 체크섬에서 멈춰요. mxFrame 필드는 WAL의 마지막 유효한 커밋 프레임의 인덱스로 설정돼요. WAL 프레임 번호는 1부터 시작하므로 mxFrame은 WAL의 유효한 프레임 수이기도 해요. "커밋 프레임"은 프레임 헤더의 바이트 4~7에 0이 아닌 값이 있는 프레임이에요. 복구 절차는 WAL의 프레임 중 얼마나 많은 프레임이 이전에 데이터베이스로 복사되었는지 알 방법이 없으므로, nBackfill 값을 0으로 초기화해요.
전역 공유 메모리 WAL-index의 복구 동안 WAL_WRITE_LOCK, WAL_CKPT_LOCK, WAL_RECOVER_LOCK, WAL_READ_LOCK(1)부터 WAL_READ_LOCK(4)에 배타적 잠금이 보유돼요. 즉 WAL-index와 관련된 모든 잠금 중 WAL_READ_LOCK(0)을 제외한 모든 잠금이 배타적으로 보유돼요. 이것은 복구가 완료될 때까지 어떤 다른 스레드도 데이터베이스에 쓰고 WAL에 보관된 트랜잭션을 읽는 것을 막아요.