LLM WikiAccess-protected knowledge portal
← 스터디 홈
138편 · 약 13분

Litestream 쓰기 가능 VFS: SQLite 페이지를 S3에 두고 관리형 DB 없이 단일 프로세스 애플리케이션을 운영하는 방법

# Litestream 쓰기 가능 VFS: SQLite 페이지를 S3에 두고 관리형 DB 없이 단일 프로세스 애플리케이션을 운영하는 방법

요약

SQLite는 파일 하나에 데이터를 저장하는 가장 단순한 관계형 데이터베이스다. 그러나 "파일 하나"가 로컬 디스크에 있어야 한다는 가정이, 재시작하면 스토리지가 초기화되는 컨테이너 환경과 근본적으로 충돌한다. Litestream은 원래 SQLite WAL을 실시간으로 S3에 복제해 백업을 제공하는 도구였다. 2026년 초 쓰기 가능 VFS(Virtual File System) 모드가 추가되면서 역할이 바뀌었다: 이제 SQLite 데이터베이스 파일 자체를 S3 버킷에 두고, 애플리케이션은 vfs=litestream 연결 문자열 하나로 마치 로컬 파일처럼 읽고 쓸 수 있다. 이 챕터는 LTX 파일 형식, LSM 트리 기반 압축 정책, 페이지 범위(Range) 요청, 단일 쓰기 충돌 감지 메커니즘을 구체적으로 살펴보고, 이 패턴이 어떤 워크로드에 적합하고 어디서 한계를 드러내는지 정리한다.


배경: 컨테이너 환경에서 SQLite의 실용적 한계

Fly.io, Railway, Render 같은 현대 PaaS 플랫폼은 컨테이너를 쉽게 배포하지만, 재배포·재시작 시 로컬 디스크 상태가 초기화된다. 영구 볼륨을 붙이면 해결되지만 추가 비용과 운영 복잡도가 생긴다.

이 환경에서 SQLite를 쓰려면 세 가지 중 하나를 선택해야 했다:

  1. 매 요청 전에 S3에서 전체 DB 파일을 내려받고, 쓰기 후 다시 업로드 — 느리고 충돌에 취약
  2. 외부 PostgreSQL/MySQL 서비스를 연결 — 비용 발생, 연결 풀 관리 필요
  3. Litestream으로 WAL 복제를 설정하고 시작 시 복원 — 배포 스크립트 복잡

쓰기 가능 VFS는 이 세 가지 우회로를 없애고 단일 연결 문자열로 문제를 해결한다.


LTX 파일 형식: WAL을 페이지 변경집합으로 대체

Litestream이 SQLite WAL을 복제하던 초기 구현은 원시 WAL 세그먼트를 S3에 올렸다. WAL 세그먼트는 쓰기 순서대로 누적되므로, 특정 시점의 페이지를 찾으려면 처음부터 WAL을 재생해야 했다.

LTX(Litestream Transaction) 형식은 이 구조를 바꿨다. LTX 파일의 핵심 특성은 다음과 같다.

페이지 번호 기준 정렬: 각 LTX 파일 안에서 변경된 페이지들은 페이지 번호 순서로 정렬된다. 파일 끝에는 페이지 번호 → (파일 내 바이트 오프셋)을 담은 트레일러 인덱스가 붙는다. 특정 페이지 조회 시 트레일러를 읽으면 전체 파일을 스캔하지 않아도 정확한 위치를 알 수 있다.

트랜잭션 경계 보존: 하나의 SQLite 트랜잭션이 수정한 페이지 집합이 하나의 LTX 파일로 묶인다. 이를 통해 원자적 복구가 가능하다.

에포크(epoch)와 TXID: 각 LTX 파일은 에포크와 트랜잭션 ID를 헤더에 포함한다. 에포크는 DB가 새로 생성되거나 롤백될 때 증가하는 단조 증가 값으로, 충돌 감지에 쓰인다.


LSM 트리 기반 압축 정책

VFS 모드에서 쓰기가 발생할 때마다 LTX 파일이 S3에 업로드된다. 시간이 지나면서 수천 개의 작은 LTX 파일이 생기면 복원 시 순차 읽기 횟수가 늘어난다. Litestream은 LSM 트리(Log-Structured Merge tree)와 유사한 계층적 압축 정책으로 이를 관리한다.

L0 — 트랜잭션 즉시 업로드
tx-001.ltx
tx-002.ltx
tx-003.ltx
각 트랜잭션 직후 업로드 (~1초 이내)
↓ 30초 간격 압축
L1 — 30초 세그먼트
seg-30s-001.ltx
seg-30s-002.ltx
L0 파일 여러 개 → 하나로 병합
↓ 5분 간격 압축
L2 — 5분 세그먼트
seg-5m-001.ltx
↓ 1시간 간격 압축
L3 — 스냅샷 + 시간별 세그먼트
snapshot.ltx
seg-1h-001.ltx
전체 스냅샷 + 이후 증분 파일
LTX 파일 압축 계층 구조

