본문으로 건너뛰기

© 2026 Molayo

HN요약2026. 05. 20. 02:16

Show HN: Turbolite – S3에서 250ms 미만의 Cold JOIN 쿼리를 제공하는 SQLite VFS

요약

Turbolite는 Rust로 작성된 SQLite VFS로, S3와 같은 객체 스토리지에서 250ms 미만의 낮은 Cold latency로 포인트 룩업 및 조인 쿼리를 수행할 수 있게 해줍니다. 페이지 레벨 압축(zstd)과 암호화(AES-256)를 지원하며, 다양한 언어 바인딩을 통해 S3 호환 스토리지에서 SQLite를 효율적으로 사용할 수 있도록 설계되었습니다.

핵심 포인트

  • S3에서 250ms 미만의 Cold latency로 쿼리 실행 가능
  • Rust 기반의 SQLite VFS로 페이지 레벨 압축(zstd) 및 암호화(AES-256) 지원
  • Python, Node.js, Go 등 다양한 언어 바인딩 및 C FFI 제공
  • AWS S3, Tigris, R2, MinIO 등 모든 S3 호환 스토리지와 호환
  • SQLite의 주요 기능(FTS, R-tree, JSON, WAL 등) 대부분 지원

turbolite

turbolite는 Rust로 작성된 SQLite VFS로, S3에서 직접 포인트 룩업 (point lookups) 및 조인 (joins)을 250ms 미만의 Cold latency로 제공합니다.

이 저장소는 두 개의 크레이트 (crates)로 구성된 Cargo 워크스페이스입니다:

  • turbolite — 순수 Rust 라이브러리. 페이지 레벨 압축 (page-level compression), 암호화 (encryption) 및 S3 티어링 (tiering) 기능을 갖춘 SQLite VFS.
  • turbolite-ffi — C FFI / 로드 가능한 확장 기능 (loadable extension) + 언어 바인딩 (Python, Node.js, Go).

또한 효율성과 저장 시 보안 (security at rest)을 위해 페이지 레벨 압축 (zstd) 및 암호화 (AES-256)를 제공하며, 이는 S3와 별개로 사용할 수 있습니다.

실험적 (Experimental). turbolite는 활발히 개발 중이며 버그가 포함되어 있을 수 있습니다. 주의하십시오.

객체 스토리지 (Object storage)는 점점 빨라지고 있습니다. S3 Express One Zone은 한 자릿수 밀리초(ms) 단위의 GET을 제공하며, Tigris 또한 매우 빠릅니다. 로컬 디스크와 클라우드 스토리지 사이의 격차는 줄어들고 있으며, turbolite는 이를 활용합니다.

설계와 이름은 클라우드 스토리지 제약 사항에 맞춰 무자비하게 아키텍처를 설계하는 turbopuffer의 접근 방식에서 영감을 받았습니다. 이 프로젝트의 초기 목표는 Neon의 500ms 이상 소요되는 Cold starts를 이기는 것이었습니다. 목표를 달성했습니다.

서버당 하나의 데이터베이스를 사용한다면 볼륨 (volume)을 사용하십시오. turbolite는 수백 또는 수천 개의 데이터베이스(테넌트당 하나, 워크스페이스당 하나, 기기당 하나)를 보유하면서도, 각 데이터베이스마다 볼륨을 할당하고 싶지 않고, 단일 쓰기 소스 (single write source)를 허용하는 방법을 탐구합니다.

turbolite는 Rust 라이브러리, SQLite 로드 가능한 확장 기능 (.so/.dylib), PythonNode.js용 언어 패키지, 그리고 Go를 위한 Github 의존성으로 제공됩니다. 모든 S3 호환 스토리지(AWS S3, Tigris, R2, MinIO 등)에서 작동합니다. 이는 페이지 레벨에서 작동하는 표준 SQLite VFS이므로, FTS, R-tree, JSON, WAL 모드 등 대부분의 SQLite 기능이 작동해야 합니다.

turbolite는 더 넓은 hadb 생태계의 일부이기도 합니다. 단독(Standalone) turbolite는 하나의 안전한 쓰기 작업자(safe writer)를 가진 스토리지 VFS입니다. 만약 고가용성(HA) 리더 선출(leader election)과 지속적인 WAL 복제(replication)를 원한다면, HaQLite와 walrust를 그 위에 계층화한 haqlite-turbolite를 통해 사용하세요. 해당 HA 경로는 매우 실험적이지만, 멀티 노드/장애 조치(failover) 작업이 지향해야 할 방향입니다.

