TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 39 READS

SQLite를 프로덕션 DB로: WAL·동시성·VFS 튜닝의 실제

SQLite는 오랫동안 모바일 클라이언트, IoT 기기, 로컬 개발 환경에 내장되는 '가벼운 도구'로 취급됐다. 제대로 된 웹 서비스라면 PostgreSQL이나 MySQL 같은 클라이언트-서버형 데이터베이스를 써야 한다는 것이 오랜 통념이었다. 그러나 이 전제는 하드웨어 지형의 변화를 반영하지 못한다. 고속 NVMe SSD가 보편화되고 단일 테넌트 엣지 배포가 늘면서, 이제 병목은 쿼리 자체가 아니라 네트워크 왕복 지연으로 옮겨갔다. 애플리케이션 프로세스와 같은 서버에서 SQLite를 직접 구동하면 이 네트워크 오버헤드가 사라지고, 읽기는 메모리 매핑된 파일 연산에 가까워져 밀리초 이하의 쿼리 실행이 가능해진다.

다만 SQLite를 프로덕션에 올리려면 사고방식 자체를 바꿔야 한다. 기본 설정은 고성능이 아니라 안전성과 호환성을 최우선으로 맞춰져 있기 때문이다. 진짜 성능을 끌어내려면 Write-Ahead Logging(WAL), 잠금 상태, 캐시 관리, 그리고 가상 파일 시스템(VFS) 계층 같은 내부 메커니즘을 이해하고 손대야 한다.

롤백 저널에서 WAL로

SQLite의 기본 저널 방식은 롤백 저널이다. 쓰기가 일어나기 전에 원본 데이터베이스 페이지를 별도 저널 파일로 복사해 두고, 트랜잭션이 성공하면 저널을 삭제하고 실패하면 이를 이용해 원상 복구한다. 안전하지만 동시성 측면에서 치명적이다. 쓰기가 읽기를 막고 읽기가 쓰기를 막아, 쓰기 중에는 한 번에 하나의 연결만 데이터베이스에 접근할 수 있다.

동시성이 높은 애플리케이션 서버를 만들려면 WAL 모드를 켜야 한다. WAL 모드에서는 메인 데이터베이스 파일을 직접 수정하지 않고 새 트랜잭션을 별도의 .sqlite-wal 파일에 덧붙인다. 이 구조 덕분에 읽기와 쓰기가 동시에 진행될 수 있다. WAL 파일에 쌓인 변경 사항을 본 파일에 반영하는 과정이 체크포인트인데, SQLite는 이를 자동으로 처리하지만 기본 동작은 지연 스파이크를 유발할 수 있다. 특히 쓰기량이 많은 서버에서 항상 활성 리더가 존재하면 WAL 파일이 무한히 커질 수 있으므로, 백그라운드 스레드나 프로세스에서 PASSIVE 또는 RESTART 체크포인트를 정해진 간격으로 명시적으로 실행하는 편이 안전하다.

디스크 동기화 병목을 줄이려면 WAL 모드에 synchronous=NORMAL 설정을 함께 쓴다. 이 모드에서는 모든 커밋마다 디스크에 동기화하지 않고 체크포인트 같은 결정적 순간에만 동기화한다. WAL 모드에서는 이것이 데이터 손상으로부터 안전한데, 서버가 갑자기 죽더라도 WAL에 남아 있던 미커밋 트랜잭션만 잃을 뿐 데이터베이스 무결성은 유지되기 때문이다.

단일 라이터 제약과 SQLITE_BUSY

WAL이 동시 읽기·쓰기를 허용하더라도 SQLite는 여전히 단일 라이터 모델을 강제한다. 어느 순간에도 쓰기 트랜잭션은 하나뿐이며, 이미 쓰기가 진행 중일 때 두 번째 연결이 쓰기를 시도하면 즉시 SQLITE_BUSY 오류가 반환된다. 따라서 연결 풀과 트랜잭션 로직이 이 제약을 우아하게 흡수하도록 설계해야 한다.

핵심은 busy timeout을 반드시 설정하는 것이다. 이 값을 주면 SQLite가 예외를 던지기 전에 지정된 시간 동안 내부적으로 지수 백오프 알고리즘으로 잠금 획득을 재시도한다. 이것만으로도 부하 정점에서 애플리케이션 레벨 오류가 극적으로 줄어든다. 여기에 더해, 쓰기 연산을 포함하는 트랜잭션은 항상 BEGIN IMMEDIATE TRANSACTION으로 시작하는 것이 좋다. 처음부터 쓰기 잠금을 확보해 트랜잭션 중간에 잠금 충돌이 나는 상황을 피하기 위해서다.

메모리와 mmap, 그리고 VFS

캐시 설정은 서버가 수행하는 디스크 I/O 횟수에 직접 영향을 준다. 기본 캐시는 대략 2MB로 매우 작으므로, 프로덕션에서는 작업 집합이 메모리에 머물도록 cache_size 프라그마로 키워야 한다. 양수 값은 페이지 수를, 음수 값은 KiB 단위 크기를 의미한다. 또한 mmap_size를 설정하면 표준 read()/write() 시스템 콜 대신 데이터베이스 파일을 프로세스 가상 주소 공간에 직접 매핑해, 커널이 페이지 캐싱을 관리하고 유저 공간 버퍼 복사를 건너뛴다. 데이터베이스 크기가 mmap_size보다 작으면 전체가 메모리에 매핑되어 디스크 읽기가 사실상 포인터 연산으로 바뀐다.

SQLite의 가장 강력한 구조적 특징 중 하나는 VFS 추상화다. SQLite는 OS 파일 시스템에 직접 쓰지 않고 open, read, write, sync 같은 모든 파일 연산을 VFS 모듈에 위임한다. 이 계층을 커스터마이즈하면 데이터를 어떻게, 어디에 저장할지 바꿀 수 있어 현대적인 복제 엔진들이 이 위에서 만들어졌다. 특히 로컬 디스크가 휘발성인 클라우드 환경(AWS ECS, 쿠버네티스, Fly.io 등)에서는 VFS 기반 복제 도구를 반드시 병행해야 내구성과 고가용성을 확보할 수 있다. 실무에서는 애플리케이션 부트스트랩 단계에서 연결을 열 때마다 이 프라그마들을 하나의 시퀀스로 실행해 초기화하는 것이 정석이다.

결국 판단 기준은 워크로드의 성격이다. 여러 지역에 걸친 복잡한 분산 쓰기 트랜잭션이 필요하거나 데이터셋이 수 테라바이트를 넘어선다면 PostgreSQL 같은 시스템이 여전히 옳은 선택이다. 그러나 읽기 중심이고 수백 기가바이트 안에 들어오며 초저지연이 중요한 시스템이라면, WAL과 메모리 매핑, 올바른 트랜잭션 경계로 잘 튜닝된 SQLite 하나가 평범한 VPS에서 하루 수백만 쿼리와 수백 건의 동시 요청을 감당한다. 운영이 단순하고 비용 효율이 높다는 점까지 고려하면, SQLite를 애플리케이션 서버에 직접 얹는 선택지는 더 이상 '장난감'의 영역이 아니다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://micrologics.org/blog/sqlite-in-production-optimizing...
SHARE
처리 중...