
S3에서의 100ms 미만 콜드 JOIN 쿼리와 페이지 수준 압축 및 암호화를 제공하는 SQLite VFS
turbolite는 Rust로 작성된 SQLite VFS로, S3에서 직접 포인트 조회와 조인을 250ms 미만의 콜드 레이턴시로 제공합니다.
이 리포지토리는 두 개의 크레이트로 구성된 Cargo 워크스페이스입니다:
turbolite — 순수 Rust 라이브러리. 페이지 수준 압축, 암호화 및 S3 계층화를 지원하는 SQLite VFS입니다.turbolite-ffi — C FFI / 로드 가능한 확장 + 언어 바인딩 (Python, Node.js, Go).또한 저장 시 효율성과 보안을 위해 페이지 수준 압축(zstd) 및 암호화(AES-256)를 제공하며, 이는 S3와 별도로 사용할 수 있습니다.
실험적. turbolite는 활발히 개발 중이며 버그가 포함되어 있습니다. 주의하세요.
객체 스토리지가 빨라지고 있습니다. S3 Express One Zone은 한 자리 수 밀리초의 GET을 제공하며, Tigris도 매우 빠릅니다. 로컬 디스크와 클라우드 스토리지 간의 격차가 줄어들고 있으며, turbolite는 이를 활용합니다.
디자인과 이름은 turbopuffer가 클라우드 스토리지 제약 조건을 과감하게 설계하는 접근 방식에서 영감을 받았습니다. 이 프로젝트의 초기 목표는 Neon의 500ms 이상 콜드 스타트를 능가하는 것이었습니다. 목표 달성.
서버당 하나의 데이터베이스가 있다면 볼륨을 사용하세요. turbolite는 수백 또는 수천 개의 데이터베이스(테넌트당 하나, 워크스페이스당 하나, 디바이스당 하나)를 갖고 각각에 볼륨을 원하지 않으며 단일 쓰기 소스에 괜찮은 경우를 탐색합니다.
turbolite는 Rust 라이브러리, SQLite 로드 가능한 확장 (.so/.dylib), Python 및 Node.js용 언어 패키지, 그리고 Go용 Github 의존성으로 제공됩니다. 모든 S3 호환 스토리지가 작동합니다(AWS S3, Tigris, R2, MinIO 등). 페이지 수준에서 작동하는 표준 SQLite VFS이므로 대부분의 SQLite 기능이 작동해야 합니다: FTS, R-tree, JSON, WAL 모드 등.
turbolite는 더 넓은 hadb 생태계의 일부입니다. 독립형 turbolite는 하나의 안전한 작성자를 가진 스토리지 VFS입니다. HA 리더 선출과 지속적인 WAL 복제를 원한다면, 그 위에 HaQLite와 walrust를 계층화하는 haqlite-turbolite를 통해 사용하세요. 그 HA 경로는 아직 매우 실험적입니다.
turbolite에 기여하고 싶거나 버그를 발견하면 풀 리퀘스트를 생성하거나 이슈를 열어주세요.
| Query | Type | Cold (S3 Express) | Cold (Tigris) |
|---|---|---|---|
| Post + user | point lookup + join | 86ms | 172ms |
| Profile | multi-table join (5 JOINs) | 251ms | 479ms |
| Who-liked | index search + join | 206ms | 302ms |
| Mutual friends | multi-search join | 19ms | 49ms |
| Indexed filter | covered index scan | 79ms | 88ms |
| Full scan + filter | full table scan | 476ms | 532ms |
1M posts / 100K users (~1.5GB stored) with nothing cached, every byte from S3. EC2 c5.2xlarge + S3 Express One Zone (same AZ, ~4ms GET latency). Fly performance-8x + Tigris (~25ms GET latency). Both: 8 dedicated vCPU, 16GB RAM, 7 prefetch worker threads. See Benchmarking and Storage backend matters.
Benchmarks are organized by cache level (what's already on local disk when the query runs):
| Cache level | What's cached | What's fetched from S3 | When this happens |
|---|---|---|---|
| none | nothing | everything | Fresh start, empty cache |
| interior | interior B-tree pages | index + data pages | First query after connection open |
| index | interior + index pages | data pages only | Normal turbolite operation |
| data | everything | nothing | Equivalent to local SQLite |
interior is the most realistic cold benchmark: interior pages load eagerly on connection open, so by the time you run your first query, they're cached. Index pages aggressively prefetch on first access in the background and may not be ready yet.
100K rows, Fly.io performance-2x (dedicated vCPU, NVMe, IAD):
| Operation | SQLite | turbolite | Overhead |
|---|---|---|---|
| Point lookup | 145K/s | 73K/s | 2.0x |
| Range scan | 8.8K/s | 8.3K/s | parity |
| Full table scan | 56/s | 60/s | parity |
| INSERT | 19K/s | 23K/s | parity |
| UPDATE by PK | 40K/s | 27K/s | 1.5x |
| Batch INSERT (in txn) | 685K/s | 740K/s | parity |
Point lookups have the highest per-page overhead (~2x). Everything else approaches or beats parity. Lock-free cache architecture means concurrent reads never block writes.
| After | Local | S3 (same-region RustFS) |
|---|---|---|
| 1K inserts | 19ms | 38ms |
| 10K batch | 17ms | 114ms |
| 1K updates | 9ms | 36ms |
Writes are always local-speed. The S3 cost is at checkpoint only. Numbers with RustFS in same Fly region (~2ms RTT). S3 Express One Zone would be comparable.
pip install turbolite
(No content provided for translation.)```python
import turbolite
conn = turbolite.connect("my.db", mode="s3",
bucket="my-bucket",
endpoint="https://t3.storage.dev")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'alice', '[email protected]')")
conn.commit()
alice = conn.cursor().execute("SELECT * FROM users").fetchone()
print(alice[1])
>>> "alice"
설치는 Installation을 참조하세요(Node, Go, Rust, 로컬 전용 모드, .so 로더블 확장 직접 사용).
turbolite는 파일 시스템 제약보다 S3의 제약에 맞게 설계되었습니다. 모든 결정은 이 모델에서 비롯됩니다:
| S3 제약 | 영향 |
|---|---|
| 왕복 요청이 느림 | 요청 횟수를 최소화합니다. 쓰기를 일괄 처리하고, 읽기를 적극적으로 프리페치합니다. |
| 대역폭이 병목임 | 대역폭 활용도를 최대화합니다. |
| PUT 및 GET은 작업당 비용이 부과됨 | 64KB GET과 16MB GET의 비용이 동일합니다. 바이트 효율이 아닌 요청 횟수를 최적화합니다. |
| 객체는 불변임 | 제자리에서 업데이트하지 않습니다. 새 버전을 쓰고 포인터를 교체합니다. 부분 쓰기 손상이 없습니다. |
| 스토리지는 저렴함 | 공간을 최적화하지 않습니다. 과잉 프로비저닝하고, 이전 버전을 유지하며, GC가 나중에 정리하도록 합니다. |
turbolite는 SQLite와 S3 사이에 페이지를 효율적으로 그룹화, 압축, 추적 및 가져오는 인트로스펙션 및 간접 계층을 추가합니다.
SQLite는 B-트리 인덱스를 사용하며 한 번에 한 페이지씩 요청합니다. 페이지 N이 바이트 오프셋 N * page_size에 있다는 것을 알고 있습니다. 그리고 이러한 페이지들은 효율적인 랜덤 액세스를 위해 페이지맵 전체에 무작위로 분산되어 있습니다. 그러나 S3에서는 요청당 한 페이지를 가져오면 쿼리당 수천 개의 잠재적으로 무작위적인 GET이 발생할 수 있습니다.
그러나 페이지는 동등하게 생성되지 않습니다. SQLite에는 다양한 유형의 페이지가 있습니다. turbolite는 유형별로 페이지 그룹을 분리합니다: 내부 B-트리, 인덱스 리프, 데이터 리프 페이지.
내부 페이지는 모든 쿼리에서 리프 페이지로의 조회를 라우팅하기 위해 접근됩니다. turbolite는 이를 감지하여 S3에 압축된 번들로 저장하고 VFS 열기 시점에 적극적으로 로드합니다. 그 후에는 모든 B-트리 탐색이 캐시 히트가 됩니다.
인덱스 리프 페이지도 동일한 처리를 받습니다: 별도의 번들, 지연된 백그라운드 프리페치, 축출에 고정됩니다. 콜드 쿼리는 데이터 페이지만 가져오면 됩니다.
turbolite는 B-트리 인트로스펙션을 활용하여 페이지가 속한 *트리(테이블 또는 인덱스)*를 파악하고, 해당 페이지들을 S3에 페이지 그룹으로 지능적으로 저장합니다: 여러 페이지가 하나의 S3 객체로 청크됩니다. 프리페치 시 대역폭을 포화시킬 만큼 크지만, 포인트 쿼리에는 충분히 작습니다. 기본값: 그룹당 256페이지, 64KB 페이지에서 ~16MB.
동일한 테이블/인덱스를 함께 저장하면 콜드 쿼리에 대해 가능한 최소한의 GET 요청이 발생합니다.
turbolite는 매니페스트 파일로 페이지 조회를 간접화하여 모든 페이지가 어디에 있는지에 대한 진실 공급원 역할을 합니다. SQLite의 암시적 offset = page * size를 명시적 포인터로 대체합니다. 이전 페이지 그룹 버전은 덮어쓰지 않습니다. 매니페스트 PUT이 원자적 커밋 지점입니다. 이전 버전은 가비지가 되어 gc()에 의해 정리됩니다.
SQLite는 기본적으로 4KB 페이지를 사용하여 파일 시스템 디스크 페이지 크기와 일치시킵니다. S3에서는 디스크 페이지 크기가 관련이 없습니다. 중요한 것은 요청 횟수를 최소화하고 B-트리 팬아웃을 최대화하는 것입니다. 답은 큰 페이지입니다: turbolite는 기본적으로 64KB 페이지를 사용합니다. 페이지 수가 적을수록 리프에 도달하기 위한 S3 왕복 요청이 줄어듭니다.
포인트 쿼리를 빠르게 만들기 위해 turbolite는 탐색 가능 압축을 사용합니다: 각 페이지 그룹은 여러 개의 zstd 프레임(그룹당 약 4페이지)으로 인코딩됩니다. 매니페스트는 프레임당 바이트 오프셋을 저장하므로, 캐시 미스 시 전체 그룹이 아닌 S3 범위 GET을 통해 필요한 페이지가 있는 ~256KB 하위 청크만 가져옵니다.
프리페치는 두 가지 계층이 있습니다: 사전 예방적(쿼리 계획 선행 실행) 및 반응적(적응형 미스 기반).
쿼리 계획 선행 실행이 먼저 실행됩니다. 쿼리가 실행되기 전에 turbolite는 EXPLAIN QUERY PLAN을 통해 SQLite 쿼리 계획을 가로채서 쿼리가 접근할 정확한 테이블과 인덱스를 추출하고, 첫 페이지가 읽히기도 전에 해당 페이지 그룹을 모두 프리페치 풀에 제출합니다. 그렇지 않으면 다섯 개의 테이블 조인이 다섯 번의 순차적인 미스 후 페치 사이클을 유발할 것을, 쿼리 시작 시점에 다섯 번의 페치를 모두 병렬로 실행합니다. SCAN 쿼리의 경우 전체 테이블이 처음부터 프리페치됩니다.
주의: SQLite는 연결당 하나의 트레이스 콜백을 지원합니다. 다른 확장이 먼저 슬롯을 차지하면 선행 실행은 자동으로 반응형 프리페치로 대체됩니다.
반응형 프리페칭은 선행 실행이 놓친 부분을 처리하고 폴백으로 작동합니다. 캐시 미스 시 두 가지가 동시에 발생합니다:
미스 카운터는 전역이 아닌 B-트리별로 추적됩니다. users(미스 1) 그런 다음 posts(미스 1)를 조회하는 프로필 쿼리는 각 트리를 1로 올바르게 추적하며 2로 추적하지 않습니다. 이는 여러 테이블 조인이 여러 트리를 접촉한다고 해서 실수로 모든 트리에서 프리페치를 증가시키는 것을 방지합니다.