turbolite에 기여하거나 버그를 발견하면, 풀 리퀘스트(pull request)를 생성하거나 이슈(issue)를 열어주세요.

성능 (Performance)

쿼리유형Cold (S3 Express)Cold (Tigris)
Post + user포인트 룩업 (point lookup) + 조인 (join)77ms192ms
...
캐시된 것이 아무것도 없는 상태에서 100만 행, 1.5GB 데이터이며, 모든 바이트를 S3에서 가져옵니다. EC2 c5.2xlarge + S3 Express One Zone (동일 AZ, 약 4ms GET 지연 시간). Fly 성능의 8배 + Tigris (약 25ms GET 지연 시간). 공통 사양: 8개의 전용 vCPU, 16GB RAM, 8개의 프리페치(prefetch) 스레드. 벤치마킹스토리지 백엔드의 중요성을 참조하세요.

벤치마크는 캐시 수준 (cache level) (쿼리가 실행될 때 로컬 디스크에 이미 있는 것)에 따라 분류됩니다:

캐시 수준캐시된 항목S3에서 가져오는 항목발생하는 시점
none없음모든 것새로 시작, 빈 캐시
...
interior는 가장 현실적인 콜드(cold) 벤치마크입니다. interior 페이지는 연결이 열릴 때 조기에 로드(eagerly load)되므로, 첫 번째 쿼리를 실행할 때쯤에는 이미 캐시되어 있습니다. 인덱스(Index) 페이지는 첫 접근 시 백그라운드에서 공격적으로 프리페치(prefetch)를 수행하며, 아직 준비되지 않았을 수도 있습니다.

웜 캐시 (Warm cache) (VFS 오버헤드 vs 일반 SQLite)

100K 행, Fly.io 성능의 2배 (전용 vCPU, NVMe, IAD):

작업 (Operation)SQLiteturbolite오버헤드 (Overhead)
포인트 룩업 (Point lookup)145K/s73K/s2.0x
...

포인트 룩업 (Point lookups)은 페이지당 오버헤드가 가장 높습니다 (~2x). 그 외의 모든 작업은 대등하거나 그 이상의 성능을 보여줍니다. 잠금 없는 캐시 (Lock-free cache) 아키텍처 덕분에 동시 읽기 작업이 쓰기 작업을 차단하지 않습니다.

체크포인트 비용 (Checkpoint cost)

이후 (After)로컬 (Local)S3 (동일 지역 RustFS)
1K 삽입 (inserts)19ms38ms
...

쓰기 작업은 항상 로컬 속도로 수행됩니다. S3 비용은 체크포인트 (checkpoint) 시에만 발생합니다. 수치는 동일한 Fly 지역 내의 RustFS (~2ms RTT) 기준입니다. S3 Express One Zone도 이와 유사한 성능을 보일 것입니다.

빠른 시작 (Quick Start)

Python

pip install turbolite
import turbolite