압축은 L0 파일들을 읽어 페이지 번호별로 최신 버전만 유지하며 병합한다. 덮어쓰인 구버전 페이지는 삭제된다. 복원 시에는 가장 최근 스냅샷 하나와 그 이후의 증분 세그먼트 파일들만 읽으면 된다.


쓰기 가능 VFS의 동작 원리

페이지 읽기: S3 Range 요청

SQLite가 페이지 번호 N을 읽으려 할 때 VFS 계층이 개입한다. VFS는 최신 LTX 파일들로 구축한 인메모리 인덱스를 조회해 페이지 N이 위치한 S3 파일명과 바이트 오프셋을 찾는다. 그런 다음 HTTP Range: bytes=<start>-<end> 헤더로 해당 바이트 블록만 S3 API에 요청한다. 전체 DB 파일을 내려받는 것이 아니라 필요한 페이지만 가져온다.

페이지 쓰기: 로컬 버퍼 후 업로드

쓰기 활성화는 환경 변수 하나로 제어된다.

LITESTREAM_WRITE_ENABLED=true

쓰기가 활성화된 상태에서 SQLite 트랜잭션이 커밋되면:

  1. 변경된 페이지들이 로컬 쓰기 버퍼 파일에 먼저 기록된다.
  2. 버퍼 파일로부터 LTX 파일이 생성된다.
  3. LTX 파일이 S3에 업로드된다.
  4. 업로드 성공 후 버퍼 파일이 정리된다.

업로드 실패(네트워크 오류 등)가 발생하면 트랜잭션 커밋이 오류로 반환된다. 내구성 창(durability window)은 마지막 성공 업로드 이후 약 1초다.

충돌 감지: 에포크 비교

단일 쓰기 제약을 강제하기 위해 VFS는 S3에 있는 최신 LTX 파일의 에포크·TXID를 자신이 알고 있는 값과 비교한다. 새 LTX 파일을 업로드하기 직전에 S3의 최신 상태를 확인해, 예상치 못한 새 파일이 있으면 다른 프로세스가 같은 레플리카에 썼다고 판단하고 업로드를 중단한다. 이 충돌 감지는 If-None-Match 또는 조건부 PUT을 사용하는 구현에 따라 다를 수 있다.


설정 예제

# litestream.yml
dbs:
  - path: /data/app.db
    replicas:
      - url: s3://my-bucket/app-replica
        retention: 72h
        sync-interval: 1s
# 컨테이너 시작 시 복원 + 애플리케이션 실행
litestream restore -if-replica-exists s3://my-bucket/app-replica /data/app.db
litestream replicate -exec "node server.js"

쓰기 VFS를 사용하는 경우:

db, err := sql.Open("sqlite3", "file:/data/app.db?vfs=litestream&_journal_mode=WAL")

LITESTREAM_WRITE_ENABLED=true 환경 변수가 없으면 이 연결은 읽기 전용으로 동작한다.


이 패턴이 적합한 워크로드

단일 쓰기, 재시작 허용 워크로드

  • 사용자별 SQLite DB를 가진 멀티테넌트 SaaS (테넌트당 1개 프로세스)
  • AI 에이전트의 중간 상태 저장소 (하나의 에이전트 인스턴스가 쓰고 다음 실행 시 이어받음)
  • 개인 도구, 내부 관리 페이지, 팀용 소규모 서비스

적합하지 않은 워크로드

  • 다중 쓰기 프로세스가 동시에 같은 DB에 접근해야 하는 경우
  • 밀리초 단위 응답이 필요한 고빈도 트랜잭션 (S3 PUT 지연이 p99에 영향을 줌)
  • 읽기 레이턴시에 민감한 쿼리가 많은 경우 (콜드 페이지마다 S3 Range 요청이 발생)

운영 체크리스트

  • [ ] LITESTREAM_WRITE_ENABLED=true 없이 읽기 전용 검증 후 쓰기를 활성화했는가?
  • [ ] S3 버킷에 버전 관리(Versioning) 또는 Object Lock을 설정해 의도치 않은 삭제를 방지했는가?
  • [ ] 레플리카 보존 기간(retention)을 서비스 RTO에 맞게 설정했는가?
  • [ ] 멀티 인스턴스 배포(수평 확장)를 시도하기 전에 단일 쓰기 제약을 인식하고 있는가?
  • [ ] 복원 절차를 실제 컨테이너에서 드릴(drill)해봤는가?
  • [ ] S3 요청 비용(GET 수가 DB 읽기 패턴에 따라 증가할 수 있음)을 모니터링하는가?

References

  • Litestream 공식 사이트 — VFS Extension Reference: https://litestream.io/reference/vfs/
  • Litestream VFS Write Mode Guide: https://litestream.io/guides/vfs-write-mode/
  • Fly.io Blog — Litestream Writable VFS: https://fly.io/blog/litestream-writable-vfs/
  • Fly.io Blog — Litestream VFS (read mode): https://fly.io/blog/litestream-vfs/
  • bex.co — Litestream Writable VFS: SQLite Mounted From an S3 Bucket (2026-08-07): https://bex.co/blog/2026/08/07/litestream-writable-vfs-sqlite-s3-self-hosted-paas
  • GitHub — benbjohnson/litestream: https://github.com/benbjohnson/litestream