SQLite의 트랜잭션
(reorchestrate.com)- 최근 몇 년 동안 SQLite는 서버 프로세스의 백엔드 용도로 강력한 인-프로세스 고신뢰성 SQL 데이터베이스 엔진으로 각광받음
- 전통적인 클라이언트 또는 엣지 애플리케이션 역할이 아닌 이런 용도로 SQLite가 사용되는 것을 SQLite 개발자들이 거의 적극적으로 만류하고 있음에도 불구하고 인기가 급증함
내가 SQLite에 관심을 갖게 된 주요 이유들:
- 개념적으로 단순함: 기본키로 파티션된 행/튜플의 B-트리를 상상해 봄. 이를 디스크에 안정적으로 지속시키기 위해 광범위하게 테스트되었고, SQL 상호작용 계층을 추가함
- Litestream을 통해 실용적인 백업 전략을 수립할 수 있음. 원격 위치로 WAL을 백업하고 지속적으로 복제함. 백업은 간단한 명령어로 시작시 자동 복원 가능함.
- 오프라인에서 실행 가능한 완전한 개발 환경을 여전히 좋아함
- file::memory:를 통해 인-메모리 작업이 가능해 필요시 테스트 코드가 인스턴스를 쉽게 시작하고 종료할 수 있음
Single-writer 제한
- SQLite 개발자들에 의해 "서버에서의 SQLite 제한"이 잘 문서화되어 있고, 최상의 서버 측 설정이 분석됨. 하지만 눈에 띄는 제한은 고트래픽 웹사이트인데, 이는 쓰기가 많은 웹사이트를 의미함
- WAL 모드에서 SQLite는 설계상 단일 Writer를 사용함. 이는 동시에 최대 1개의 쓰기 트랜잭션과 여러 읽기전용 트랜잭션을 허용함
- 이 설계는 고트래픽 쓰기 집중 웹사이트의 병목을 해당 단일 Writer의 처리량 관리에 두고 있음. 이는 현대 기술의 핵심 구성요소 중 하나로 다시 돌아옴
SQLite
- SQLite는 기본적으로 엄격한 SERIALIZABLE Isolated 트랜잭션을 제공함. 이는 Isolation 보증의 가장 강력한 수준임
- 단일 Writer를 사용함으로써 SQLite는 쓰기 트랜잭션이 진행되는 동안 기본 데이터가 변경되지 않았음을 쉽게 보장할 수 있는 Pessimistic concurrency control 형식을 사용하고 있음
Postgres
- Postgres는 실제로 SQL 표준에서 정의된
SERIALIZABLE
기본값과 다르며, 더 완화된READ COMMITTED
를 선택함(훨씬 더 복잡한 Multiversion concurrency control에도 불구하고)- 이러한 엄격성 감소는 non-repeatable reads의 위험이 있음. 즉, 동일한 트랜잭션 내에서도 다른 COMMITTED 트랜잭션에 의해 백그라운드에서 값이 변경되면 동일한 읽기 쿼리를 여러 번 실행할 때 다른 결과를 검색할 수 있음
- 이 Isolation 수준을 선택함으로써 Postgres는 트랜잭션이 오래된 데이터로 작동할 위험을 열어둠. 개발자는 이 사실을 명심해야 함
-
SERIALIZABLE
로 설정하면 Postgres는 optimistic-concurrency control 체계를 사용하여 transaction 중에 액세스한 데이터를 추적하고 커밋 전에 변경되지 않았는지 확인함- Postgres는 메모리 사용량을 관리하기 위해 transaction에 따라 row level 또는 page level의 잠금을 기반으로 이를 수행함
- 이 패턴은 transaction이 커밋될 때 transaction에 의해 모니터링되는 데이터가 더 세분화될수록 변경될 가능성이 적기 때문에 기본 데이터가 변경되지 않을 것으로 예상되기 때문에
optimistic
이라고 함
FoundationDB
- 트랜잭션은 관계형 데이터베이스에만 국한되지 않음. 분산 키-값 저장소에서 SERIALIZABLE 보장을 달성하기 위해 Optimistic concurrency control 사용
- NoSQL이 등장했을 당시, ACID 보증이 있는 분산 NoSQL 저장소는 일반적이지 않았음. FoundationDB는 트랜잭션 매니페스토를 작성하여 개발자가 ACID 보증으로부터 크게 혜택을 받을 수 있음을 강조함
- FoundationDB는 Optimistic concurrency control을 위한 코드 작성 방법과 때로는 동시 트랜잭션 충돌로 인해 데이터가 변경되고 트랜잭션이 자동으로 재시도된다는 사실에 대한 조언을 제공함
Idempotence
- 멱등 트랜잭션은 한 번 커밋할 때와 두 번 커밋할 때 동일한 효과를 갖는 트랜잭션임
- FoundationDB는 트랜잭션을 멱등으로 만들기 위해 충돌로 인해 트랜잭션을 여러 번 재시도해야 하는 경우 문제 방지를 위한 패턴을 제공함
그렇다면 이 모든 것을 염두에 뒀을때 SQLite는 어떤 옵션을 제공할까?
BEGIN …
SQLite는 개발자들이 WAL 모드에서 DEFERRED vs IMMEDIATE로 축소될 수 있는 IMMEDIATE, EXCLUSIVE, DEFERRED 키워드 형태로 트랜잭션이 동작할 방식을 엔진에 표시할 수 있는 여러 방법을 제공함
DEFERRED
- 트랜잭션이 다른 읽기 또는 쓰기 트랜잭션과 동시에 실행될 수 있는 READ 모드로 시작됨
- DB 상태를 수정하는 쿼리(INSERT, UPDATE, DELETE)가 실행되는 경우에만 차단 READ-WRITE 트랜잭션으로 업그레이드됨
- 업그레이드시 DB가 다른 트랜잭션에 의해 잠겨있으면 SQLITE_BUSY 오류 반환. 클라이언트가 이를 처리해야 함
IMMEDIATE
- 트랜잭션이 즉시 READ-WRITE 모드로 시작됨
- 쓰기 트랜잭션이 이미 실행 중인 경우 즉시 SQLITE_BUSY 반환
- 클라이언트가 처리 방법을 결정해야 함
CONCURRENT
- SQLite에는 트랜잭션을 Pessimistic에서 제한된 Optimistic으로 이동하는 실험 지점이 있음
- 제한된 이유는 Optimistic locking이 행/튜플 수준이 아닌 DB 페이지 수준(기본 4096 bytes)에서 작동하기 때문
- CONCURRENT 모드에서 SQLite는 여러 쓰기 트랜잭션을 동시에 활성화할 수 있지만, 커밋 전에 트랜잭션 수행 중 액세스한 페이지가 트랜잭션 시작 이후 변경되지 않았는지 확인함
- 충돌이 발생하지 않으면 변경 사항이 순차적으로 커밋되고 엄격한 SERIALIZABLE 보장을 달성함. 충돌 발생시 SQLITE_BUSY 반환
HC-Tree
- SQLite의 또 다른 실험 브랜치는 [HC-Tree]로, 낙관적 행/튜플 수준 잠금을 제공하는 것을 목표로 하는 진행 중인 작업임. 흥미로운 결과 중 하나는 그러한 설계의 성능 이점을
BEGIN CONCURRENT
브랜치와 비교하여 보여주는 우수한 벤치마크 세트를 제공한다는 것
그들의 벤치마킹 접근 방식을 가져와서 표준 옵션에 대해 실행해 보는 것은 어떨까?
벤치마킹
nUpdate=1, nScan=0
- 이 write-only 트랜잭션은 IMMEDIATE vs DEFERRED의 이점이 분명함을 보여줌. Locking은 즉시 발생하며 트랜잭션은 upgrading 비용의 영향을 받지 않음
- CONCURRENT는 스레드 수가 늘어나고 충돌이 증가함에 따라 처리량 증가를 보여줌
nUpdate=10, nScan=0
- 예상대로 write 일괄 처리는 16 스레드에서 업데이트된 행 수에 많은 도움이 됨. CONCURRENT는 ~12k/sec에서 ~19k/sec로 증가함
- IMMEDIATE vs DEFERRED는 덜 중요해짐. 트랜잭션 업그레이드 비용보다 업데이트 자체 비용이 더 중요해지기 때문
nUpdate=1, nScan=10
- 이 트랜잭션은 random read로 인해 page 수준 CONCURRENT locking의 약점을 노출시켜야 함
- 업데이트할 트랜잭션에 IMMEDIATE를 사용하는 것이 DEFERRED 업그레이드 비용보다 중요한 이유를 바로 보여줌
- CONCURRENT의 경우 기본 충돌이 실제로 크게 증가하지 않았기 때문에 이러한 결과가 매우 견고함
nUpdate=0, nScan=10
- 이 read-only 일괄 처리 트랜잭션은 Pessimistic concurrency control의 영향 보여줌
- 모든 트랜잭션에 IMMEDIATE를 기본값으로 설정하면 안 되는 이유를 보여줌
- CONCURRENT vs IMMEDIATE는 CONCURRENT 모드를 사용할 때 약간의 단점이 있음을 나타냄. "모든 경우에 성능이 약간 떨어짐"
- 하지만 CONCURRENT는 좋은 기본 옵션이 될 것임