conn = turbolite.connect("my.db", mode="s3",
...

Node, Go, Rust, 로컬 전용 모드 및 .so 로드 가능한 확장 프로그램을 직접 사용하는 방법은 설치(Installation) 섹션을 참조하세요.

설계 (Design)

turbolite는 파일 시스템의 제약 조건보다 S3의 제약 조건을 고려하여 설계되었습니다. 모든 결정은 이 모델로부터 비롯됩니다:

S3 제약 조건 (Constraint)시사점 (Implication)
왕복 시간 (Round trips)이 느림요청 횟수를 최소화합니다. 쓰기를 배치(Batch) 처리하고, 읽기를 공격적으로 프리페치(prefetch)합니다.
...

아키텍처 (Architecture)

turbolite는 SQLite와 S3 사이에 페이지를 효율적으로 그룹화, 압축, 추적 및 가져오는 자기 성찰(introspection) 및 간접(indirection) 계층을 추가합니다.

SQLite는 B-tree 인덱스를 사용하며 한 번에 하나의 페이지를 요청합니다. SQLite는 N번째 페이지가 N * page_size 바이트 오프셋에 있다는 것을 알고 있습니다. 그리고 이러한 페이지들은 효율적인 임의 접근 (random access)을 위해 페이지 맵 (pagemap) 전체에 무작위로 분산되어 있습니다. 하지만 S3에서는 요청당 하나의 페이지를 가져오는 것이 쿼리당 수천 개의 잠재적으로 무작위적인 GET 요청을 의미하게 됩니다.

그러나 모든 페이지가 동일하게 생성되는 것은 아닙니다. SQLite에는 다양한 유형의 페이지가 있습니다. turbolite는 페이지 그룹을 유형별로 분리합니다: 내부 B-tree (interior B-tree), 인덱스 리프 (index leaf), 그리고 데이터 리프 (data leaf) 페이지입니다.

내부 페이지 (Interior pages)는 조회 (lookup)를 리프 페이지 (leaf pages)로 라우팅하기 위해 모든 쿼리에서 참조됩니다. turbolite는 이를 감지하여 S3에 압축된 번들 (bundles) 형태로 저장하며, VFS 오픈 시점에 이를 즉시 로드 (eagerly load)합니다. 그 이후의 모든 B-tree 순회 (traversal)는 캐시 히트 (cache hit)가 됩니다.

인덱스 리프 페이지 (Index leaf pages)도 동일한 처리를 받습니다: 별도의 번들로 구성되며, 지연된 백그라운드 프리페치 (lazy background prefetch)를 수행하고, 교체 (eviction)되지 않도록 고정 (pinned)됩니다. 콜드 쿼리 (Cold queries)는 데이터 페이지 (data pages)만 가져오면 됩니다.

turbolite는 **B-tree 내성 (B-tree introspection)**을 활용하여 특정 페이지가 어떤 트리(테이블 또는 인덱스)의 일부인지를 파악하며, 해당 페이지들을 S3에 **페이지 그룹 (page groups)**으로서 지능적으로 함께 저장합니다. 페이지 그룹은 여러 페이지를 하나의 S3 객체로 묶은 것입니다. 이는 프리페치 시 대역폭 (bandwidth)을 포화시킬 만큼 충분히 크면서도, 포인트 쿼리 (point queries)를 수행하기에는 충분히 작습니다. 기본값은 그룹당 256개 페이지이며, 64KB 페이지 기준 약 16MB입니다.

동일한 테이블/인덱스를 함께 저장한다는 것은 콜드 쿼리에 대해 가능한 최소한의 GET 요청만을 수행함을 의미합니다.

turbolite는 모든 페이지가 어디에 존재하는지에 대한 신뢰할 수 있는 원천 (source of truth)인 **매니페스트 파일 (manifest file)을 통해 페이지 조회를 간접화 (indirects)**합니다. 이는 SQLite의 암시적인 offset = page * size 방식을 명시적인 포인터 (explicit pointers)로 대체합니다. 이전 페이지 그룹 버전은 절대 덮어쓰이지 않으며, 매니페스트의 PUT 작업이 원자적 커밋 지점 (atomic commit point)이 됩니다. 이전 버전들은 가비지 (garbage)가 되며, gc()에 의해 정리됩니다.

SQLite는 파일 시스템의 디스크 페이지 크기에 맞추기 위해 기본적으로 4KB 페이지를 사용합니다. 하지만 S3에서는 디스크 페이지 크기가 무의미합니다. 중요한 것은 요청 횟수를 최소화하고 B-tree 팬아웃 (fan-out)을 최대화하는 것입니다. 그 해답은 **대용량 페이지 (large pages)**입니다. turbolite는 기본적으로 64KB 페이지를 사용합니다. 페이지 수가 적을수록 리프에 도달하기 위한 S3 왕복 (round trips) 횟수가 줄어듭니다.

포인트 쿼리를 빠르게 만들기 위해, turbolite는 **탐색 가능한 압축 (seekable compression)**을 사용합니다. 각 페이지 그룹은 여러 개의 **zstd 프레임 (zstd frames)**으로 인코딩됩니다 (프레임당 약 4개 페이지). 매니페스트는 프레임당 바이트 오프셋 (byte offsets)을 저장하므로, 캐시 미스 (cache miss)가 발생하더라도 S3 범위 GET (range GET)을 통해 전체 그룹이 아닌 필요한 페이지가 포함된 약 256KB의 서브 청크 (sub-chunk)만 가져옵니다.

프리페칭 (Prefetching)에는 두 가지 계층이 있습니다: 선제적 (proactive) (쿼리 계획 (query-plan) 선행) 방식과 반응적 (reactive) (미스 기반 적응형 (adaptive miss-based)) 방식입니다.

Query-plan frontrunning (쿼리 계획 선행) 방식이 먼저 실행됩니다. 쿼리가 실행되기 전, turbolite는 EXPLAIN QUERY PLAN을 통해 SQLite 쿼리 계획을 가로채고, 쿼리가 건드릴 정확한 테이블과 인덱스를 추출하여 첫 번째 페이지가 읽히기도 전에 모든 페이지 그룹을 프리페치 풀 (prefetch pool)에 제출합니다. 평소라면 5번의 순차적인 '미스 후 가져오기 (miss-then-fetch)' 사이클을 유발했을 5개 테이블 조인(join)의 경우, 쿼리 시작 시점에 5개의 가져오기를 모두 병렬로 실행합니다. SCAN 쿼리의 경우, 이는 테이블 전체를 사전에 프리페치(prefetch)함을 의미합니다.

주의사항: SQLite는 연결(connection)당 하나의 트레이스 콜백 (trace callback)만 지원합니다. 만약 다른 확장 프로그램이 이 슬롯을 먼저 점유하면, 선행 방식은 조용히 반응적 프리페치 방식으로 전환됩니다.

Reactive prefetching (반응적 프리페치) 방식은 선행 방식이 놓친 부분을 처리하며 폴백 (fallback) 역할을 수행합니다. 캐시 미스 (cache miss)가 발생하면 다음 두 가지 작업이 동시에 일어납니다:

  1. Inline range GET (인라인 범위 GET): 필요한 페이지를 포함하는 특정 서브 청크 (sub-chunk)를 가져온 후 즉시 SQLite로 반환합니다.
  2. Background prefetch (백그라운드 프리페치): 스케줄에 따라 해당 트리의 형제 그룹 (sibling groups)을 프리페치 풀에 제출합니다.

미스 카운터 (miss counters)는 전역적으로가 아니라 B-tree별로 추적됩니다. users (미스 1)를 조회한 후 posts (미스 1)를 조회하는 프로파일 쿼리의 경우, 각 트리를 2가 아닌 1로 정확하게 추적합니다. 이는 다중 테이블 조인이 여러 테이블을 건드린다는 이유만으로 모든 트리에서 실수로 프리페치를 에스컬레이션 (escalating)하는 것을 방지합니다.

연속적인 미스가 발생할 때마다 동일 트리 그룹의 어느 정도 비율을 프리페치할지 제어하는 **프리페치 스케줄 (prefetch schedule)**을 따라 진행됩니다. turbolite는 쿼리 계획을 기반으로 스케줄을 자동으로 선택합니다:

  • Search schedule (검색 스케줄) [0.3, 0.3, 0.4]: 인덱스의 알 수 없는 부분을 스캔하는 SEARCH ... USING INDEX 쿼리용입니다. 인덱스의 어느 정도가 스캔될지 알 수 없으므로 첫 번째 미스부터 공격적으로 동작합니다.
  • Lookup schedule (조회 스케줄) [0.0, 0.0, 0.0]: 트리당 1~2개의 페이지를 건드리는 포인트 쿼리 (point queries) 및 인덱스 조회용입니다. 프리페치가 시작되기 전 세 번의 무료 홉 (free hops)을 제공합니다. 0이 많은 스케줄은 S3 Express와 Tigris 모두에서 초기 급증 (early-ramp) 방식보다 성능이 뛰어납니다.

TurboliteConfig에서 prefetch.search / prefetch.lookup을 설정함으로써 오픈 시점에 프리페치 스케줄 (prefetch schedule)을 조정할 수 있습니다. 예상되는 워크로드 형태 (workload shape)를 이미 알고 있으므로, VFS가 이를 추측할 필요가 없습니다. Configuring prefetch를 참조하세요.

두 스케줄 모두 B-tree introspection (B-tree 내부 조사)의 이점을 활용합니다. 프리페치된 모든 그룹은 올바른 트리의 페이지를 포함하는 것이 보장됩니다. 예를 들어, SQLite가 users 테이블에서 페이지를 요청한 후 동일한 테이블에서 다른 페이지를 요청하면, Turbolite는 스캔 (scan)이 이어질 것으로 가정하고 백그라운드에서 users 테이블의 나머지 부분만 프리페치하며 다른 것은 가져오지 않습니다. B-tree introspection이 없다면, 데이터가 디스크 상에서 서로 인접해 있다는 이유만으로 users 테이블의 절반과 posts 테이블의 절반을 실수로 가져오게 될 것입니다.

인메모리 페이지 캐시 (In-memory page cache)

turbolite는 SQLite의 내장 페이지 캐시를 대체하는 자체 인메모리 페이지 캐시를 가지고 있습니다. SQLite의 페이저 (pager)는 페이지를 내부적으로 캐싱하며, 캐싱된 페이지에 대해서는 VFS에서 다시 읽지 않습니다. 이는 단일 작성자 (single-writer) 데이터베이스에는 문제가 없지만, 읽기 복제본 (read replicas, HA followers, manifest-polling readers)의 경우 복제를 통해 기본 데이터가 변경되면 SQLite의 캐시가 오래된 상태 (stale)가 됩니다.

turbolite의 캐시는 매니페스트 인지 (manifest-aware) 방식입니다. set_manifest()가 실행되면 (복제를 통한 새로운 데이터 유입), 디스크 캐시와 인메모리 캐시 모두에서 영향을 받는 페이지를 무효화 (invalidate)합니다. 쓰기 작업 또한 인메모리 캐시 내의 해당 페이지를 무효화합니다. 이를 통해 복제 또는 쓰기 작업 이후에 신선한 (fresh) 읽기를 보장합니다.

아키텍처 (Architecture):

SQLite (PRAGMA cache_size=0)
  -> turbolite VFS xRead
    -> 인메모리 페이지 캐시 (기본 64MB, AtomicPtr, zero-lock 읽기)
...

설정 (Configuration):

  • TurboliteConfigcache.mem_budget (바이트 단위). 기본값: 64MB.
  • TURBOLITE_MEM_CACHE_BUDGET 환경 변수 (예: 128MB, 1GB).
  • 0으로 설정하면 인메모리 캐시를 완전히 비활성화합니다.

turbolite.connect() (Python/Go/TypeScript)는 SQLite의 페이지 캐시 (page cache)를 자동으로 비활성화하고 대신 turbolite의 캐시를 사용합니다. Connection::open_with_flags_and_vfs를 직접 사용하는 Rust 소비자들은 동일한 동작을 얻기 위해 PRAGMA cache_size=0을 설정해야 합니다.

암호화 (Encryption) & 압축 (Compression)

압축 (Compression)

모든 데이터는 저장 전에 zstd로 압축됩니다. 페이지 그룹 (page groups)은 각 프레임(frame, 약 4페이지, 약 256KB)을 독립적으로 압축하는 탐색 가능한 멀티 프레임 인코딩 (seekable multi-frame encoding)을 사용하여, 포인트 룩업 (point lookup) 시 전체 페이지 그룹이 아닌 관련 프레임만 압축 해제합니다. 사용자 정의 zstd 사전 (dictionaries)을 사용하면 압축률을 더욱 향상시킬 수 있습니다.

로컬 (S3가 아닌) 모드에서도 zstd를 사용하여 페이지 수준에서 압축을 수행합니다. 사전 학습 도구는 CLI를 참조하십시오.

암호화 (Encryption)

암호화가 활성화되면 turbolite는 S3 객체, 로컬 캐시, WAL, 메타데이터를 포함한 모든 것을 암호화합니다. S3 데이터는 프레임당 무작위 논스 (nonces)를 사용하는 AES-256-GCM (인증 및 변조 탐지 가능)을 사용합니다. 로컬 데이터는 크기 오버헤드가 없는 AES-256-CTR을 사용합니다. 암호화는 압축 후에 수행됩니다: 평문 (plaintext) → zstd → 암호화 (encrypt) → S3.

AI 자동 생성 콘텐츠

본 콘텐츠는 HN Design Systems의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0